From b6b2057df2c5c298d063f107883b4c6a28e26b03 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Sun, 26 Jul 2026 19:58:27 +0330 Subject: [PATCH] =?UTF-8?q?=D8=A7=D9=88=D9=84=DB=8C=D9=86=20=DA=A9=D8=A7?= =?UTF-8?q?=D9=85=DB=8C=D8=AA=20=D9=BE=D8=B1=D9=88=DA=98=D9=87=20vernova?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 57 +- .gitignore | 2 +- ..._expense_and_pettycash_missing_columns.php | 94 + AppServiceProvider.php | 40 + AuditLog.php | 49 + AuditLogMiddleware.php | 171 + Employee.php | 73 + Expense.php | 113 + INSTALL-WINDOWS-XAMPP.md | 465 + INSTALL.md | 340 + INSTALL.txt | 32 + InventoryTransactionPolicy.php | 63 + ItemPolicy.php | 63 + Kernel.php | 55 + PettyCash.php | 86 + PettyCashController.php | 220 + PettyCashPolicy.php | 98 + README.md | 92 +- app/Helpers/CalendarHelper.php | 138 + app/Helpers/CurrencyHelper.php | 96 + app/Helpers/JalaliHelper.php | 128 + app/Helpers/NumberHelper.php | 89 + app/Helpers/helpers.php | 126 + app/Http/Controllers/AttachmentController.php | 105 + app/Http/Controllers/AuthController.php | 83 + app/Http/Controllers/Controller.php | 32 +- app/Http/Controllers/DashboardController.php | 118 + app/Http/Controllers/DocumentController.php | 177 + app/Http/Controllers/EmployeeController.php | 216 + app/Http/Controllers/ExpenseController.php | 258 + app/Http/Controllers/InventoryController.php | 441 + .../InventoryTransactionController.php | 176 + app/Http/Controllers/LetterController.php | 258 + app/Http/Controllers/PayrollController.php | 467 + app/Http/Controllers/PettyCashController.php | 375 + app/Http/Controllers/ProjectController.php | 269 + app/Http/Controllers/ReportController.php | 201 + app/Http/Controllers/SettingsController.php | 178 + app/Http/Controllers/TaskController.php | 362 + app/Http/Controllers/WarehouseController.php | 117 + app/Http/Kernel.php | 67 + app/Http/Middleware/AuditLogMiddleware.php | 187 + app/Http/Middleware/Authenticate.php | 17 + app/Http/Middleware/ContentSecurityPolicy.php | 22 + app/Http/Middleware/EncryptCookies.php | 17 + app/Http/Middleware/EnsureEmailIsVerified.php | 26 + .../Middleware/RedirectIfAuthenticated.php | 27 + app/Http/Middleware/SetLocale.php | 77 + app/Http/Middleware/SetTenant.php | 117 + app/Http/Middleware/TenantScope.php | 61 + app/Http/Middleware/ValidateSignature.php | 17 + app/Http/Middleware/VerifyCsrfToken.php | 17 + app/Http/Requests/StoreEmployeeRequest.php | 58 + app/Http/Requests/StoreExpenseRequest.php | 54 + app/Http/Requests/StoreProjectRequest.php | 51 + app/Http/Requests/StoreTaskRequest.php | 52 + app/Models/Attachment.php | 119 + app/Models/AuditLog.php | 49 + app/Models/Concerns/HasAttachments.php | 135 + app/Models/Currency.php | 30 + app/Models/Document.php | 82 + app/Models/Employee.php | 92 + app/Models/EmployeeFinancial.php | 57 + app/Models/EmployeeSettlement.php | 70 + app/Models/Expense.php | 116 + app/Models/InventoryTransaction.php | 75 + app/Models/Item.php | 38 + app/Models/Letter.php | 66 + app/Models/LetterAttachment.php | 83 + app/Models/PayrollPeriod.php | 58 + app/Models/Permission.php | 15 + app/Models/PettyCash.php | 127 + app/Models/Project.php | 103 + app/Models/ProjectUser.php | 27 + app/Models/Role.php | 28 + app/Models/Task.php | 180 + app/Models/TaskLog.php | 39 + app/Models/Tenant.php | 66 + app/Models/User.php | 69 +- app/Models/Warehouse.php | 39 + app/Models/WorkLog.php | 66 + app/Policies/AttachmentPolicy.php | 68 + app/Policies/DocumentPolicy.php | 40 + app/Policies/EmployeePolicy.php | 76 + app/Policies/ExpensePolicy.php | 92 + app/Policies/InventoryTransactionPolicy.php | 55 + app/Policies/ItemPolicy.php | 55 + app/Policies/LetterPolicy.php | 55 + app/Policies/PettyCashPolicy.php | 86 + app/Policies/ProjectPolicy.php | 76 + app/Policies/TaskPolicy.php | 96 + app/Policies/WarehousePolicy.php | 63 + app/Providers/AppServiceProvider.php | 48 +- app/Providers/BladeServiceProvider.php | 40 + app/Providers/RouteServiceProvider.php | 36 + app/helpers.php | 67 + bootstrap/app.php | 13 +- bootstrap/providers.php | 2 + composer.json | 43 +- composer.lock | 9844 +++++++++++++++++ config/app.php | 2 +- config/database.php | 2 +- config/permission.php | 119 + config/projectra.php | 30 + config/projectra_app_additions.php | 32 + database/.gitignore | 1 - database/factories/UserFactory.php | 44 - .../0001_01_01_000000_create_users_table.php | 49 - .../0001_01_01_000001_create_cache_table.php | 35 - .../0001_01_01_000002_create_jobs_table.php | 57 - ...4_01_01_000001_create_currencies_table.php | 27 + ...2024_01_01_000002_create_tenants_table.php | 32 + .../2024_01_01_000003_create_users_table.php | 32 + ...000004_create_roles_permissions_tables.php | 57 + ...024_01_01_000005_create_projects_table.php | 36 + ...1_01_000006_create_project_users_table.php | 26 + ...24_01_01_000007_create_employees_table.php | 41 + ...24_01_01_000008_create_work_logs_table.php | 33 + ...01_01_000009_create_petty_cashes_table.php | 35 + ...024_01_01_000010_create_expenses_table.php | 40 + ...00011_create_employee_financials_table.php | 38 + .../2024_01_01_000012_create_items_table.php | 30 + ...4_01_01_000013_create_warehouses_table.php | 29 + ...14_create_inventory_transactions_table.php | 39 + .../2024_01_01_000015_create_tasks_table.php | 40 + ...24_01_01_000016_create_task_logs_table.php | 30 + ...2024_01_01_000017_create_letters_table.php | 38 + ...000018_create_letter_attachments_table.php | 28 + ...24_01_01_000019_create_documents_table.php | 32 + ...4_01_01_000020_create_audit_logs_table.php | 34 + ...1_000021_add_missing_columns_to_tables.php | 148 + ..._expense_and_pettycash_missing_columns.php | 94 + ..._01_01_000023_create_attachments_table.php | 33 + ..._000024_create_task_dependencies_table.php | 35 + ...025_add_missing_columns_to_tasks_table.php | 66 + ...0_225223_add_created_by_to_tasks_table.php | 28 + ...327_add_requested_by_to_expenses_table.php | 26 + ...and_request_date_to_petty_cashes_table.php | 24 + database/seeders/CurrencySeeder.php | 66 + database/seeders/DatabaseSeeder.php | 26 +- database/seeders/EmployeeSeeder.php | 297 + database/seeders/ExpenseSeeder.php | 376 + database/seeders/PermissionSeeder.php | 55 + database/seeders/ProjectSeeder.php | 169 + database/seeders/RolePermissionSeeder.php | 216 + database/seeders/TaskSeeder.php | 247 + database/seeders/TenantSeeder.php | 64 + database/seeders/UserSeeder.php | 103 + move-files.bat | 0 package-lock.json | 2546 +++++ package.json | 7 +- postcss.config.js | 6 + public/css/jalali-datepicker.css | 135 + public/js/jalali-datepicker.js | 326 + resources/css/app.css | 50 + resources/js/app.js | 63 +- resources/js/bootstrap.js | 4 - resources/lang/en.json | 761 ++ resources/lang/fa.json | 826 ++ resources/views/auth/login.blade.php | 82 + .../components/currency-display.blade.php | 7 + .../views/components/date-input.blade.php | 55 + .../views/components/localized-date.blade.php | 13 + .../views/components/priority-badge.blade.php | 25 + .../views/components/status-badge.blade.php | 39 + resources/views/dashboard/index.blade.php | 242 + resources/views/documents/form.blade.php | 55 + resources/views/documents/index.blade.php | 91 + resources/views/documents/show.blade.php | 45 + resources/views/employees/create.blade.php | 18 + resources/views/employees/edit.blade.php | 19 + .../views/employees/financials.blade.php | 113 + resources/views/employees/form.blade.php | 138 + resources/views/employees/index.blade.php | 78 + resources/views/employees/show.blade.php | 125 + resources/views/employees/work-logs.blade.php | 97 + .../views/expenses/expense-form.blade.php | 126 + .../views/expenses/expense-index.blade.php | 167 + .../views/expenses/expense-show.blade.php | 292 + resources/views/expenses/form.blade.php | 125 + resources/views/expenses/index.blade.php | 140 + resources/views/expenses/show.blade.php | 262 + resources/views/help/index.blade.php | 43 + .../inventory-transactions/form.blade.php | 199 + .../inventory-transactions/index.blade.php | 106 + .../inventory-transactions/show.blade.php | 103 + .../views/inventory/create-item.blade.php | 71 + .../inventory/create-transaction.blade.php | 77 + .../inventory/create-warehouse.blade.php | 48 + resources/views/inventory/form.blade.php | 132 + resources/views/inventory/index.blade.php | 157 + .../views/inventory/inventory-index.blade.php | 138 + resources/views/inventory/item-form.blade.php | 116 + resources/views/inventory/items.blade.php | 120 + resources/views/inventory/show.blade.php | 77 + .../views/inventory/transactions.blade.php | 467 + .../views/inventory/warehouse-form.blade.php | 76 + .../views/inventory/warehouses.blade.php | 99 + resources/views/layouts/app.blade.php | 96 + resources/views/layouts/header.blade.php | 113 + resources/views/layouts/sidebar.blade.php | 241 + resources/views/letters/form.blade.php | 141 + resources/views/letters/index.blade.php | 139 + resources/views/letters/show.blade.php | 224 + resources/views/payroll/calculate.blade.php | 170 + resources/views/payroll/index.blade.php | 179 + resources/views/payroll/settlement.blade.php | 148 + resources/views/payroll/show.blade.php | 151 + .../petty-cashes/PettyCashController.php | 255 + resources/views/petty-cashes/create.blade.php | 18 + .../views/petty-cashes/expense-form.blade.php | 112 + resources/views/petty-cashes/form.blade.php | 108 + resources/views/petty-cashes/index.blade.php | 164 + .../petty-cashes/petty-cash-index.blade.php | 165 + resources/views/petty-cashes/show.blade.php | 291 + resources/views/projects/form.blade.php | 118 + resources/views/projects/index.blade.php | 95 + resources/views/projects/show.blade.php | 208 + .../views/reports/employee-worklog.blade.php | 13 + .../views/reports/financial-pdf.blade.php | 295 + resources/views/reports/financial.blade.php | 30 + resources/views/reports/index.blade.php | 41 + resources/views/reports/inventory.blade.php | 13 + resources/views/reports/payroll.blade.php | 13 + resources/views/settings/index.blade.php | 8 + .../settings/partials/currencies.blade.php | 173 + .../settings/partials/organization.blade.php | 84 + .../views/settings/partials/users.blade.php | 203 + resources/views/tasks/form.blade.php | 113 + resources/views/tasks/gantt.blade.php | 150 + resources/views/tasks/index.blade.php | 135 + resources/views/tasks/kanban.blade.php | 153 + resources/views/tasks/show.blade.php | 158 + resources/views/warehouses/form.blade.php | 95 + resources/views/warehouses/index.blade.php | 92 + resources/views/warehouses/show.blade.php | 64 + resources/views/welcome.blade.php | 172 - routes/api.php | 79 + routes/console.php | 2 +- routes/letters-routes.php | 10 + routes/web.php | 185 +- sidebar-addition.txt | 16 + storage/framework/views/.gitignore | 2 - .../0815a55745834eb105f466067e7b4e86.php | 55 + .../0dc35d574885e9ed87093ad3cc8d1de5.php | 183 + .../15dc9805bd50ffbccd898a04c0572745.php | 183 + .../42fce2afce1274cac2c89c79739f9fc3.php | 118 + .../448a787e655d73dc75d1ab0fcc84b86f.php | 188 + .../46e7c1d03b179af2b30bd5f0a8a09de0.php | 269 + .../4ec001dbe225074190f1867b3ff08122.php | 8 + .../5625be61c087bf17bbc0b8985f4cf4b2.php | 30 + .../5c57a35cb8207405007d3ffb6056da18.php | 35 + .../63827679d7679f25341f7abe0c3ec203.php | 99 + .../65f6c99804a6a0edbd8042acc1b80c49.php | 250 + .../663a1a1fea235ffe5ad3f8ff45ecda31.php | 93 + .../7379735e393d6b0e3866bee72506a75c.php | 41 + .../86a592b10d7a65e83deaa9802eeb20fb.php | 163 + .../8978f0f93ca9bd010ca93ff26a7c11ff.php | 118 + .../90d43165439e87034f1fc57adfb2dd86.php | 150 + .../95971f91e644c2c50f9234aa4ccd2329.php | 103 + .../98463ca068fc45f46d96fd75afccf591.php | 168 + .../9b62b89896774f58ae7160b9f9c3f8f5.php | 7 + .../a03636601cf2476624659c1e63087b5d.php | 251 + .../a11f4e6f418ca75d82ec12f95844d120.php | 174 + .../a767be18f85f06a40736e43baa9c1c94.php | 100 + .../abc1086b0925e0d145a47b081ba4eb8c.php | 164 + .../b1e4ba57b9976591108a1c9b3ffe68a8.php | 83 + .../b3640ad9e480045721de9d9873d8e305.php | 131 + .../b70330c10889c719d7f5f7c4f8975d50.php | 235 + .../c2413d3aa9744e18fe67bd7a0d816197.php | 85 + .../cc7cee66c4891fd4b5e2d2b84bf0b502.php | 171 + .../cf61b670207f0f276880b2ae809d4364.php | 130 + .../d0cbbc094801c6a7d98901eb18134bcd.php | 99 + .../d1152c3fd7ed43b88a51d21a99ef2690.php | 173 + .../d2d7153ed22b6afa438b863b8619689d.php | 141 + .../d4837b0709b873e59cd24d661c7a1525.php | 148 + .../e3253f4ba065d81f5eeecfaef7fa0887.php | 159 + .../e3358e6b2ca544b1228f9c327b88a6e0.php | 125 + .../eb3da118726a3243dc4a5dd762b87bb8.php | 528 + .../ebeebd8922e18d3f7f95a6b1c67f8bfe.php | 117 + .../f65f51438d6e40d1a0e9db5fe3643c14.php | 158 + tailwind.config.js | 54 + vite.config.js | 5 + 283 files changed, 42722 insertions(+), 527 deletions(-) create mode 100644 2024_01_01_000022_add_expense_and_pettycash_missing_columns.php create mode 100644 AppServiceProvider.php create mode 100644 AuditLog.php create mode 100644 AuditLogMiddleware.php create mode 100644 Employee.php create mode 100644 Expense.php create mode 100644 INSTALL-WINDOWS-XAMPP.md create mode 100644 INSTALL.md create mode 100644 INSTALL.txt create mode 100644 InventoryTransactionPolicy.php create mode 100644 ItemPolicy.php create mode 100644 Kernel.php create mode 100644 PettyCash.php create mode 100644 PettyCashController.php create mode 100644 PettyCashPolicy.php create mode 100644 app/Helpers/CalendarHelper.php create mode 100644 app/Helpers/CurrencyHelper.php create mode 100644 app/Helpers/JalaliHelper.php create mode 100644 app/Helpers/NumberHelper.php create mode 100644 app/Helpers/helpers.php create mode 100644 app/Http/Controllers/AttachmentController.php create mode 100644 app/Http/Controllers/AuthController.php create mode 100644 app/Http/Controllers/DashboardController.php create mode 100644 app/Http/Controllers/DocumentController.php create mode 100644 app/Http/Controllers/EmployeeController.php create mode 100644 app/Http/Controllers/ExpenseController.php create mode 100644 app/Http/Controllers/InventoryController.php create mode 100644 app/Http/Controllers/InventoryTransactionController.php create mode 100644 app/Http/Controllers/LetterController.php create mode 100644 app/Http/Controllers/PayrollController.php create mode 100644 app/Http/Controllers/PettyCashController.php create mode 100644 app/Http/Controllers/ProjectController.php create mode 100644 app/Http/Controllers/ReportController.php create mode 100644 app/Http/Controllers/SettingsController.php create mode 100644 app/Http/Controllers/TaskController.php create mode 100644 app/Http/Controllers/WarehouseController.php create mode 100644 app/Http/Kernel.php create mode 100644 app/Http/Middleware/AuditLogMiddleware.php create mode 100644 app/Http/Middleware/Authenticate.php create mode 100644 app/Http/Middleware/ContentSecurityPolicy.php create mode 100644 app/Http/Middleware/EncryptCookies.php create mode 100644 app/Http/Middleware/EnsureEmailIsVerified.php create mode 100644 app/Http/Middleware/RedirectIfAuthenticated.php create mode 100644 app/Http/Middleware/SetLocale.php create mode 100644 app/Http/Middleware/SetTenant.php create mode 100644 app/Http/Middleware/TenantScope.php create mode 100644 app/Http/Middleware/ValidateSignature.php create mode 100644 app/Http/Middleware/VerifyCsrfToken.php create mode 100644 app/Http/Requests/StoreEmployeeRequest.php create mode 100644 app/Http/Requests/StoreExpenseRequest.php create mode 100644 app/Http/Requests/StoreProjectRequest.php create mode 100644 app/Http/Requests/StoreTaskRequest.php create mode 100644 app/Models/Attachment.php create mode 100644 app/Models/AuditLog.php create mode 100644 app/Models/Concerns/HasAttachments.php create mode 100644 app/Models/Currency.php create mode 100644 app/Models/Document.php create mode 100644 app/Models/Employee.php create mode 100644 app/Models/EmployeeFinancial.php create mode 100644 app/Models/EmployeeSettlement.php create mode 100644 app/Models/Expense.php create mode 100644 app/Models/InventoryTransaction.php create mode 100644 app/Models/Item.php create mode 100644 app/Models/Letter.php create mode 100644 app/Models/LetterAttachment.php create mode 100644 app/Models/PayrollPeriod.php create mode 100644 app/Models/Permission.php create mode 100644 app/Models/PettyCash.php create mode 100644 app/Models/Project.php create mode 100644 app/Models/ProjectUser.php create mode 100644 app/Models/Role.php create mode 100644 app/Models/Task.php create mode 100644 app/Models/TaskLog.php create mode 100644 app/Models/Tenant.php create mode 100644 app/Models/Warehouse.php create mode 100644 app/Models/WorkLog.php create mode 100644 app/Policies/AttachmentPolicy.php create mode 100644 app/Policies/DocumentPolicy.php create mode 100644 app/Policies/EmployeePolicy.php create mode 100644 app/Policies/ExpensePolicy.php create mode 100644 app/Policies/InventoryTransactionPolicy.php create mode 100644 app/Policies/ItemPolicy.php create mode 100644 app/Policies/LetterPolicy.php create mode 100644 app/Policies/PettyCashPolicy.php create mode 100644 app/Policies/ProjectPolicy.php create mode 100644 app/Policies/TaskPolicy.php create mode 100644 app/Policies/WarehousePolicy.php create mode 100644 app/Providers/BladeServiceProvider.php create mode 100644 app/Providers/RouteServiceProvider.php create mode 100644 app/helpers.php create mode 100644 composer.lock create mode 100644 config/permission.php create mode 100644 config/projectra.php create mode 100644 config/projectra_app_additions.php delete mode 100644 database/.gitignore delete mode 100644 database/factories/UserFactory.php delete mode 100644 database/migrations/0001_01_01_000000_create_users_table.php delete mode 100644 database/migrations/0001_01_01_000001_create_cache_table.php delete mode 100644 database/migrations/0001_01_01_000002_create_jobs_table.php create mode 100644 database/migrations/2024_01_01_000001_create_currencies_table.php create mode 100644 database/migrations/2024_01_01_000002_create_tenants_table.php create mode 100644 database/migrations/2024_01_01_000003_create_users_table.php create mode 100644 database/migrations/2024_01_01_000004_create_roles_permissions_tables.php create mode 100644 database/migrations/2024_01_01_000005_create_projects_table.php create mode 100644 database/migrations/2024_01_01_000006_create_project_users_table.php create mode 100644 database/migrations/2024_01_01_000007_create_employees_table.php create mode 100644 database/migrations/2024_01_01_000008_create_work_logs_table.php create mode 100644 database/migrations/2024_01_01_000009_create_petty_cashes_table.php create mode 100644 database/migrations/2024_01_01_000010_create_expenses_table.php create mode 100644 database/migrations/2024_01_01_000011_create_employee_financials_table.php create mode 100644 database/migrations/2024_01_01_000012_create_items_table.php create mode 100644 database/migrations/2024_01_01_000013_create_warehouses_table.php create mode 100644 database/migrations/2024_01_01_000014_create_inventory_transactions_table.php create mode 100644 database/migrations/2024_01_01_000015_create_tasks_table.php create mode 100644 database/migrations/2024_01_01_000016_create_task_logs_table.php create mode 100644 database/migrations/2024_01_01_000017_create_letters_table.php create mode 100644 database/migrations/2024_01_01_000018_create_letter_attachments_table.php create mode 100644 database/migrations/2024_01_01_000019_create_documents_table.php create mode 100644 database/migrations/2024_01_01_000020_create_audit_logs_table.php create mode 100644 database/migrations/2024_01_01_000021_add_missing_columns_to_tables.php create mode 100644 database/migrations/2024_01_01_000022_add_expense_and_pettycash_missing_columns.php create mode 100644 database/migrations/2024_01_01_000023_create_attachments_table.php create mode 100644 database/migrations/2024_01_01_000024_create_task_dependencies_table.php create mode 100644 database/migrations/2024_01_01_000025_add_missing_columns_to_tasks_table.php create mode 100644 database/migrations/2026_05_30_225223_add_created_by_to_tasks_table.php create mode 100644 database/migrations/2026_05_30_233327_add_requested_by_to_expenses_table.php create mode 100644 database/migrations/2026_06_04_143839_add_requested_by_and_request_date_to_petty_cashes_table.php create mode 100644 database/seeders/CurrencySeeder.php create mode 100644 database/seeders/EmployeeSeeder.php create mode 100644 database/seeders/ExpenseSeeder.php create mode 100644 database/seeders/PermissionSeeder.php create mode 100644 database/seeders/ProjectSeeder.php create mode 100644 database/seeders/RolePermissionSeeder.php create mode 100644 database/seeders/TaskSeeder.php create mode 100644 database/seeders/TenantSeeder.php create mode 100644 database/seeders/UserSeeder.php create mode 100644 move-files.bat create mode 100644 package-lock.json create mode 100644 postcss.config.js create mode 100644 public/css/jalali-datepicker.css create mode 100644 public/js/jalali-datepicker.js delete mode 100644 resources/js/bootstrap.js create mode 100644 resources/lang/en.json create mode 100644 resources/lang/fa.json create mode 100644 resources/views/auth/login.blade.php create mode 100644 resources/views/components/currency-display.blade.php create mode 100644 resources/views/components/date-input.blade.php create mode 100644 resources/views/components/localized-date.blade.php create mode 100644 resources/views/components/priority-badge.blade.php create mode 100644 resources/views/components/status-badge.blade.php create mode 100644 resources/views/dashboard/index.blade.php create mode 100644 resources/views/documents/form.blade.php create mode 100644 resources/views/documents/index.blade.php create mode 100644 resources/views/documents/show.blade.php create mode 100644 resources/views/employees/create.blade.php create mode 100644 resources/views/employees/edit.blade.php create mode 100644 resources/views/employees/financials.blade.php create mode 100644 resources/views/employees/form.blade.php create mode 100644 resources/views/employees/index.blade.php create mode 100644 resources/views/employees/show.blade.php create mode 100644 resources/views/employees/work-logs.blade.php create mode 100644 resources/views/expenses/expense-form.blade.php create mode 100644 resources/views/expenses/expense-index.blade.php create mode 100644 resources/views/expenses/expense-show.blade.php create mode 100644 resources/views/expenses/form.blade.php create mode 100644 resources/views/expenses/index.blade.php create mode 100644 resources/views/expenses/show.blade.php create mode 100644 resources/views/help/index.blade.php create mode 100644 resources/views/inventory-transactions/form.blade.php create mode 100644 resources/views/inventory-transactions/index.blade.php create mode 100644 resources/views/inventory-transactions/show.blade.php create mode 100644 resources/views/inventory/create-item.blade.php create mode 100644 resources/views/inventory/create-transaction.blade.php create mode 100644 resources/views/inventory/create-warehouse.blade.php create mode 100644 resources/views/inventory/form.blade.php create mode 100644 resources/views/inventory/index.blade.php create mode 100644 resources/views/inventory/inventory-index.blade.php create mode 100644 resources/views/inventory/item-form.blade.php create mode 100644 resources/views/inventory/items.blade.php create mode 100644 resources/views/inventory/show.blade.php create mode 100644 resources/views/inventory/transactions.blade.php create mode 100644 resources/views/inventory/warehouse-form.blade.php create mode 100644 resources/views/inventory/warehouses.blade.php create mode 100644 resources/views/layouts/app.blade.php create mode 100644 resources/views/layouts/header.blade.php create mode 100644 resources/views/layouts/sidebar.blade.php create mode 100644 resources/views/letters/form.blade.php create mode 100644 resources/views/letters/index.blade.php create mode 100644 resources/views/letters/show.blade.php create mode 100644 resources/views/payroll/calculate.blade.php create mode 100644 resources/views/payroll/index.blade.php create mode 100644 resources/views/payroll/settlement.blade.php create mode 100644 resources/views/payroll/show.blade.php create mode 100644 resources/views/petty-cashes/PettyCashController.php create mode 100644 resources/views/petty-cashes/create.blade.php create mode 100644 resources/views/petty-cashes/expense-form.blade.php create mode 100644 resources/views/petty-cashes/form.blade.php create mode 100644 resources/views/petty-cashes/index.blade.php create mode 100644 resources/views/petty-cashes/petty-cash-index.blade.php create mode 100644 resources/views/petty-cashes/show.blade.php create mode 100644 resources/views/projects/form.blade.php create mode 100644 resources/views/projects/index.blade.php create mode 100644 resources/views/projects/show.blade.php create mode 100644 resources/views/reports/employee-worklog.blade.php create mode 100644 resources/views/reports/financial-pdf.blade.php create mode 100644 resources/views/reports/financial.blade.php create mode 100644 resources/views/reports/index.blade.php create mode 100644 resources/views/reports/inventory.blade.php create mode 100644 resources/views/reports/payroll.blade.php create mode 100644 resources/views/settings/index.blade.php create mode 100644 resources/views/settings/partials/currencies.blade.php create mode 100644 resources/views/settings/partials/organization.blade.php create mode 100644 resources/views/settings/partials/users.blade.php create mode 100644 resources/views/tasks/form.blade.php create mode 100644 resources/views/tasks/gantt.blade.php create mode 100644 resources/views/tasks/index.blade.php create mode 100644 resources/views/tasks/kanban.blade.php create mode 100644 resources/views/tasks/show.blade.php create mode 100644 resources/views/warehouses/form.blade.php create mode 100644 resources/views/warehouses/index.blade.php create mode 100644 resources/views/warehouses/show.blade.php delete mode 100644 resources/views/welcome.blade.php create mode 100644 routes/api.php create mode 100644 routes/letters-routes.php create mode 100644 sidebar-addition.txt delete mode 100644 storage/framework/views/.gitignore create mode 100644 storage/framework/views/0815a55745834eb105f466067e7b4e86.php create mode 100644 storage/framework/views/0dc35d574885e9ed87093ad3cc8d1de5.php create mode 100644 storage/framework/views/15dc9805bd50ffbccd898a04c0572745.php create mode 100644 storage/framework/views/42fce2afce1274cac2c89c79739f9fc3.php create mode 100644 storage/framework/views/448a787e655d73dc75d1ab0fcc84b86f.php create mode 100644 storage/framework/views/46e7c1d03b179af2b30bd5f0a8a09de0.php create mode 100644 storage/framework/views/4ec001dbe225074190f1867b3ff08122.php create mode 100644 storage/framework/views/5625be61c087bf17bbc0b8985f4cf4b2.php create mode 100644 storage/framework/views/5c57a35cb8207405007d3ffb6056da18.php create mode 100644 storage/framework/views/63827679d7679f25341f7abe0c3ec203.php create mode 100644 storage/framework/views/65f6c99804a6a0edbd8042acc1b80c49.php create mode 100644 storage/framework/views/663a1a1fea235ffe5ad3f8ff45ecda31.php create mode 100644 storage/framework/views/7379735e393d6b0e3866bee72506a75c.php create mode 100644 storage/framework/views/86a592b10d7a65e83deaa9802eeb20fb.php create mode 100644 storage/framework/views/8978f0f93ca9bd010ca93ff26a7c11ff.php create mode 100644 storage/framework/views/90d43165439e87034f1fc57adfb2dd86.php create mode 100644 storage/framework/views/95971f91e644c2c50f9234aa4ccd2329.php create mode 100644 storage/framework/views/98463ca068fc45f46d96fd75afccf591.php create mode 100644 storage/framework/views/9b62b89896774f58ae7160b9f9c3f8f5.php create mode 100644 storage/framework/views/a03636601cf2476624659c1e63087b5d.php create mode 100644 storage/framework/views/a11f4e6f418ca75d82ec12f95844d120.php create mode 100644 storage/framework/views/a767be18f85f06a40736e43baa9c1c94.php create mode 100644 storage/framework/views/abc1086b0925e0d145a47b081ba4eb8c.php create mode 100644 storage/framework/views/b1e4ba57b9976591108a1c9b3ffe68a8.php create mode 100644 storage/framework/views/b3640ad9e480045721de9d9873d8e305.php create mode 100644 storage/framework/views/b70330c10889c719d7f5f7c4f8975d50.php create mode 100644 storage/framework/views/c2413d3aa9744e18fe67bd7a0d816197.php create mode 100644 storage/framework/views/cc7cee66c4891fd4b5e2d2b84bf0b502.php create mode 100644 storage/framework/views/cf61b670207f0f276880b2ae809d4364.php create mode 100644 storage/framework/views/d0cbbc094801c6a7d98901eb18134bcd.php create mode 100644 storage/framework/views/d1152c3fd7ed43b88a51d21a99ef2690.php create mode 100644 storage/framework/views/d2d7153ed22b6afa438b863b8619689d.php create mode 100644 storage/framework/views/d4837b0709b873e59cd24d661c7a1525.php create mode 100644 storage/framework/views/e3253f4ba065d81f5eeecfaef7fa0887.php create mode 100644 storage/framework/views/e3358e6b2ca544b1228f9c327b88a6e0.php create mode 100644 storage/framework/views/eb3da118726a3243dc4a5dd762b87bb8.php create mode 100644 storage/framework/views/ebeebd8922e18d3f7f95a6b1c67f8bfe.php create mode 100644 storage/framework/views/f65f51438d6e40d1a0e9db5fe3643c14.php create mode 100644 tailwind.config.js diff --git a/.env.example b/.env.example index 7b49625a..9c3b0797 100644 --- a/.env.example +++ b/.env.example @@ -1,54 +1,32 @@ -APP_NAME=Laravel +APP_NAME=Projectra APP_ENV=local APP_KEY= APP_DEBUG=true -APP_TIMEZONE=UTC APP_URL=http://localhost -APP_LOCALE=en -APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US - -APP_MAINTENANCE_DRIVER=file -APP_MAINTENANCE_STORE=database - -BCRYPT_ROUNDS=12 - LOG_CHANNEL=stack -LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=projectra +DB_USERNAME=root +DB_PASSWORD= -SESSION_DRIVER=database -SESSION_LIFETIME=120 -SESSION_ENCRYPT=false -SESSION_PATH=/ -SESSION_DOMAIN=null - -BROADCAST_CONNECTION=log +BROADCAST_DRIVER=log +CACHE_DRIVER=file FILESYSTEM_DISK=local QUEUE_CONNECTION=database +SESSION_DRIVER=file +SESSION_LIFETIME=120 -CACHE_STORE=database -CACHE_PREFIX= +SANCTUM_STATEFUL_DOMAINS=localhost:8000,localhost -MEMCACHED_HOST=127.0.0.1 - -REDIS_CLIENT=phpredis -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=log -MAIL_HOST=127.0.0.1 -MAIL_PORT=2525 +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null @@ -62,3 +40,8 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +# Projectra Custom Settings +DEFAULT_LOCALE=fa +DEFAULT_CALENDAR=jalali +DEFAULT_CURRENCY=IRR \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7fe978f8..a7182b27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -/.phpunit.cache /node_modules /public/build /public/hot @@ -8,6 +7,7 @@ .env .env.backup .env.production +.phpactor.json .phpunit.result.cache Homestead.json Homestead.yaml diff --git a/2024_01_01_000022_add_expense_and_pettycash_missing_columns.php b/2024_01_01_000022_add_expense_and_pettycash_missing_columns.php new file mode 100644 index 00000000..2c4f61ae --- /dev/null +++ b/2024_01_01_000022_add_expense_and_pettycash_missing_columns.php @@ -0,0 +1,94 @@ +foreignId('requested_by')->nullable()->after('receipt_path')->constrained('users')->nullOnDelete(); + } + if (!Schema::hasColumn('expenses', 'approval_date')) { + $table->timestamp('approval_date')->nullable()->after('approved_by'); + } + if (!Schema::hasColumn('expenses', 'amount_usd')) { + $table->decimal('amount_usd', 15, 2)->nullable()->after('amount'); + } + if (!Schema::hasColumn('expenses', 'original_amount')) { + $table->decimal('original_amount', 15, 0)->nullable()->after('amount_usd'); + } + }); + + // Add 'paid' to expenses status enum if needed + // Note: MySQL requires altering the enum column directly + if (Schema::hasColumn('expenses', 'status')) { + \DB::statement("ALTER TABLE expenses MODIFY COLUMN status ENUM('pending','approved','rejected','paid') DEFAULT 'pending'"); + } + + // ============================================================ + // 2. PETTY CASHES — add receipt_path column + // ============================================================ + Schema::table('petty_cashes', function (Blueprint $table) { + if (!Schema::hasColumn('petty_cashes', 'receipt_path')) { + $table->string('receipt_path')->nullable()->after('request_date'); + } + }); + + // ============================================================ + // 3. AUDIT LOGS — add url and method columns (for middleware) + // ============================================================ + Schema::table('audit_logs', function (Blueprint $table) { + if (!Schema::hasColumn('audit_logs', 'url')) { + $table->string('url')->nullable()->after('user_agent'); + } + if (!Schema::hasColumn('audit_logs', 'method')) { + $table->string('method', 10)->nullable()->after('url'); + } + }); + } + + public function down(): void + { + // 3. Audit Logs + Schema::table('audit_logs', function (Blueprint $table) { + if (Schema::hasColumn('audit_logs', 'method')) { + $table->dropColumn('method'); + } + if (Schema::hasColumn('audit_logs', 'url')) { + $table->dropColumn('url'); + } + }); + + // 2. Petty Cashes + Schema::table('petty_cashes', function (Blueprint $table) { + if (Schema::hasColumn('petty_cashes', 'receipt_path')) { + $table->dropColumn('receipt_path'); + } + }); + + // 1. Expenses — revert status enum + \DB::statement("ALTER TABLE expenses MODIFY COLUMN status ENUM('pending','approved','rejected') DEFAULT 'pending'"); + + Schema::table('expenses', function (Blueprint $table) { + if (Schema::hasColumn('expenses', 'original_amount')) { + $table->dropColumn('original_amount'); + } + if (Schema::hasColumn('expenses', 'amount_usd')) { + $table->dropColumn('amount_usd'); + } + if (Schema::hasColumn('expenses', 'approval_date')) { + $table->dropColumn('approval_date'); + } + if (Schema::hasColumn('expenses', 'requested_by')) { + $table->dropConstrainedForeignId('requested_by'); + } + }); + } +}; diff --git a/AppServiceProvider.php b/AppServiceProvider.php new file mode 100644 index 00000000..9b49e3d6 --- /dev/null +++ b/AppServiceProvider.php @@ -0,0 +1,40 @@ +environment('production')) { + \URL::forceScheme('https'); + } + + // Register policies explicitly (ensures discovery even without AuthServiceProvider) + Gate::policy(Item::class, ItemPolicy::class); + Gate::policy(PettyCash::class, PettyCashPolicy::class); + Gate::policy(InventoryTransaction::class, InventoryTransactionPolicy::class); + } +} diff --git a/AuditLog.php b/AuditLog.php new file mode 100644 index 00000000..9d6dec66 --- /dev/null +++ b/AuditLog.php @@ -0,0 +1,49 @@ + 'array', + 'new_values' => 'array', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get the auditable model (polymorphic-like relationship using model_type/model_id). + */ + public function subject() + { + if ($this->model_type && class_exists($this->model_type)) { + return $this->morphTo(null, 'model_type', 'model_id'); + } + + return null; + } +} diff --git a/AuditLogMiddleware.php b/AuditLogMiddleware.php new file mode 100644 index 00000000..9b45d832 --- /dev/null +++ b/AuditLogMiddleware.php @@ -0,0 +1,171 @@ +method(); + $isMutating = in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE']); + + // For PUT/PATCH/DELETE, capture the current state before the request runs + $oldValues = null; + $modelType = null; + $modelId = null; + + if ($isMutating && in_array($method, ['PUT', 'PATCH', 'DELETE'])) { + $resolvedModel = $this->resolveRouteModel($request); + + if ($resolvedModel) { + $modelType = get_class($resolvedModel); + $modelId = $resolvedModel->getKey(); + + if ($method !== 'POST') { + $oldValues = $resolvedModel->toArray(); + } + } + } + + // Execute the request + $response = $next($request); + + // Only log if the request was successful (2xx status) + if ($isMutating && $response->getStatusCode() >= 200 && $response->getStatusCode() < 300) { + $action = $this->determineAction($method, $request); + $newValues = null; + + // For POST/PUT/PATCH, capture new values from the model + if (in_array($method, ['POST', 'PUT', 'PATCH'])) { + $resolvedModel = $this->resolveRouteModel($request); + + if ($resolvedModel) { + $modelType = get_class($resolvedModel); + $modelId = $resolvedModel->getKey(); + $newValues = $resolvedModel->toArray(); + } elseif ($method === 'POST') { + // For POST with resource creation, try to get the model from the response redirect + $createdModel = $this->extractCreatedModel($request, $response); + if ($createdModel) { + $modelType = get_class($createdModel); + $modelId = $createdModel->getKey(); + $newValues = $createdModel->toArray(); + } + } + } + + // Filter out sensitive fields from values + $sensitiveFields = ['password', 'password_hash', 'remember_token', 'national_code', 'bank_account', 'base_salary']; + $oldValues = $this->filterSensitiveFields($oldValues, $sensitiveFields); + $newValues = $this->filterSensitiveFields($newValues, $sensitiveFields); + + try { + AuditLog::create([ + 'user_id' => Auth::id(), + 'tenant_id' => session('tenant_id'), + 'model_type' => $modelType, + 'model_id' => $modelId, + 'action' => $action, + 'old_values' => $oldValues, + 'new_values' => $newValues, + 'ip_address' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'url' => $request->fullUrl(), + 'method' => $method, + ]); + } catch (\Throwable $e) { + // Silently fail — audit logging should never break the request + logger()->error('Audit log failed: ' . $e->getMessage()); + } + } + + return $response; + } + + /** + * Resolve the Eloquent model from the current route. + */ + protected function resolveRouteModel(Request $request): ?\Illuminate\Database\Eloquent\Model + { + $route = $request->route(); + + if (!$route) { + return null; + } + + foreach ($route->parameters() as $parameter) { + if ($parameter instanceof \Illuminate\Database\Eloquent\Model) { + return $parameter; + } + } + + return null; + } + + /** + * Determine the action label based on HTTP method. + */ + protected function determineAction(string $method, Request $request): string + { + // Check if the route name suggests an approve/reject action + $routeName = $request->route()?->getName() ?? ''; + + if (str_contains($routeName, 'approve')) { + return 'approve'; + } + if (str_contains($routeName, 'reject')) { + return 'reject'; + } + if (str_contains($routeName, 'settle')) { + return 'settle'; + } + if (str_contains($routeName, 'status')) { + return 'status_change'; + } + + return match ($method) { + 'POST' => 'create', + 'PUT', 'PATCH' => 'update', + 'DELETE' => 'delete', + default => strtolower($method), + }; + } + + /** + * Try to extract a newly created model from a redirect response. + */ + protected function extractCreatedModel(Request $request, $response): ?\Illuminate\Database\Eloquent\Model + { + // If the response redirects to a show route, the model was just created + // We can't easily extract it from the redirect, so we fall back to null + // The model type/id will be populated by a subsequent GET if needed + return null; + } + + /** + * Filter out sensitive fields from an array of values. + */ + protected function filterSensitiveFields(?array $values, array $sensitiveFields): ?array + { + if ($values === null) { + return null; + } + + foreach ($sensitiveFields as $field) { + unset($values[$field]); + } + + return $values; + } +} diff --git a/Employee.php b/Employee.php new file mode 100644 index 00000000..2bac0196 --- /dev/null +++ b/Employee.php @@ -0,0 +1,73 @@ + 'date', + 'termination_date' => 'date', + 'national_code' => 'encrypted', + 'bank_account' => 'encrypted', + 'base_salary' => 'encrypted', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + public function workLogs() + { + return $this->hasMany(WorkLog::class); + } + + public function financials() + { + return $this->hasMany(EmployeeFinancial::class); + } + + public function expenses() + { + return $this->hasMany(Expense::class, 'employee_id'); + } + + public function getFullNameAttribute() + { + return $this->first_name . ' ' . $this->last_name; + } +} diff --git a/Expense.php b/Expense.php new file mode 100644 index 00000000..8c55e370 --- /dev/null +++ b/Expense.php @@ -0,0 +1,113 @@ + 'decimal:2', + 'amount_usd' => 'decimal:2', + 'original_amount' => 'decimal:2', + 'expense_date' => 'date', + 'approval_date' => 'datetime', + 'approved_at' => 'datetime', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function pettyCash() + { + return $this->belongsTo(PettyCash::class); + } + + public function employee() + { + return $this->belongsTo(Employee::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + public function requestedBy() + { + return $this->belongsTo(User::class, 'requested_by'); + } + + public function approvedBy() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Alias for approvedBy (backward compatibility). + */ + public function approver() + { + return $this->approvedBy(); + } + + /** + * Scope: filter by category. + */ + public function scopeByCategory($query, $category) + { + return $query->where('category', $category); + } + + /** + * Scope: filter by status. + */ + public function scopeByStatus($query, $status) + { + return $query->where('status', $status); + } + + /** + * Get the status label for display. + */ + public function getStatusLabelAttribute(): string + { + return [ + 'pending' => __('expense.pending'), + 'approved' => __('expense.approved'), + 'rejected' => __('expense.rejected'), + 'paid' => __('expense.paid'), + ][$this->status] ?? $this->status; + } +} diff --git a/INSTALL-WINDOWS-XAMPP.md b/INSTALL-WINDOWS-XAMPP.md new file mode 100644 index 00000000..41a0fe62 --- /dev/null +++ b/INSTALL-WINDOWS-XAMPP.md @@ -0,0 +1,465 @@ +# 🏗️ Vernova - راهنمای نصب روی ویندوز + XAMPP + VSCode + +## پیش‌نیازها + +### ۱. نصب XAMPP +دانلود XAMPP با PHP 8.2+ از: +👉 https://www.apachefriends.org/download.html + +نسخه‌ای که PHP 8.2 یا 8.3 داره رو انتخاب کنید. + +بعد از نصب، مطمئن بشید XAMPP در مسیر زیر هست: +``` +C:\xampp +``` + +### ۲. نصب Composer +دانلود از: +👉 https://getcomposer.org/download/ + +⚠️ حتماً موقع نصب، مسیر PHP رو بهش بدید: +``` +C:\xampp\php\php.exe +``` + +### ۳. نصب Node.js +دانلود LTS از: +👉 https://nodejs.org/ + +### ۴. نصب Git (اختیاری ولی پیشنهادی) +👉 https://git-scm.com/download/win + +### ۵. نصب VSCode +👉 https://code.visualstudio.com/ + +--- + +## مرحله ۱: بررسی پیش‌نیازها + +CMD (Command Prompt) رو باز کنید و بزنید: + +```cmd +php -v +``` +باید PHP 8.2+ نشون بده. اگه نشناخت: +``` +سیستم > متغیرهای محیطی > Path > اضافه کنید: +C:\xampp\php +``` + +```cmd +composer --version +node -v +npm -v +``` + +همه باید جواب بده. ✅ + +--- + +## مرحله ۲: فعال‌کردن اکستنشن‌های PHP + +فایل زیر رو با Notepad یا VSCode باز کنید: +``` +C:\xampp\php\php.ini +``` + +این خطوط رو پیدا کنید و سمی‌کالن (;) اولشون رو حذف کنید: + +```ini +; قبل: +;extension=intl +;extension=pdo_mysql +;extension=mbstring +;extension=fileinfo +;extension=openssl +;extension=curl + +; بعد: +extension=intl +extension=pdo_mysql +extension=mbstring +extension=fileinfo +extension=openssl +extension=curl +``` + +⚠️ **اکستنشن `intl` خیلی مهمه!** برای تبدیل اعداد و ارز لازمه. + +بعد از تغییر، Apache رو از پنل XAMPP ری‌استارت کنید. + +--- + +## مرحله ۳: ساخت دیتابیس + +1. پنل XAMPP → **MySQL** رو Start کنید +2. مرورگر رو باز کنید: `http://localhost/phpmyadmin` +3. روی **New** کلیک کنید +4. نام دیتابیس: `projectra` +5. Collation: `utf8mb4_unicode_ci` +6. **Create** رو بزنید + +--- + +## مرحله ۴: ساخت پروژه Laravel + +CMD رو باز کنید: + +```cmd +cd C:\xampp\htdocs + +composer create-project laravel/laravel projectra +``` + +این مرحله ممکنه ۳-۵ دقیقه طول بکشه. ⏳ + +--- + +## مرحله ۵: کپی فایل‌های Projectra + +فایل ZIP (`projectra-laravel.zip`) رو Extract کنید. + +### فایل‌هایی که باید کپی بشن: + +``` +از ZIP → به C:\xampp\htdocs\projectra\ +``` + +| پوشه/فایل | عملیات | +|-----------|---------| +| `app/` | جایگزین (overwrite) | +| `config/projectra.php` | کپی | +| `config/projectra_app_additions.php` | کپی | +| `database/migrations/` | جایگزین | +| `database/seeders/` | جایگزین | +| `resources/views/` | جایگزین | +| `resources/lang/` | جایگزین | +| `resources/css/` | جایگزین | +| `resources/js/` | جایگزین | +| `routes/web.php` | جایگزین | +| `routes/api.php` | جایگزین | +| `composer.json` | جایگزین | +| `package.json` | جایگزین | +| `vite.config.js` | جایگزین | +| `tailwind.config.js` | جایگزین | +| `postcss.config.js` | جایگزین | +| `.env.example` | جایگزین | + +--- + +## مرحله ۶: نصب وابستگی‌ها + +```cmd +cd C:\xampp\htdocs\projectra + +:: نصب پکیج‌های PHP +composer install + +:: نصب پکیج‌های Frontend +npm install +``` + +اگه `composer install` ارور داد پکیج‌ها رو پیدا نمی‌کنه: +```cmd +composer update +``` + +--- + +## مرحله ۷: تنظیم فایل .env + +```cmd +copy .env.example .env +php artisan key:generate +``` + +فایل `.env` رو با VSCode باز کنید و این بخش رو ویرایش کنید: + +```env +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=projectra +DB_USERNAME=root +DB_PASSWORD= +``` + +> 💡 در XAMPP، کاربر پیش‌فرض MySQL `root` هست و رمز عبور **خالیه** (بدون رمز). + +--- + +## مرحله ۸: تنظیم config/app.php + +فایل `config/app.php` رو باز کنید: + +### اضافه کردن Providers: +```php +'providers' => [ + // ... provider های موجود ... + + // Vernova + Spatie\Permission\PermissionServiceProvider::class, + Morilog\Jalali\JalaliServiceProvider::class, + Maatwebsite\Excel\ExcelServiceProvider::class, + Barryvdh\DomPDF\ServiceProvider::class, + App\Providers\BladeServiceProvider::class, +], +``` + +### اضافه کردن Aliases: +```php +'aliases' => [ + // ... alias های موجود ... + + // Vernova + 'Jalali' => Morilog\Jalali\JalaliFacade::class, + 'Excel' => Maatwebsite\Excel\Facades\Excel::class, + 'PDF' => Barryvdh\DomPDF\Facade\Pdf::class, +], +``` + +### تغییر locale پیش‌فرض: +```php +'locale' => env('APP_LOCALE', 'fa'), +'fallback_locale' => 'en', +``` + +--- + +## مرحله ۹: تنظیم autoload + +فایل `composer.json` رو باز کنید. در بخش `autoload` اضافه کنید: + +```json +"autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + }, + "files": [ + "app/Helpers/helpers.php" + ] +}, +``` + +سپس: +```cmd +composer dump-autoload +``` + +--- + +## مرحله ۱۰: اجرای Migration و Seeder + +```cmd +php artisan migrate +php artisan db:seed +``` + +اگه ارور `Specified key was too long` گرفتید، در `AppServiceProvider.php` در متد `boot()` اضافه کنید: +```php +Schema::defaultStringLength(191); +``` + +--- + +## مرحله ۱۱: ساخت Asset‌ها + +```cmd +npm run build +``` + +برای توسعه (با auto-refresh): +```cmd +npm run dev +``` + +--- + +## مرحله ۱۲: اجرای پروژه 🚀 + +### روش ۱: با artisan (پیشنهادی) +```cmd +php artisan serve +``` +مرورگر: `http://localhost:8000` + +### روش ۲: با Apache (XAMPP) +1. Apache رو در XAMPP استارت کنید +2. مرورگر: `http://localhost/projectra/public` + +> 💡 برای روش ۲، بهتره Virtual Host تنظیم کنید (مرحله بعدی). + +--- + +## 🔧 تنظیم Virtual Host (اختیاری ولی حرفه‌ای) + +فایل زیر رو باز کنید: +``` +C:\xampp\apache\conf\extra\httpd-vhosts.conf +``` + +اضافه کنید: +```apache + + DocumentRoot "C:/xampp/htdocs/projectra/public" + ServerName projectra.test + + AllowOverride All + Require all granted + + +``` + +فایل hosts ویندوز رو باز کنید (با Notepad به صورت Run as Administrator): +``` +C:\Windows\System32\drivers\etc\hosts +``` + +اضافه کنید: +``` +127.0.0.1 projectra.test +``` + +Apache رو ری‌استارت کنید و بعد: +👉 `http://projectra.test` + +--- + +## 👤 حساب‌های پیش‌فرض + +| نقش | ایمیل | رمز عبور | +|------|-------|----------| +| Super Admin | admin@projectra.ir | password | +| Admin (سازندگی سپید) | admin@sepideh.ir | password | +| Project Manager | manager@sepideh.ir | password | +| Admin (Global) | admin@global.com | password | + +--- + +## 🛠️ تنظیمات VSCode + +### اکستنشن‌های پیشنهادی: + +| اکستنشن | کاربرد | +|---------|---------| +| **Laravel Blade** | رنگ‌بندی Blade templates | +| **PHP Intelephense** | autocomplete و تحلیل PHP | +| **Tailwind CSS IntelliSense** | autocomplete کلاس‌های Tailwind | +| **Alpine.js IntelliSense** | autocomplete Alpine.js | +| **Laravel Artisan** | اجرای دستورات Artisan از VSCode | +| **PHP Debug** | دیباگ PHP با Xdebug | +| **Prettier** | فرمت‌بندی کد | + +### تنظیم Xdebug (اختیاری): + +فایل `C:\xampp\php\php.ini` اضافه کنید: +```ini +[xdebug] +zend_extension=xdebug +xdebug.mode=debug +xdebug.start_with_request=trigger +xdebug.client_port=9003 +``` + +در VSCode فایل `.vscode/launch.json`: +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9003 + } + ] +} +``` + +--- + +## ❓ مشکلات رایج ویندوز + +### ۱. ارور "php not recognized" +``` +Path اضافه کنید: C:\xampp\php +سیستم > متغیرهای محیطی > Path > Edit > New +``` + +### ۲. ارور "composer not recognized" +``` +Path اضافه کنید: C:\ProgramData\ComposerSetup\bin +یا Composer رو دوباره نصب کنید +``` + +### ۳. ارور "SSL Certificate" در composer +```cmd +composer config -g -- disable-tls false +composer config -g -- secure-http false +``` + +### ۴. ارور "Allowed memory size exhausted" +```cmd +php -d memory_limit=-1 C:\path\to\composer.phar install +``` + +### ۵. ارور "npm ERR!" +```cmd +npm cache clean --force +npm install +``` + +### ۶. ارور مخفف نشدن صفحه (blank page) +```cmd +php artisan config:clear +php artisan cache:clear +php artisan view:clear +php artisan route:clear +``` + +### ۷. ارور permission در storage +```cmd +icacls storage /grant Everyone:F /T +icacls bootstrap\cache /grant Everyone:F /T +``` + +--- + +## 📌 خلاصه سریع (Quick Start) + +```cmd +:: 1. ساخت پروژه +cd C:\xampp\htdocs +composer create-project laravel/laravel projectra + +:: 2. کپی فایل‌ها از ZIP → projectra/ + +:: 3. نصب وابستگی‌ها +cd projectra +composer install +npm install + +:: 4. تنظیم env +copy .env.example .env +php artisan key:generate + +:: 5. تنظیم config/app.php (Providers + Aliases) +:: 6. تنظیم composer.json autoload (helpers.php) +composer dump-autoload + +:: 7. ساخت دیتابیس در phpMyAdmin: projectra + +:: 8. Migration + Seed +php artisan migrate +php artisan db:seed + +:: 9. Build +npm run build + +:: 10. اجرا! 🚀 +php artisan serve +``` + +مرورگر → `http://localhost:8000` ✅ diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..e1179fc5 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,340 @@ +# 🏗️ Projectra - راهنمای نصب و راه‌اندازی + +## پیش‌نیازها + +قبل از شروع، مطمئن شوید این نرم‌افزارها روی سیستم شما نصب هستند: + +| نرم‌افزار | حداقل نسخه | بررسی | +|-----------|------------|-------| +| PHP | 8.2+ | `php -v` | +| Composer | 2.x | `composer --version` | +| Node.js | 18+ | `node -v` | +| npm/bun | هر دو قابل استفاده‌اند | `npm -v` | +| MySQL | 8.0+ / MariaDB 10.6+ | `mysql --version` | +| Git | 2.x | `git --version` | + +### اکستنشن‌های PHP مورد نیاز: +```bash +php -m | grep -E "intl|pdo|mbstring|xml|curl|openssl|fileinfo" +``` +باید شامل: `intl`, `pdo_mysql`, `mbstring`, `simplexml`, `curl`, `openssl`, `fileinfo` + +--- + +## مرحله ۱: ساخت پروژه Laravel + +```bash +# ساخت پروژه جدید Laravel +composer create-project laravel/laravel projectra +cd projectra +``` + +--- + +## مرحله ۲: کپی فایل‌های Projectra + +فایل‌های ZIP دانلود شده را Extract کنید و **فقط** پوشه‌ها و فایل‌های زیر را در پروژه Laravel کپی کنید: + +### فایل‌های ریشه (جایگزین کنید): +``` +composer.json +package.json +vite.config.js +tailwind.config.js +postcss.config.js +.env.example +``` + +### پوشه app/ (جایگزین کنید): +``` +app/Http/Controllers/ +app/Http/Middleware/ +app/Http/Requests/ +app/Http/Kernel.php +app/Models/ +app/Helpers/ +app/Policies/ +app/Providers/AppServiceProvider.php +app/Providers/BladeServiceProvider.php +``` + +### پوشه‌های دیگر: +``` +config/projectra.php +config/projectra_app_additions.php +database/migrations/ +database/seeders/ +resources/views/ +resources/lang/ +resources/css/ +resources/js/ +routes/web.php +routes/api.php +public/logo.svg +``` + +--- + +## مرحله ۳: نصب وابستگی‌ها + +```bash +# نصب پکیج‌های PHP +composer install + +# نصب پکیج‌های Frontend +npm install +``` + +--- + +## مرحله ۴: تنظیم محیط + +```bash +# کپی فایل env +cp .env.example .env + +# ساخت کلید اپلیکیشن +php artisan key:generate +``` + +فایل `.env` را باز کنید و تنظیمات دیتابیس را وارد کنید: +```env +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=projectra +DB_USERNAME=root +DB_PASSWORD=your_password +``` + +--- + +## مرحله ۵: تنظیمات اضافی config/app.php + +فایل `config/app.php` را باز کنید و تغییرات زیر را اعمال کنید: + +### اضافه کردن Providers: +```php +'providers' => [ + // ... existing providers ... + + // Projectra additions + Spatie\Permission\PermissionServiceProvider::class, + Morilog\Jalali\JalaliServiceProvider::class, + Maatwebsite\Excel\ExcelServiceProvider::class, + Barryvdh\DomPDF\ServiceProvider::class, + App\Providers\BladeServiceProvider::class, +], +``` + +### اضافه کردن Aliases: +```php +'aliases' => [ + // ... existing aliases ... + + // Projectra additions + 'Jalali' => Morilog\Jalali\JalaliFacade::class, + 'Excel' => Maatwebsite\Excel\Facades\Excel::class, + 'PDF' => Barryvdh\DomPDF\Facade\Pdf::class, +], +``` + +### تنظیم locale پیش‌فرض: +```php +'locale' => env('APP_LOCALE', 'fa'), +'fallback_locale' => 'en', +``` + +--- + +## مرحله ۶: تنظیم composer.json + +فایل `composer.json` را باز کنید و در بخش `autoload` اضافه کنید: + +```json +"autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + }, + "files": [ + "app/Helpers/helpers.php" + ] +}, +``` + +سپس: +```bash +composer dump-autoload +``` + +--- + +## مرحله ۷: ساخت دیتابیس + +```bash +# ورود به MySQL +mysql -u root -p + +# ساخت دیتابیس +CREATE DATABASE projectra CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +EXIT; +``` + +--- + +## مرحله ۸: اجرای Migration و Seeder + +```bash +# اجرای Migration‌ها +php artisan migrate + +# اجرای Seeder (داده نمونه) +php artisan db:seed +``` + +--- + +## مرحله ۹: ساخت Asset‌ها + +```bash +# ساخت CSS/JS برای توسعه +npm run dev + +# یا برای تولید +npm run build +``` + +--- + +## مرحله ۱۰: اجرای پروژه + +```bash +php artisan serve +``` + +پروژه در آدرس `http://localhost:8000` در دسترس خواهد بود. + +--- + +## 👤 حساب‌های پیش‌فرض (بعد از db:seed) + +| نقش | ایمیل | رمز عبور | Tenant | +|------|-------|----------|--------| +| Super Admin | admin@projectra.ir | password | - | +| Admin (سازندگی سپید) | admin@sepideh.ir | password | سازندگی سپید | +| Project Manager | manager@sepideh.ir | password | سازندگی سپید | +| Admin (Global) | admin@global.com | password | Global Construction | + +> ⚠️ رمز عبور‌ها را حتماً بعد از نصب تغییر دهید! + +--- + +## 📁 ساختار پوشه‌ها + +``` +projectra/ +├── app/ +│ ├── Http/ +│ │ ├── Controllers/ ← ۸ کنترلر +│ │ ├── Middleware/ ← ۳ میدلور (SetTenant, SetLocale, TenantScope) +│ │ ├── Requests/ ← ۴ Form Request +│ │ └── Kernel.php +│ ├── Models/ ← ۱۹ مدل Eloquent +│ ├── Helpers/ ← ۵ فایل Helper (Jalali, Number, Currency, Calendar, helpers) +│ ├── Policies/ ← ۴ Policy +│ └── Providers/ ← AppServiceProvider + BladeServiceProvider +├── config/ +│ └── projectra.php ← تنظیمات اختصاصی Projectra +├── database/ +│ ├── migrations/ ← ۲۰ Migration +│ └── seeders/ ← ۹ Seeder +├── resources/ +│ ├── views/ ← ۱۸ Blade Template +│ │ ├── layouts/ ← app, sidebar, header +│ │ ├── dashboard/ ← داشبورد اصلی +│ │ ├── projects/ ← لیست، فرم، جزئیات +│ │ ├── tasks/ ← لیست، فرم +│ │ ├── employees/ ← لیست، فرم +│ │ ├── expenses/ ← لیست، فرم +│ │ ├── auth/ ← صفحه ورود +│ │ └── components/ ← status-badge, localized-date, currency-display, priority-badge +│ ├── lang/ +│ │ ├── fa.json ← ۱۱۲ کلید فارسی +│ │ └── en.json ← ۱۱۲ کلید انگلیسی +│ ├── css/ +│ └── js/ +└── routes/ + ├── web.php ← مسیرهای وب + └── api.php ← مسیرهای API +``` + +--- + +## 🔧 قابلیت‌های بین‌المللی‌سازی (i18n) + +### Blade Directive‌ها: +```blade +@localizeNumber(1234567) ← ۱,۲۳۴,۵۶۷ (فارسی) / 1,234,567 (انگلیسی) +@localizeDate($date) ← ۱۴۰۴/۰۳/۰۵ (شمسی) / 2025/05/26 (میلادی) +@localizeCurrency(45000000, 'IRR') ← ۴۵,۰۰۰,۰۰۰ ﷼ +@direction ← rtl یا ltr +@isRtl ... @endIsRtl ← بلوک شرطی RTL +``` + +### Helper Function‌ها: +```php +jalali_date($date) // تبدیل به شمسی +persian_num(12345) // ۱۲۳۴۵ +format_currency(45000000, 'IRR')// ۴۵,۰۰۰,۰۰۰ ﷼ +format_number(1234567) // ۱,۲۳۴,۵۶۷ +calendar_date($date) // تاریخ بر اساس تقویم فعلی +get_direction() // 'rtl' یا 'ltr' +``` + +### اولویت زبان/تقویم: +``` +کاربر (User) → Tenant → Session → مرورگر → پیش‌فرض سیستم +``` + +--- + +## ❓ مشکلات رایج + +### 1. خطای "Class not found" +```bash +composer dump-autoload +``` + +### 2. خطای permission در storage +```bash +chmod -R 775 storage bootstrap/cache +``` + +### 3. خطای اکستنشن intl +```bash +# Ubuntu +sudo apt-get install php8.2-intl + +# macOS +brew install php@8.2 +``` + +### 4. مشکل Vite +```bash +npm install && npm run build +``` + +--- + +## 📞 پشتیبانی + +- مستندات پروژه: پوشه `upload/` (اسناد ۱ تا ۱۱) +- لاگ تغییرات: `worklog.md` + +--- + +**نسخه:** MVP (Phase 0) +**تاریخ:** ۱۴۰۴/۰۳/۰۶ +**تکنولوژی:** Laravel 11 + Blade + Tailwind CSS + Alpine.js diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 00000000..dbcf47d0 --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,32 @@ +راهنمای نصب اصلاحات تقویم شمسی v2 - ورنوا +================================================ + +تغییرات این نسخه: +- تقویم شمسی کاملاً سفارشی با Vanilla JS (بدون jQuery، بدون eval، بدون مشکل CSP) +- کامپوننت اصلاح شده +- فیلدهای ورودی تاریخ شمسی با تقویم پاپ‌آپ + +فایل‌ها: +-------- +1. public/css/jalali-datepicker.css → فایل جدید (استایل تقویم) +2. public/js/jalali-datepicker.js → جایگزین شود (تقویم Vanilla JS) +3. resources/views/components/date-input.blade.php → جایگزین شود +4. resources/views/layouts/app.blade.php → جایگزین شود +5. resources/views/expenses/form.blade.php → جایگزین شود +6. app/Helpers/helpers.php → جایگزین شود (اگه هنوز نکردی) + +نکته مهم: فایل‌های قدیمی jQuery و persian-datepicker از CDN حذف شدن. +این نسخه هیچ وابستگی خارجی نداره! + +مراحل نصب: +========== +1. فایل‌ها رو جایگزین کن +2. کش رو پاک کن: php artisan view:clear && php artisan cache:clear +3. تست کن + +برای سایر فرم‌ها: +================= +هر input type="date" رو با جایگزین کن. +مثلاً: + قبل: + بعد: diff --git a/InventoryTransactionPolicy.php b/InventoryTransactionPolicy.php new file mode 100644 index 00000000..b465a960 --- /dev/null +++ b/InventoryTransactionPolicy.php @@ -0,0 +1,63 @@ +tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can view the inventory transaction. + */ + public function view(User $user, InventoryTransaction $inventoryTransaction): bool + { + return $user->tenant_id === $inventoryTransaction->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can create inventory transactions. + */ + public function create(User $user): bool + { + return $user->tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can update the inventory transaction. + */ + public function update(User $user, InventoryTransaction $inventoryTransaction): bool + { + return $user->tenant_id === $inventoryTransaction->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can delete the inventory transaction. + */ + public function delete(User $user, InventoryTransaction $inventoryTransaction): bool + { + return $user->tenant_id === $inventoryTransaction->tenant_id + && ($user->hasRole('admin') + || $user->hasPermissionTo('manage inventory')); + } +} diff --git a/ItemPolicy.php b/ItemPolicy.php new file mode 100644 index 00000000..824a2838 --- /dev/null +++ b/ItemPolicy.php @@ -0,0 +1,63 @@ +tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can view the item. + */ + public function view(User $user, Item $item): bool + { + return $user->tenant_id === $item->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can create items. + */ + public function create(User $user): bool + { + return $user->tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can update the item. + */ + public function update(User $user, Item $item): bool + { + return $user->tenant_id === $item->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can delete the item. + */ + public function delete(User $user, Item $item): bool + { + return $user->tenant_id === $item->tenant_id + && ($user->hasRole('admin') + || $user->hasPermissionTo('manage inventory')); + } +} diff --git a/Kernel.php b/Kernel.php new file mode 100644 index 00000000..ff590a31 --- /dev/null +++ b/Kernel.php @@ -0,0 +1,55 @@ + [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\SetTenant::class, + \App\Http\Middleware\SetLocale::class, + ], + + 'api' => [ + \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\SetTenant::class, + \App\Http\Middleware\SetLocale::class, + ], + ]; + + protected $middlewareAliases = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, + 'signed' => \App\Http\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class, + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + 'tenant.scope' => \App\Http\Middleware\TenantScope::class, + 'audit.log' => \App\Http\Middleware\AuditLogMiddleware::class, + ]; +} diff --git a/PettyCash.php b/PettyCash.php new file mode 100644 index 00000000..fb537bae --- /dev/null +++ b/PettyCash.php @@ -0,0 +1,86 @@ + 'decimal:2', + 'original_amount' => 'decimal:2', + 'amount_usd' => 'decimal:2', + 'remaining' => 'decimal:2', + 'request_date' => 'date', + 'approval_date' => 'datetime', + 'approved_at' => 'datetime', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + public function requestedBy() + { + return $this->belongsTo(User::class, 'requested_by'); + } + + public function approvedBy() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function expenses() + { + return $this->hasMany(Expense::class); + } + + public function scopeByStatus($query, $status) + { + return $query->where('status', $status); + } + + public function getStatusLabelAttribute() + { + return [ + 'pending' => __('pettyCash.pending'), + 'approved' => __('pettyCash.approved'), + 'rejected' => __('pettyCash.rejected'), + 'settled' => __('pettyCash.settled'), + ][$this->status] ?? $this->status; + } +} diff --git a/PettyCashController.php b/PettyCashController.php new file mode 100644 index 00000000..9b31694b --- /dev/null +++ b/PettyCashController.php @@ -0,0 +1,220 @@ +filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + $query->orderBy('request_date', 'desc'); + + $pettyCashes = $query->paginate(20)->appends($request->query()); + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $statuses = ['pending', 'approved', 'rejected', 'settled']; + + return view('petty-cashes.index', compact('pettyCashes', 'projects', 'statuses')); + } + + /** + * Show the form for creating a new petty cash request. + */ + public function create() + { + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('petty-cashes.form', compact('projects', 'currencies', 'baseCurrency')); + } + + /** + * Store a newly created petty cash request. + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'project_id' => 'required|exists:projects,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'required|exists:currencies,id', + 'request_date' => 'required|date', + 'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + ]); + + $validated['tenant_id'] = session('tenant_id'); + $validated['requested_by'] = Auth::id(); + $validated['status'] = 'pending'; + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($validated['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $validated['original_amount'] = $validated['amount']; + $validated['amount'] = CurrencyHelper::convert( + (float) $validated['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload + if ($request->hasFile('receipt')) { + $validated['receipt_path'] = $request->file('receipt')->store('receipts/petty-cash', 'public'); + } + + $pettyCash = PettyCash::create($validated); + + return redirect() + ->route('petty-cashes.show', $pettyCash) + ->with('success', __('expense.created_successfully')); + } + + /** + * Display the specified petty cash request. + */ + public function show(PettyCash $pettyCash) + { + $pettyCash->load(['project', 'currency', 'requestedBy', 'approvedBy', 'expenses']); + + return view('petty-cashes.show', compact('pettyCash')); + } + + /** + * Show the form for editing the specified petty cash request. + */ + public function edit(PettyCash $pettyCash) + { + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('petty-cashes.form', compact('pettyCash', 'projects', 'currencies', 'baseCurrency')); + } + + /** + * Update the specified petty cash request. + */ + public function update(Request $request, PettyCash $pettyCash) + { + $validated = $request->validate([ + 'project_id' => 'required|exists:projects,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'required|exists:currencies,id', + 'request_date' => 'required|date', + 'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + ]); + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($validated['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $validated['original_amount'] = $validated['amount']; + $validated['amount'] = CurrencyHelper::convert( + (float) $validated['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload + if ($request->hasFile('receipt')) { + // Delete old receipt if exists + if ($pettyCash->receipt_path) { + \Illuminate\Support\Facades\Storage::disk('public')->delete($pettyCash->receipt_path); + } + $validated['receipt_path'] = $request->file('receipt')->store('receipts/petty-cash', 'public'); + } + + $pettyCash->update($validated); + + return redirect() + ->route('petty-cashes.show', $pettyCash) + ->with('success', __('expense.updated_successfully')); + } + + /** + * Remove the specified petty cash request. + */ + public function destroy(PettyCash $pettyCash) + { + $pettyCash->delete(); + + return redirect() + ->route('petty-cashes.index') + ->with('success', __('expense.deleted_successfully')); + } + + /** + * Approve a petty cash request. + */ + public function approve(PettyCash $pettyCash) + { + $pettyCash->update([ + 'status' => 'approved', + 'approved_by' => Auth::id(), + 'approval_date' => now(), + ]); + + return response()->json([ + 'success' => true, + 'message' => __('expense.approved_successfully'), + ]); + } + + /** + * Reject a petty cash request. + */ + public function reject(PettyCash $pettyCash) + { + $pettyCash->update([ + 'status' => 'rejected', + 'approved_by' => Auth::id(), + 'approval_date' => now(), + ]); + + return response()->json([ + 'success' => true, + 'message' => __('expense.rejected_successfully'), + ]); + } + + /** + * Settle a petty cash request (mark as settled after usage). + */ + public function settle(PettyCash $pettyCash) + { + $pettyCash->update([ + 'status' => 'settled', + ]); + + return response()->json([ + 'success' => true, + 'message' => __('expense.settled_successfully'), + ]); + } +} diff --git a/PettyCashPolicy.php b/PettyCashPolicy.php new file mode 100644 index 00000000..3ba6d2b8 --- /dev/null +++ b/PettyCashPolicy.php @@ -0,0 +1,98 @@ +tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage petty cash')); + } + + /** + * Determine whether the user can view the petty cash request. + */ + public function view(User $user, PettyCash $pettyCash): bool + { + if ($user->tenant_id !== $pettyCash->tenant_id) { + return false; + } + + if ($user->hasRole('admin') || $user->hasRole('manager')) { + return true; + } + + if ($user->hasPermissionTo('manage petty cash')) { + return true; + } + + // Users can view their own petty cash requests + return $pettyCash->requested_by === $user->id; + } + + /** + * Determine whether the user can create petty cash requests. + */ + public function create(User $user): bool + { + return $user->tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage petty cash')); + } + + /** + * Determine whether the user can update the petty cash request. + */ + public function update(User $user, PettyCash $pettyCash): bool + { + if ($user->tenant_id !== $pettyCash->tenant_id) { + return false; + } + + // Only pending requests can be updated + if ($pettyCash->status !== 'pending') { + return false; + } + + if ($user->hasRole('admin') || $user->hasRole('manager')) { + return true; + } + + if ($user->hasPermissionTo('manage petty cash')) { + return true; + } + + return $pettyCash->requested_by === $user->id; + } + + /** + * Determine whether the user can delete the petty cash request. + */ + public function delete(User $user, PettyCash $pettyCash): bool + { + return $user->tenant_id === $pettyCash->tenant_id + && ($user->hasRole('admin') + || $user->hasPermissionTo('manage petty cash')); + } + + /** + * Determine whether the user can approve the petty cash request. + * Only admin/manager can approve. + */ + public function approve(User $user, PettyCash $pettyCash): bool + { + return $user->tenant_id === $pettyCash->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager')); + } +} diff --git a/README.md b/README.md index 1a4c26ba..3974a209 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,52 @@ -

Laravel Logo

+# Vernova - Calendar Fix (رفع مشکل تقویم) -

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

+## مشکل +بخش‌هایی از برنامه تاریخ‌ها رو به میلادی نمایش می‌داد حتی وقتی کاربر تقویم رو روی شمسی ست کرده بود. -## About Laravel +## تغییرات -Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: +### 1. app/Http/Middleware/SetLocale.php +**مشکل:** اولویت خواندن تقویم از سشن اشتباه بود. Middleware در هر درخواست، مقدار سشن رو با مقدار دیتابیس کاربر override می‌کرد. اگر کاربر تقویم رو از هدر تغییر بده، در درخواست بعدی مقدار سشن دوباره از دیتابیس خونده می‌شد و تغییر کاربر نادیده گرفته می‌شد. -- [Simple, fast routing engine](https://laravel.com/docs/routing). -- [Powerful dependency injection container](https://laravel.com/docs/container). -- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. -- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). -- Database agnostic [schema migrations](https://laravel.com/docs/migrations). -- [Robust background job processing](https://laravel.com/docs/queues). -- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). +**رفع:** اولویت سشن بالاتر از دیتابیس قرار گرفت. حالا ترتیب اولویت: +1. سشن (بالاترین اولویت - کاربر همین الان تغییر داده) +2. تنظیمات دیتابیس کاربر +3. تنظیمات tenant +4. تنظیمات مرورگر +5. تنظیمات پیش‌فرض config -Laravel is accessible, powerful, and provides tools required for large, robust applications. +### 2. app/Http/Controllers/DashboardController.php +**مشکل:** کلید کانفیگ اشتباه بود: `config('projectra.defaults.calendar')` به جای `config('projectra.default_calendar')`. -## Learning Laravel +**رفع:** کلید کانفیگ اصلاح شد. -Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. +### 3. resources/views/layouts/header.blade.php +**مشکل:** سینتکس `@x-localized-date :date="now()"` معتبر نیست و به صورت متنی خام نمایش داده می‌شد. -You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. +**رفع:** خط نامعتبر حذف شد و فقط `{{ calendar_date(now()) }}` باقی ماند. -If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. +### 4. app/Helpers/helpers.php +**مشکل:** تابع `calendar_date()` تاریخ شمسی رو با ارقام لاتین نمایش می‌داد (مثلاً 1403/01/15 به جای ۱۴۰۳/۰۱/۱۵). -## Laravel Sponsors +**رفع:** تبدیل خودکار ارقام فارسی وقتی locale روی 'fa' هست اضافه شد. -We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). +### 5. app/Models/Project.php +**مشکل:** اکسسور `getStatusLabelAttribute()` وجود نداشت اما ویوها از `$project->status_label` استفاده می‌کردن. -### Premium Partners +**رفع:** اکسسور `status_label` و رابطه `defaultCurrency()` اضافه شد. -- **[Vehikl](https://vehikl.com/)** -- **[Tighten Co.](https://tighten.co)** -- **[WebReinvent](https://webreinvent.com/)** -- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** -- **[64 Robots](https://64robots.com)** -- **[Curotec](https://www.curotec.com/services/technologies/laravel/)** -- **[Cyber-Duck](https://cyber-duck.co.uk)** -- **[DevSquad](https://devsquad.com/hire-laravel-developers)** -- **[Jump24](https://jump24.co.uk)** -- **[Redberry](https://redberry.international/laravel/)** -- **[Active Logic](https://activelogic.com)** -- **[byte5](https://byte5.de)** -- **[OP.GG](https://op.gg)** - -## Contributing - -Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). - -## Code of Conduct - -In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). - -## Security Vulnerabilities - -If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. - -## License - -The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). +## نحوه نصب +فایل‌ها رو در مسیرهای مربوطه در پروژه لاراول کپی کنید: +``` +calendar-fix/ +├── app/ +│ ├── Http/ +│ │ ├── Middleware/SetLocale.php +│ │ └── Controllers/DashboardController.php +│ ├── Helpers/helpers.php +│ └── Models/Project.php +├── resources/ +│ └── views/ +│ └── layouts/header.blade.php +└── README.md +``` diff --git a/app/Helpers/CalendarHelper.php b/app/Helpers/CalendarHelper.php new file mode 100644 index 00000000..3d53ec8b --- /dev/null +++ b/app/Helpers/CalendarHelper.php @@ -0,0 +1,138 @@ +user()?->preferred_calendar ?? 'jalali'); + + if ($calendar === 'jalali' && class_exists(\Morilog\Jalali\Jalalian::class)) { + try { + // Try Jalali format: 1404/03/15 or 1404-03-15 + $normalized = str_replace('/', '-', $date); + $parts = explode('-', $normalized); + + if (count($parts) === 3) { + $jYear = (int) $parts[0]; + $jMonth = (int) $parts[1]; + $jDay = (int) $parts[2]; + + // If year looks like a Jalali year (> 1300), convert + if ($jYear > 1300 && $jYear < 1600) { + $gregorian = \Morilog\Jalali\Jalalian::fromDateTime( + \Morilog\Jalali\CalendarUtils::toGregorian($jYear, $jMonth, $jDay) + ); + + return $gregorian->format('Y-m-d'); + } + } + } catch (\Exception $e) { + // Fall through to Carbon parse + } + } + + // Fallback: try Carbon parse + try { + return \Carbon\Carbon::parse($date)->format('Y-m-d'); + } catch (\Exception $e) { + return $date; + } + } + + /** + * Format a date based on the user's calendar preference. + */ + public static function formatDate(?string $date, string $calendar = 'jalali', string $format = 'Y/m/d'): string + { + return JalaliHelper::formatDateForDisplay($date, $calendar, $format); + } + + /** + * Get the month name for a given month number. + */ + public static function getMonthName(int $month, string $calendar = 'jalali'): string + { + if ($calendar === 'jalali') { + return JalaliHelper::jalaliMonthName($month); + } + + $months = [ + 1 => 'January', + 2 => 'February', + 3 => 'March', + 4 => 'April', + 5 => 'May', + 6 => 'June', + 7 => 'July', + 8 => 'August', + 9 => 'September', + 10 => 'October', + 11 => 'November', + 12 => 'December', + ]; + + return $months[$month] ?? ''; + } + + /** + * Get the day (of week) name. + */ + public static function getDayName(int $dayOfWeek, string $calendar = 'jalali'): string + { + if ($calendar === 'jalali') { + return JalaliHelper::jalaliDayName($dayOfWeek); + } + + $days = [ + 0 => 'Sunday', + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + ]; + + return $days[$dayOfWeek] ?? ''; + } + + /** + * Get the current date formatted based on the calendar preference. + */ + public static function getCurrentDate(string $calendar = 'jalali', string $format = 'Y/m/d'): string + { + return self::formatDate(now()->format('Y-m-d'), $calendar, $format); + } + + /** + * Get the query date range for a given month and year. + */ + public static function getQueryRangeForMonth(int $year, int $month, string $calendar = 'jalali'): array + { + if ($calendar === 'jalali') { + return JalaliHelper::getQueryRangeForJalaliMonth($year, $month); + } + + // Gregorian range + $start = \Carbon\Carbon::create($year, $month, 1)->startOfDay(); + $end = \Carbon\Carbon::create($year, $month, 1)->endOfMonth()->endOfDay(); + + return [$start, $end]; + } +} diff --git a/app/Helpers/CurrencyHelper.php b/app/Helpers/CurrencyHelper.php new file mode 100644 index 00000000..734ffefe --- /dev/null +++ b/app/Helpers/CurrencyHelper.php @@ -0,0 +1,96 @@ +addHours(24), function () use ($currencyCode) { + $currency = Currency::where('code', $currencyCode) + ->where('is_active', true) + ->first(); + + return $currency?->exchange_rate_to_usd ?? 1; + }); + } + + /** + * Get the symbol for a currency. + */ + public static function getCurrencySymbol(string $currencyCode): string + { + $currency = Currency::where('code', $currencyCode)->first(); + + return $currency?->symbol ?? $currencyCode; + } + + /** + * Get the decimal places for a currency. + */ + public static function getDecimalPlaces(string $currencyCode): int + { + $currency = Currency::where('code', $currencyCode)->first(); + + return $currency?->decimal_places ?? 0; + } + + /** + * Format an amount with the currency symbol. + */ + public static function formatWithSymbol(float $amount, string $currencyCode, string $locale = 'fa_IR'): string + { + $symbol = static::getCurrencySymbol($currencyCode); + $decimals = static::getDecimalPlaces($currencyCode); + $formatted = NumberHelper::formatNumber($amount, $decimals, $locale); + + return $formatted . ' ' . $symbol; + } + + /** + * Get the base currency for the current tenant. + */ + public static function getBaseCurrency(): ?Currency + { + $tenantId = session('tenant_id'); + + if (!$tenantId) { + return null; + } + + return Cache::remember("base_currency_{$tenantId}", now()->addHours(24), function () use ($tenantId) { + $tenant = \App\Models\Tenant::find($tenantId); + + return $tenant?->baseCurrency; + }); + } +} diff --git a/app/Helpers/JalaliHelper.php b/app/Helpers/JalaliHelper.php new file mode 100644 index 00000000..c0f40cd9 --- /dev/null +++ b/app/Helpers/JalaliHelper.php @@ -0,0 +1,128 @@ +toArray(); + } + + /** + * Convert a Jalali date to Gregorian. + */ + public static function toGregorian(int $year, int $month, int $day): array + { + return (new Jalalian($year, $month, $day))->toGregorian()->toArray(); + } + + /** + * Format a Gregorian date string as Jalali. + */ + public static function formatJalali(string $date, string $format = 'Y/m/d'): string + { + if (empty($date)) { + return ''; + } + + return Jalalian::fromDateTime($date)->format($format); + } + + /** + * Get the Jalali month name. + */ + public static function jalaliMonthName(int $month): string + { + $months = [ + 1 => 'فروردین', + 2 => 'اردیبهشت', + 3 => 'خرداد', + 4 => 'تیر', + 5 => 'مرداد', + 6 => 'شهریور', + 7 => 'مهر', + 8 => 'آبان', + 9 => 'آذر', + 10 => 'دی', + 11 => 'بهمن', + 12 => 'اسفند', + ]; + + return $months[$month] ?? ''; + } + + /** + * Get the Jalali day (week) name. + */ + public static function jalaliDayName(int $dayOfWeek): string + { + $days = [ + 0 => 'یکشنبه', + 1 => 'دوشنبه', + 2 => 'سه‌شنبه', + 3 => 'چهارشنبه', + 4 => 'پنجشنبه', + 5 => 'جمعه', + 6 => 'شنبه', + ]; + + return $days[$dayOfWeek] ?? ''; + } + + /** + * Get today's date in Jalali as a formatted string. + */ + public static function todayJalali(string $format = 'Y/m/d'): string + { + return Jalalian::now()->format($format); + } + + /** + * Format a date for display based on the user's calendar preference. + */ + public static function formatDateForDisplay(?string $date, string $calendar = 'jalali', string $format = 'Y/m/d'): string + { + if (empty($date)) { + return ''; + } + + if ($calendar === 'jalali') { + return static::formatJalali($date, $format); + } + + return date($format, strtotime($date)); + } + + /** + * Get the start and end dates for a given Jalali month as Gregorian Carbon instances. + * Useful for querying records within a specific Jalali month. + */ + public static function getQueryRangeForJalaliMonth(int $year, int $month): array + { + $start = (new Jalalian($year, $month, 1))->toCarbon(); + $end = (new Jalalian($year, $month, $start->format('t')))->toCarbon()->endOfDay(); + + // Jalalian doesn't have daysInMonth directly, calculate it + $jalaliEndDay = 29; + if ($month <= 6) { + $jalaliEndDay = 31; + } elseif ($month <= 11) { + $jalaliEndDay = 30; + } else { + // Esfand - check for leap year + $j = new Jalalian($year, 1, 1); + $jalaliEndDay = $j->isLeapYear() ? 30 : 29; + } + + $start = (new Jalalian($year, $month, 1))->toCarbon()->startOfDay(); + $end = (new Jalalian($year, $month, $jalaliEndDay))->toCarbon()->endOfDay(); + + return [$start, $end]; + } +} diff --git a/app/Helpers/NumberHelper.php b/app/Helpers/NumberHelper.php new file mode 100644 index 00000000..08e05d47 --- /dev/null +++ b/app/Helpers/NumberHelper.php @@ -0,0 +1,89 @@ +setAttribute(\NumberFormatter::FRACTION_DIGITS, $decimals); + + return $formatter->format((float) $number); + } catch (\Exception $e) { + // Fallback below + } + } + + return number_format((float) $number, $decimals); + } + + /** + * Format a number as currency. + */ + public static function formatCurrency(string|int|float $amount, string $currency = 'IRR', string $locale = 'fa_IR', int $decimals = 0): string + { + if (class_exists(\NumberFormatter::class)) { + try { + $formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $decimals); + + return $formatter->formatCurrency((float) $amount, $currency); + } catch (\Exception $e) { + // Fallback below + } + } + + return number_format((float) $amount, $decimals) . ' ' . $currency; + } + + /** + * Format a number as a percentage. + */ + public static function formatPercentage(string|int|float $value, int $decimals = 1, string $locale = 'fa_IR'): string + { + if (class_exists(\NumberFormatter::class)) { + try { + $formatter = new \NumberFormatter($locale, \NumberFormatter::PERCENT); + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $decimals); + + return $formatter->format((float) $value / 100); + } catch (\Exception $e) { + // Fallback below + } + } + + return number_format((float) $value, $decimals) . '%'; + } +} diff --git a/app/Helpers/helpers.php b/app/Helpers/helpers.php new file mode 100644 index 00000000..cd3d0738 --- /dev/null +++ b/app/Helpers/helpers.php @@ -0,0 +1,126 @@ +format('Y-m-d H:i:s'); + } else { + $dateString = (string) $date; + } + + if ($calendar === 'jalali') { + $jalali = \Morilog\Jalali\Jalalian::fromDateTime($dateString); + $result = $jalali->format($format); + } else { + $result = date($format, strtotime($dateString)); + } + } catch (\Exception $e) { + // Fallback: try Carbon format + try { + if ($date instanceof \DateTimeInterface) { + $result = $date->format($format); + } else { + $result = date($format, strtotime((string) $date)); + } + } catch (\Exception $e2) { + return (string) $date; + } + } + + // Convert to Persian digits if locale is fa + if (app()->getLocale() === 'fa') { + $result = persian_num($result); + } + + return $result; + } +} + +if (!function_exists('calendar_date_raw')) { + /** + * Format a date based on calendar preference WITHOUT Persian digits. + * Useful for form values that need English digits. + */ + function calendar_date_raw($date, $format = 'Y/m/d') + { + if (!$date) return ''; + + $calendar = session('calendar', 'jalali'); + + try { + if ($date instanceof \DateTimeInterface) { + $dateString = $date->format('Y-m-d H:i:s'); + } else { + $dateString = (string) $date; + } + + if ($calendar === 'jalali') { + $jalali = \Morilog\Jalali\Jalalian::fromDateTime($dateString); + return $jalali->format($format); + } else { + return date($format, strtotime($dateString)); + } + } catch (\Exception $e) { + if ($date instanceof \DateTimeInterface) { + return $date->format($format); + } + return (string) $date; + } + } +} + +if (!function_exists('persian_num')) { + function persian_num($number) + { + $persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; + $english = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + return str_replace($english, $persian, (string) $number); + } +} + +if (!function_exists('format_currency')) { + function format_currency($amount, $currency = 'IRR') + { + $formatted = number_format((float) $amount, $currency === 'IRR' ? 0 : 2); + + if (app()->getLocale() === 'fa') { + $formatted = persian_num($formatted); + } + + $symbol = ''; + switch ($currency) { + case 'IRR': $symbol = 'ریال'; break; + case 'USD': $symbol = '$'; break; + case 'EUR': $symbol = '€'; break; + case 'AED': $symbol = 'د.إ'; break; + } + + return $formatted . ' ' . $symbol; + } +} + +if (!function_exists('get_direction')) { + function get_direction() + { + return app()->getLocale() === 'fa' ? 'rtl' : 'ltr'; + } +} + +if (!function_exists('is_rtl')) { + function is_rtl() + { + return app()->getLocale() === 'fa'; + } +} diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php new file mode 100644 index 00000000..0e8e599d --- /dev/null +++ b/app/Http/Controllers/AttachmentController.php @@ -0,0 +1,105 @@ +validate([ + 'attachable_type' => 'required|string', + 'attachable_id' => 'required|integer', + 'files' => 'required|array|min:1', + 'files.*' => 'file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx|max:10240', + 'category' => 'nullable|string|in:receipt,document,photo,other', + ]); + + // Resolve the model + $allowedTypes = [ + 'App\\Models\\Expense', + 'App\\Models\\PettyCash', + 'App\\Models\\Project', + 'App\\Models\\Task', + 'App\\Models\\Letter', + 'App\\Models\\Document', + ]; + + if (!in_array($validated['attachable_type'], $allowedTypes)) { + return response()->json([ + 'success' => false, + 'message' => __('attachment.invalid_model_type'), + ], 422); + } + + $model = $validated['attachable_type']::find($validated['attachable_id']); + + if (!$model) { + return response()->json([ + 'success' => false, + 'message' => __('attachment.model_not_found'), + ], 404); + } + + // Authorize + if (method_exists($model, 'update')) { + $this->authorize('update', $model); + } + + $category = $validated['category'] ?? 'receipt'; + $attachments = $model->attachFiles($validated['files'], $category); + + return response()->json([ + 'success' => true, + 'message' => __('attachment.uploaded_successfully'), + 'attachments' => collect($attachments)->map(fn ($a) => [ + 'id' => $a->id, + 'file_name' => $a->file_name, + 'file_size' => $a->formatted_size, + 'mime_type' => $a->mime_type, + 'category' => $a->category, + 'url' => $a->url, + 'is_image' => $a->is_image, + 'icon' => $a->icon, + 'created_at' => $a->created_at?->format('Y-m-d H:i'), + ]), + ]); + } + + /** + * Delete an attachment. + */ + public function destroy(Attachment $attachment) + { + $this->authorize('delete', $attachment); + + $attachment->delete(); + + return response()->json([ + 'success' => true, + 'message' => __('attachment.deleted_successfully'), + ]); + } + + /** + * Download an attachment. + */ + public function download(Attachment $attachment) + { + $this->authorize('view', $attachment); + + if (!\Illuminate\Support\Facades\Storage::disk('public')->exists($attachment->file_path)) { + abort(404); + } + + return \Illuminate\Support\Facades\Storage::disk('public') + ->download($attachment->file_path, $attachment->file_name); + } +} diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 00000000..22d33a37 --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,83 @@ +route('dashboard'); + } + + return view('auth.login'); + } + + /** + * Handle login attempt with tenant and locale setup. + */ + public function login(Request $request) + { + $credentials = $request->validate([ + 'email' => 'required|email', + 'password' => 'required|string', + ]); + + $remember = $request->boolean('remember'); + + if (Auth::attempt($credentials, $remember)) { + $request->session()->regenerate(); + + $user = Auth::user(); + + // Set tenant context + if ($user->tenant_id) { + session(['tenant_id' => $user->tenant_id]); + $user->load('tenant'); + + // Set tenant defaults + if ($user->tenant) { + session([ + 'locale' => $user->locale ?? $user->tenant->default_locale ?? config('Vernova.defaults.locale', 'fa'), + 'calendar' => $user->preferred_calendar ?? $user->tenant->default_calendar ?? config('Vernova.defaults.calendar', 'jalali'), + 'currency' => $user->tenant->baseCurrency?->code ?? config('Vernova.defaults.currency', 'IRR'), + ]); + } + } + + // Set application locale + $locale = session('locale', $user->locale ?? config('Vernova.defaults.locale', 'fa')); + app()->setLocale($locale); + session(['locale' => $locale]); + + return redirect()->intended(route('dashboard')) + ->with('success', __('auth.login_success')); + } + + return back()->withErrors([ + 'email' => __('auth.failed'), + ])->onlyInput('email'); + } + + /** + * Log the user out. + */ + public function logout(Request $request) + { + Auth::logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login') + ->with('success', __('auth.logout_success')); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5c..b1ea0790 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,7 +2,35 @@ namespace App\Http\Controllers; -abstract class Controller +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Foundation\Validation\ValidatesRequests; +use Illuminate\Routing\Controller as BaseController; +use Illuminate\Support\Facades\Auth; + +class Controller extends BaseController { - // + use AuthorizesRequests, ValidatesRequests; + + /** + * Get the current tenant ID from session or authenticated user. + * Falls back to the authenticated user's tenant_id if session is missing. + */ + protected function getTenantId(): ?int + { + $id = session('tenant_id'); + + if ($id) { + return (int) $id; + } + + // Fallback: get from authenticated user + $user = Auth::user(); + + if ($user && $user->tenant_id) { + session(['tenant_id' => $user->tenant_id]); + return (int) $user->tenant_id; + } + + return null; + } } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 00000000..2653541b --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,118 @@ +getTenantId(); + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $currencyCode = $baseCurrency?->code ?? config('projectra.defaults.currency', 'IRR'); + + // --- KPIs --- + $activeProjectsCount = Project::whereIn('status', ['planning', 'active'])->count(); + $totalBudget = Project::whereIn('status', ['planning', 'active']) + ->sum('budget'); + $pendingTasksCount = Task::where('status', 'pending')->count(); + $activeEmployeesCount = Employee::where('status', 'active')->count(); + + // Budget in base currency + $totalBudgetFormatted = CurrencyHelper::formatWithSymbol((float) $totalBudget, $currencyCode); + + // --- Trends (compare with previous month) --- + $lastMonthStart = now()->subMonth()->startOfMonth(); + $lastMonthEnd = now()->subMonth()->endOfMonth(); + + $lastMonthProjects = Project::whereBetween('created_at', [$lastMonthStart, $lastMonthEnd])->count(); + $thisMonthProjects = Project::whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])->count(); + $projectTrend = $lastMonthProjects > 0 + ? round((($thisMonthProjects - $lastMonthProjects) / $lastMonthProjects) * 100, 1) + : ($thisMonthProjects > 0 ? 100 : 0); + + $lastMonthTasks = Task::whereBetween('created_at', [$lastMonthStart, $lastMonthEnd])->count(); + $thisMonthTasks = Task::whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])->count(); + $taskTrend = $lastMonthTasks > 0 + ? round((($thisMonthTasks - $lastMonthTasks) / $lastMonthTasks) * 100, 1) + : ($thisMonthTasks > 0 ? 100 : 0); + + $lastMonthExpenses = Expense::whereBetween('expense_date', [$lastMonthStart, $lastMonthEnd]) + ->where('status', 'approved')->sum('amount'); + $thisMonthExpenses = Expense::whereBetween('expense_date', [now()->startOfMonth(), now()->endOfMonth()]) + ->where('status', 'approved')->sum('amount'); + $expenseTrend = $lastMonthExpenses > 0 + ? round((($thisMonthExpenses - $lastMonthExpenses) / $lastMonthExpenses) * 100, 1) + : ($thisMonthExpenses > 0 ? 100 : 0); + + $lastMonthEmployees = Employee::whereBetween('hire_date', [$lastMonthStart, $lastMonthEnd])->count(); + $thisMonthEmployees = Employee::whereBetween('hire_date', [now()->startOfMonth(), now()->endOfMonth()])->count(); + $employeeTrend = $lastMonthEmployees > 0 + ? round((($thisMonthEmployees - $lastMonthEmployees) / $lastMonthEmployees) * 100, 1) + : ($thisMonthEmployees > 0 ? 100 : 0); + + // --- Project Progress --- + $projectsInProgress = Project::whereIn('status', ['active', 'planning']) + ->with(['manager', 'tasks']) + ->orderBy('progress', 'desc') + ->limit(5) + ->get(); + + // --- Recent Activities --- + $recentActivities = AuditLog::where('tenant_id', $tenantId) + ->with('user') + ->orderBy('created_at', 'desc') + ->limit(8) + ->get(); + + // --- Tasks by status for current user --- + $myTasks = Task::where('assignee_id', $user->id) + ->whereNotIn('status', ['done', 'cancelled']) + ->with('project') + ->orderBy('due_date', 'asc') + ->limit(5) + ->get(); + + // --- Calendar & i18n info --- + $currentCalendar = session('calendar', config('projectra.default_calendar', 'jalali')); + $currentLocale = app()->getLocale(); + $todayDisplay = CalendarHelper::getCurrentDate($currentCalendar); + if ($currentLocale === 'fa') { + $todayDisplay = persian_num($todayDisplay); + } + + return view('dashboard.index', compact( + 'activeProjectsCount', + 'totalBudget', + 'totalBudgetFormatted', + 'pendingTasksCount', + 'activeEmployeesCount', + 'projectTrend', + 'taskTrend', + 'expenseTrend', + 'employeeTrend', + 'projectsInProgress', + 'recentActivities', + 'myTasks', + 'currentCalendar', + 'currentLocale', + 'todayDisplay', + 'currencyCode' + )); + } +} diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php new file mode 100644 index 00000000..6847ee01 --- /dev/null +++ b/app/Http/Controllers/DocumentController.php @@ -0,0 +1,177 @@ +filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('file_type')) { + $query->where('file_type', $request->file_type); + } + + if ($request->filled('search')) { + $query->search($request->search); + } + + $documents = $query->orderBy('created_at', 'desc') + ->paginate(20) + ->appends($request->query()); + + $projects = Project::all(); + $fileTypes = ['pdf', 'image', 'word', 'excel', 'text', 'other']; + + return view('documents.index', compact('documents', 'projects', 'fileTypes')); + } + + /** + * Show the form for creating a new document. + */ + public function create() + { + $projects = Project::all(); + $document = null; + + return view('documents.form', compact('projects', 'document')); + } + + /** + * Store a newly created document in storage. + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'project_id' => 'nullable|exists:projects,id', + 'file' => 'required|file|max:20480', + ]); + + $file = $request->file('file'); + $path = $file->store('documents', 'public'); + + $extension = strtolower($file->getClientOriginalExtension()); + $fileType = match(true) { + $extension === 'pdf' => 'pdf', + in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp']) => 'image', + in_array($extension, ['doc', 'docx']) => 'word', + in_array($extension, ['xls', 'xlsx', 'csv']) => 'excel', + in_array($extension, ['txt', 'rtf']) => 'text', + default => 'other', + }; + + Document::create([ + 'tenant_id' => $this->getTenantId(), + 'project_id' => $validated['project_id'] ?? null, + 'title' => $validated['title'], + 'description' => $validated['description'] ?? null, + 'file_path' => $path, + 'file_type' => $fileType, + 'uploaded_by' => Auth::id(), + ]); + + return redirect() + ->route('documents.index') + ->with('success', __('document.created_successfully')); + } + + /** + * Display the specified document. + */ + public function show(Document $document) + { + $document->load(['project', 'uploader']); + + return view('documents.show', compact('document')); + } + + /** + * Show the form for editing the specified document. + */ + public function edit(Document $document) + { + $projects = Project::all(); + + return view('documents.form', compact('document', 'projects')); + } + + /** + * Update the specified document in storage. + */ + public function update(Request $request, Document $document) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'project_id' => 'nullable|exists:projects,id', + 'file' => 'nullable|file|max:20480', + ]); + + if ($request->hasFile('file')) { + // Delete old file + Storage::disk('public')->delete($document->file_path); + + $file = $request->file('file'); + $path = $file->store('documents', 'public'); + + $extension = strtolower($file->getClientOriginalExtension()); + $fileType = match(true) { + $extension === 'pdf' => 'pdf', + in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp']) => 'image', + in_array($extension, ['doc', 'docx']) => 'word', + in_array($extension, ['xls', 'xlsx', 'csv']) => 'excel', + in_array($extension, ['txt', 'rtf']) => 'text', + default => 'other', + }; + + $validated['file_path'] = $path; + $validated['file_type'] = $fileType; + } + + $document->update($validated); + + return redirect() + ->route('documents.index') + ->with('success', __('document.updated_successfully')); + } + + /** + * Remove the specified document from storage. + */ + public function destroy(Document $document) + { + Storage::disk('public')->delete($document->file_path); + $document->delete(); + + return redirect() + ->route('documents.index') + ->with('success', __('document.deleted_successfully')); + } + + /** + * Download the specified document. + */ + public function download(Document $document) + { + if (!Storage::disk('public')->exists($document->file_path)) { + abort(404, __('document.file_not_found')); + } + + return Storage::disk('public')->download($document->file_path, $document->title . '.' . pathinfo($document->file_path, PATHINFO_EXTENSION)); + } +} diff --git a/app/Http/Controllers/EmployeeController.php b/app/Http/Controllers/EmployeeController.php new file mode 100644 index 00000000..015de563 --- /dev/null +++ b/app/Http/Controllers/EmployeeController.php @@ -0,0 +1,216 @@ +authorize('viewAny', Employee::class); + + $query = Employee::with(['project', 'currency']); + + // Filter by project + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + // Filter by status (active/inactive) + if ($request->filled('status')) { + $query->where('status', $request->status); + } + + // Filter by contract type + if ($request->filled('contract_type')) { + $query->where('contract_type', $request->contract_type); + } + + // Filter by search + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('first_name', 'like', "%{$search}%") + ->orWhere('last_name', 'like', "%{$search}%") + ->orWhere('national_code', 'like', "%{$search}%") + ->orWhere('position', 'like', "%{$search}%"); + }); + } + + $query->orderBy('first_name')->orderBy('last_name'); + + $employees = $query->paginate(16)->appends($request->query()); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $contractTypes = ['full_time', 'part_time', 'contractor', 'intern', 'consultant']; + + return view('employees.index', compact('employees', 'projects', 'contractTypes')); + } + + /** + * Show the form for creating a new employee. + */ + public function create() + { + $this->authorize('create', Employee::class); + + $employee = null; + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $contractTypes = ['full_time', 'part_time', 'contractor', 'intern', 'consultant']; + + return view('employees.form', compact('employee', 'projects', 'currencies', 'contractTypes')); + } + + /** + * Store a newly created employee in storage. + */ + public function store(StoreEmployeeRequest $request) + { + $this->authorize('create', Employee::class); + + $data = $request->validated(); + $data['tenant_id'] = $this->getTenantId(); + + // Convert jalali date if needed + if (!empty($data['hire_date'])) { + $data['hire_date'] = \App\Helpers\CalendarHelper::parseDate($data['hire_date']); + } + + $employee = Employee::create($data); + + return redirect() + ->route('employees.show', $employee) + ->with('success', __('employee.created_successfully')); + } + + /** + * Display the specified employee with work logs and financials. + */ + public function show(Employee $employee) + { + $this->authorize('view', $employee); + + $employee->load([ + 'project', + 'currency', + 'workLogs.project', + 'workLogs.task', + 'financials.currency', + ]); + + return view('employees.show', compact('employee')); + } + + /** + * Show the form for editing the specified employee. + */ + public function edit(Employee $employee) + { + $this->authorize('update', $employee); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $contractTypes = ['full_time', 'part_time', 'contractor', 'intern', 'consultant']; + + return view('employees.form', compact('employee', 'projects', 'currencies', 'contractTypes')); + } + + /** + * Update the specified employee in storage. + */ + public function update(StoreEmployeeRequest $request, Employee $employee) + { + $this->authorize('update', $employee); + + $data = $request->validated(); + + // Convert jalali date if needed + if (!empty($data['hire_date'])) { + $data['hire_date'] = \App\Helpers\CalendarHelper::parseDate($data['hire_date']); + } + + $employee->update($data); + + return redirect() + ->route('employees.show', $employee) + ->with('success', __('employee.updated_successfully')); + } + + /** + * Remove the specified employee from storage. + */ + public function destroy(Employee $employee) + { + $this->authorize('delete', $employee); + + $employee->delete(); + + return redirect() + ->route('employees.index') + ->with('success', __('employee.deleted_successfully')); + } + + /** + * Display work logs for the specified employee. + */ + public function workLogs(Request $request, Employee $employee) + { + $this->authorize('view', $employee); + + $query = $employee->workLogs()->with(['project', 'task']); + + if ($request->filled('from_date') && $request->filled('to_date')) { + $query->dateRange($request->from_date, $request->to_date); + } + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + $totalHours = (clone $query)->sum('hours_worked'); + $overtimeHours = (clone $query)->overtime()->sum('overtime_hours'); + $workLogs = $query->orderBy('date', 'desc')->paginate(20); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + + return view('employees.work-logs', compact('employee', 'workLogs', 'totalHours', 'overtimeHours', 'projects')); + } + + /** + * Display financial records for the specified employee. + */ + public function financials(Request $request, Employee $employee) + { + $this->authorize('viewFinancials', $employee); + + $query = $employee->financials()->with('currency'); + + if ($request->filled('type')) { + $query->byType($request->type); + } + + if ($request->filled('is_paid') && $request->is_paid !== '') { + $query->where('is_paid', $request->boolean('is_paid')); + } + + $financials = $query->orderBy('effective_date', 'desc')->paginate(20); + + $totalPaid = $employee->financials()->where('is_paid', true)->sum('amount'); + $totalUnpaid = $employee->financials()->where('is_paid', false)->sum('amount'); + + $financialTypes = ['salary', 'bonus', 'deduction', 'overtime', 'advance', 'reimbursement']; + + return view('employees.financials', compact('employee', 'financials', 'totalPaid', 'totalUnpaid', 'financialTypes')); + } +} diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php new file mode 100644 index 00000000..212658e4 --- /dev/null +++ b/app/Http/Controllers/ExpenseController.php @@ -0,0 +1,258 @@ +authorize('viewAny', Expense::class); + + $query = Expense::with(['project', 'currency', 'requestedBy', 'approvedBy']); + + // Filter by project + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + // Filter by category + if ($request->filled('category')) { + $query->byCategory($request->category); + } + + // Filter by status + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + // Filter by date range + if ($request->filled('from_date')) { + $query->where('expense_date', '>=', $request->from_date); + } + if ($request->filled('to_date')) { + $query->where('expense_date', '<=', $request->to_date); + } + + $query->orderBy('expense_date', 'desc'); + + $expenses = $query->paginate(20)->appends($request->query()); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $categories = ['materials', 'labor', 'equipment', 'transport', 'services', 'food', 'accommodation', 'other']; + $statuses = ['pending', 'approved', 'rejected', 'paid']; + + return view('expenses.index', compact('expenses', 'projects', 'categories', 'statuses')); + } + + /** + * Show the form for creating a new expense (multi-currency aware). + */ + public function create() + { + $this->authorize('create', Expense::class); + + $expense = null; // ← این خط اضافه بشه + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $categories = ['materials', 'labor', 'equipment', 'transport', 'services', 'food', 'accommodation', 'other']; + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('expenses.form', compact('expense', 'projects', 'currencies', 'categories', 'baseCurrency')); + } + + /** + * Store a newly created expense in storage. + */ + public function store(StoreExpenseRequest $request) + { + $this->authorize('create', Expense::class); + + $data = $request->validated(); + $data['tenant_id'] = $this->getTenantId(); + $data['requested_by'] = Auth::id(); + + // Convert jalali date if needed + if (!empty($data['expense_date'])) { + $data['expense_date'] = \App\Helpers\CalendarHelper::parseDate($data['expense_date']); + } + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($data['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $data['original_amount'] = $data['amount']; + $data['amount'] = CurrencyHelper::convert( + (float) $data['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload + if ($request->hasFile('receipt')) { + $data['receipt_path'] = $request->file('receipt')->store('receipts', 'public'); + } + + $expense = Expense::create($data); + + // Handle multiple file attachments + if ($request->hasFile('attachments')) { + $expense->attachFiles($request->file('attachments'), 'receipt'); + } + + return redirect() + ->route('expenses.show', $expense) + ->with('success', __('expense.created_successfully')); + } + + /** + * Display the specified expense. + */ + public function show(Expense $expense) + { + $this->authorize('view', $expense); + + $expense->load(['project', 'currency', 'requestedBy', 'approvedBy', 'pettyCash']); + + return view('expenses.show', compact('expense')); + } + + /** + * Show the form for editing the specified expense. + */ + public function edit(Expense $expense) + { + $this->authorize('update', $expense); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $categories = ['materials', 'labor', 'equipment', 'transport', 'services', 'food', 'accommodation', 'other']; + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('expenses.form', compact('expense', 'projects', 'currencies', 'categories', 'baseCurrency')); + } + + /** + * Update the specified expense in storage. + */ + public function update(StoreExpenseRequest $request, Expense $expense) + { + $this->authorize('update', $expense); + + $data = $request->validated(); + + // Convert jalali date if needed + if (!empty($data['expense_date'])) { + $data['expense_date'] = \App\Helpers\CalendarHelper::parseDate($data['expense_date']); + } + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($data['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $data['original_amount'] = $data['amount']; + $data['amount'] = CurrencyHelper::convert( + (float) $data['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload + if ($request->hasFile('receipt')) { + // Delete old receipt + if ($expense->receipt_path) { + \Illuminate\Support\Facades\Storage::disk('public')->delete($expense->receipt_path); + } + $data['receipt_path'] = $request->file('receipt')->store('receipts', 'public'); + } + + $expense->update($data); + + // Handle multiple file attachments + if ($request->hasFile('attachments')) { + $expense->attachFiles($request->file('attachments'), 'receipt'); + } + + return redirect() + ->route('expenses.show', $expense) + ->with('success', __('expense.updated_successfully')); + } + + /** + * Remove the specified expense from storage. + */ + public function destroy(Expense $expense) + { + $this->authorize('delete', $expense); + + $expense->delete(); + + return redirect() + ->route('expenses.index') + ->with('success', __('expense.deleted_successfully')); + } + + /** + * Approve an expense via AJAX. + */ + public function approve(Expense $expense) + { + $this->authorize('approve', $expense); + + $expense->update([ + 'status' => 'approved', + 'approved_by' => Auth::id(), + 'approval_date' => now(), + ]); + + // Update petty cash remaining if linked + if ($expense->petty_cash_id && $expense->pettyCash) { + $expense->pettyCash->updateRemaining(); + } + + return response()->json([ + 'success' => true, + 'message' => __('expense.approved_successfully'), + 'status' => $expense->status, + ]); + } + + /** + * Reject an expense via AJAX. + */ + public function reject(Expense $expense) + { + $this->authorize('approve', $expense); + + $expense->update([ + 'status' => 'rejected', + 'approved_by' => Auth::id(), + 'approval_date' => now(), + ]); + + // Update petty cash remaining if linked + if ($expense->petty_cash_id && $expense->pettyCash) { + $expense->pettyCash->updateRemaining(); + } + + return response()->json([ + 'success' => true, + 'message' => __('expense.rejected_successfully'), + 'status' => $expense->status, + ]); + } +} diff --git a/app/Http/Controllers/InventoryController.php b/app/Http/Controllers/InventoryController.php new file mode 100644 index 00000000..84452a2b --- /dev/null +++ b/app/Http/Controllers/InventoryController.php @@ -0,0 +1,441 @@ +count(); + $totalWarehouses = Warehouse::where('is_active', true)->count(); + $recentTransactions = InventoryTransaction::with(['item', 'warehouse', 'createdBy']) + ->orderBy('created_at', 'desc') + ->limit(10) + ->get(); + + // Low stock items (below min_stock or zero) + $lowStockItems = Item::where('is_active', true) + ->with(['inventoryTransactions']) + ->get() + ->filter(function ($item) { + $stock = $this->getCurrentStockForItem($item->id); + $minStock = $item->min_stock ?? 0; + return $stock <= $minStock; + }) + ->take(10); + + return view('inventory.index', compact( + 'totalItems', 'activeItems', 'totalWarehouses', + 'recentTransactions', 'lowStockItems' + )); + } + + // ========================================== + // ITEMS CRUD + // ========================================== + + public function items(Request $request) + { + $query = Item::with(['inventoryTransactions.currency', 'inventoryTransactions.warehouse']); + + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('code', 'like', "%{$search}%") + ->orWhere('category', 'like', "%{$search}%"); + }); + } + if ($request->filled('category')) { + $query->where('category', $request->category); + } + if ($request->filled('is_active')) { + $query->where('is_active', $request->boolean('is_active')); + } + + $items = $query->orderBy('name')->paginate(20)->appends($request->query()); + $categories = Item::distinct()->pluck('category')->filter()->values(); + + // Calculate current stock for each item + $items->through(function ($item) { + $item->current_stock = $this->getCurrentStockForItem($item->id); + return $item; + }); + + return view('inventory.items', compact('items', 'categories')); + } + + public function createItem() + { + $this->authorize('create', Item::class); + + $item = null; + $categories = Item::distinct()->pluck('category')->filter()->values(); + + return view('inventory.item-form', compact('item', 'categories')); + } + + public function storeItem(Request $request) + { + $this->authorize('create', Item::class); + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'code' => 'nullable|string|max:50|unique:items,code', + 'unit' => 'required|string|max:50', + 'category' => 'nullable|string|max:100', + 'description' => 'nullable|string', + 'min_stock' => 'nullable|numeric|min:0', + 'is_active' => 'boolean', + ]); + + $validated['tenant_id'] = $this->getTenantId(); + $validated['is_active'] = $request->boolean('is_active', true); + + $item = Item::create($validated); + + return redirect() + ->route('inventory.items') + ->with('success', __('inventory.item_created')); + } + + public function editItem(Item $item) + { + $this->authorize('update', $item); + + $categories = Item::distinct()->pluck('category')->filter()->values(); + + return view('inventory.item-form', compact('item', 'categories')); + } + + public function updateItem(Request $request, Item $item) + { + $this->authorize('update', $item); + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'code' => 'nullable|string|max:50|unique:items,code,' . $item->id, + 'unit' => 'required|string|max:50', + 'category' => 'nullable|string|max:100', + 'description' => 'nullable|string', + 'min_stock' => 'nullable|numeric|min:0', + 'is_active' => 'boolean', + ]); + + $validated['is_active'] = $request->boolean('is_active'); + + $item->update($validated); + + return redirect() + ->route('inventory.items') + ->with('success', __('inventory.item_updated')); + } + + public function destroyItem(Item $item) + { + $this->authorize('delete', $item); + + // Check if item has transactions + if ($item->inventoryTransactions()->count() > 0) { + return back()->withErrors(['item' => __('inventory.item_has_transactions')]); + } + + $item->delete(); + + return redirect() + ->route('inventory.items') + ->with('success', __('inventory.item_deleted')); + } + + // ========================================== + // WAREHOUSES CRUD + // ========================================== + + public function warehouses(Request $request) + { + $query = Warehouse::with(['project']); + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + if ($request->filled('is_active')) { + $query->where('is_active', $request->boolean('is_active')); + } + + $warehouses = $query->orderBy('name')->paginate(20)->appends($request->query()); + $projects = Project::all(); + + return view('inventory.warehouses', compact('warehouses', 'projects')); + } + + public function createWarehouse() + { + $this->authorize('create', Warehouse::class); + + $warehouse = null; + $projects = Project::all(); + + return view('inventory.warehouse-form', compact('warehouse', 'projects')); + } + + public function storeWarehouse(Request $request) + { + $this->authorize('create', Warehouse::class); + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'location' => 'nullable|string|max:255', + 'project_id' => 'nullable|exists:projects,id', + 'is_active' => 'boolean', + ]); + + $validated['tenant_id'] = $this->getTenantId(); + $validated['is_active'] = $request->boolean('is_active', true); + + Warehouse::create($validated); + + return redirect() + ->route('inventory.warehouses') + ->with('success', __('inventory.warehouse_created')); + } + + public function editWarehouse(Warehouse $warehouse) + { + $this->authorize('update', $warehouse); + + $projects = Project::all(); + + return view('inventory.warehouse-form', compact('warehouse', 'projects')); + } + + public function updateWarehouse(Request $request, Warehouse $warehouse) + { + $this->authorize('update', $warehouse); + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'location' => 'nullable|string|max:255', + 'project_id' => 'nullable|exists:projects,id', + 'is_active' => 'boolean', + ]); + + $validated['is_active'] = $request->boolean('is_active'); + + $warehouse->update($validated); + + return redirect() + ->route('inventory.warehouses') + ->with('success', __('inventory.warehouse_updated')); + } + + public function destroyWarehouse(Warehouse $warehouse) + { + $this->authorize('delete', $warehouse); + + if ($warehouse->inventoryTransactions()->count() > 0) { + return back()->withErrors(['warehouse' => __('inventory.warehouse_has_transactions')]); + } + + $warehouse->delete(); + + return redirect() + ->route('inventory.warehouses') + ->with('success', __('inventory.warehouse_deleted')); + } + + // ========================================== + // TRANSACTIONS + // ========================================== + + public function transactions(Request $request) + { + $query = InventoryTransaction::with(['item', 'warehouse', 'project', 'currency', 'createdBy']); + + if ($request->filled('item_id')) { $query->where('item_id', $request->item_id); } + if ($request->filled('warehouse_id')) { $query->where('warehouse_id', $request->warehouse_id); } + if ($request->filled('type')) { $query->byType($request->type); } + if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } + if ($request->filled('from_date')) { $query->where('created_at', '>=', $request->from_date); } + if ($request->filled('to_date')) { $query->where('created_at', '<=', $request->to_date); } + + $transactions = $query->orderBy('created_at', 'desc')->paginate(20)->appends($request->query()); + $items = Item::where('is_active', true)->orderBy('name')->get(); + $warehouses = Warehouse::where('is_active', true)->orderBy('name')->get(); + $projects = Project::all(); + $currencies = Currency::where('is_active', true)->get(); + $types = ['in', 'out', 'transfer', 'return', 'adjustment']; + + return view('inventory.transactions', compact( + 'transactions', 'items', 'warehouses', 'projects', 'currencies', 'types' + )); + } + + public function stockIn(Request $request) + { + $validated = $request->validate([ + 'item_id' => 'required|exists:items,id', + 'warehouse_id' => 'required|exists:warehouses,id', + 'project_id' => 'nullable|exists:projects,id', + 'quantity' => 'required|numeric|min:0.01', + 'unit_price' => 'nullable|numeric|min:0', + 'currency_id' => 'nullable|exists:currencies,id', + 'reference_number' => 'nullable|string|max:100', + 'notes' => 'nullable|string', + ]); + + $validated['tenant_id'] = $this->getTenantId(); + $validated['type'] = 'in'; + $validated['created_by'] = Auth::id(); + + if (!empty($validated['unit_price']) && !empty($validated['quantity'])) { + $validated['total_price'] = $validated['unit_price'] * $validated['quantity']; + } + + InventoryTransaction::create($validated); + + return redirect() + ->route('inventory.transactions') + ->with('success', __('inventory.stock_in_success')); + } + + public function stockOut(Request $request) + { + $validated = $request->validate([ + 'item_id' => 'required|exists:items,id', + 'warehouse_id' => 'required|exists:warehouses,id', + 'project_id' => 'nullable|exists:projects,id', + 'quantity' => 'required|numeric|min:0.01', + 'unit_price' => 'nullable|numeric|min:0', + 'currency_id' => 'nullable|exists:currencies,id', + 'reference_number' => 'nullable|string|max:100', + 'notes' => 'nullable|string', + ]); + + $currentStock = $this->getCurrentStockForItem($validated['item_id'], $validated['warehouse_id']); + + if ($currentStock < $validated['quantity']) { + return back() + ->withErrors(['quantity' => __('inventory.insufficient_stock')]) + ->withInput(); + } + + $validated['tenant_id'] = $this->getTenantId(); + $validated['type'] = 'out'; + $validated['created_by'] = Auth::id(); + + if (!empty($validated['unit_price']) && !empty($validated['quantity'])) { + $validated['total_price'] = $validated['unit_price'] * $validated['quantity']; + } + + InventoryTransaction::create($validated); + + return redirect() + ->route('inventory.transactions') + ->with('success', __('inventory.stock_out_success')); + } + + public function stockTransfer(Request $request) + { + $validated = $request->validate([ + 'item_id' => 'required|exists:items,id', + 'from_warehouse_id' => 'required|exists:warehouses,id', + 'to_warehouse_id' => 'required|exists:warehouses,id|different:from_warehouse_id', + 'project_id' => 'nullable|exists:projects,id', + 'quantity' => 'required|numeric|min:0.01', + 'notes' => 'nullable|string', + ]); + + // Check source stock + $currentStock = $this->getCurrentStockForItem($validated['item_id'], $validated['from_warehouse_id']); + if ($currentStock < $validated['quantity']) { + return back() + ->withErrors(['quantity' => __('inventory.insufficient_stock_source')]) + ->withInput(); + } + + DB::transaction(function () use ($validated) { + $tenantId = $this->getTenantId(); + $userId = Auth::id(); + + // Out from source + InventoryTransaction::create([ + 'tenant_id' => $tenantId, + 'item_id' => $validated['item_id'], + 'warehouse_id' => $validated['from_warehouse_id'], + 'project_id' => $validated['project_id'] ?? null, + 'type' => 'transfer', + 'quantity' => $validated['quantity'], + 'reference_number' => 'TRF-' . time(), + 'notes' => ($validated['notes'] ?? '') . ' — انتقال به انبار مقصد', + 'created_by' => $userId, + ]); + + // In to destination + InventoryTransaction::create([ + 'tenant_id' => $tenantId, + 'item_id' => $validated['item_id'], + 'warehouse_id' => $validated['to_warehouse_id'], + 'project_id' => $validated['project_id'] ?? null, + 'type' => 'transfer', + 'quantity' => $validated['quantity'], + 'reference_number' => 'TRF-' . time(), + 'notes' => ($validated['notes'] ?? '') . ' — انتقال از انبار مبدأ', + 'created_by' => $userId, + ]); + }); + + return redirect() + ->route('inventory.transactions') + ->with('success', __('inventory.transfer_success')); + } + + public function currentStock(Request $request) + { + $request->validate([ + 'item_id' => 'required|exists:items,id', + 'warehouse_id' => 'nullable|exists:warehouses,id', + ]); + + $stock = $this->getCurrentStockForItem($request->item_id, $request->warehouse_id); + + return response()->json([ + 'item_id' => $request->item_id, + 'warehouse_id' => $request->warehouse_id, + 'current_stock' => $stock, + ]); + } + + // ========================================== + // STOCK HELPERS + // ========================================== + + private function getCurrentStockForItem(int $itemId, ?int $warehouseId = null): float + { + $query = InventoryTransaction::where('item_id', $itemId); + + if ($warehouseId) { + $query->where('warehouse_id', $warehouseId); + } + + $totalIn = (clone $query)->whereIn('type', ['in', 'return'])->sum('quantity'); + $totalOut = (clone $query)->where('type', 'out')->sum('quantity'); + $adjustments = (clone $query)->where('type', 'adjustment')->sum('quantity'); + + return (float) ($totalIn - $totalOut + $adjustments); + } +} diff --git a/app/Http/Controllers/InventoryTransactionController.php b/app/Http/Controllers/InventoryTransactionController.php new file mode 100644 index 00000000..d9ca1fa0 --- /dev/null +++ b/app/Http/Controllers/InventoryTransactionController.php @@ -0,0 +1,176 @@ +tenant_id; + $query = InventoryTransaction::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->with(['item', 'warehouse', 'project', 'currency', 'createdBy']); + + if ($request->filled('type')) { + $query->where('type', $request->type); + } + + if ($request->filled('item_id')) { + $query->where('item_id', $request->item_id); + } + + if ($request->filled('warehouse_id')) { + $query->where('warehouse_id', $request->warehouse_id); + } + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('from_date')) { + $query->where('date', '>=', $request->from_date); + } + + if ($request->filled('to_date')) { + $query->where('date', '<=', $request->to_date); + } + + $transactions = $query->orderBy('date', 'desc')->orderBy('created_at', 'desc')->paginate(20)->appends($request->query()); + + $items = Item::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->orderBy('name')->get(); + + $warehouses = Warehouse::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->where('is_active', true)->orderBy('name')->get(); + + $projects = Project::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->whereIn('status', ['planning', 'in_progress'])->get(); + + $types = ['in', 'out', 'return', 'adjustment']; + + return view('inventory-transactions.index', compact('transactions', 'items', 'warehouses', 'projects', 'types')); + } + + public function create() + { + $tenantId = tenant_id() ?? Auth::user()?->tenant_id; + $warehouses = Warehouse::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->orderBy('name')->get(); + + $warehouses = Warehouse::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->where('is_active', true)->orderBy('name')->get(); + + $projects = Project::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->whereIn('status', ['planning', 'in_progress'])->get(); + + $currencies = Currency::all(); + $types = ['in' => 'in', 'out' => 'out', 'return' => 'return', 'adjustment' => 'adjustment']; + + return view('inventory-transactions.form', compact('items', 'warehouses', 'projects', 'currencies', 'types')); + } + + public function store(Request $request) + { + $tenantId = tenant_id() ?? Auth::user()?->tenant_id; + + $validated = $request->validate([ + 'type' => 'required|in:in,out,return,adjustment', + 'item_id' => 'required|exists:items,id', + 'warehouse_id' => 'required|exists:warehouses,id', + 'project_id' => 'nullable|exists:projects,id', + 'quantity' => 'required|numeric|min:0.01', + 'unit_price' => 'nullable|numeric|min:0', + 'currency_id' => 'nullable|exists:currencies,id', + 'date' => 'required|date', + 'reference' => 'nullable|string|max:100', + 'description' => 'nullable|string', + ]); + + // بررسی موجودی برای خروج + if ($validated['type'] === 'out') { + $currentStock = $this->getCurrentStock($validated['item_id'], $validated['warehouse_id']); + if ($currentStock < $validated['quantity']) { + return back()->withErrors(['quantity' => __('inventory.insufficient_stock')]) + ->withInput(); + } + } + + $validated['tenant_id'] = $tenantId; + $validated['created_by'] = Auth::id(); + + if (!empty($validated['unit_price']) && !empty($validated['quantity'])) { + $validated['total_price'] = $validated['unit_price'] * $validated['quantity']; + } + + InventoryTransaction::create($validated); + + return redirect() + ->route('inventory-transactions.index') + ->with('success', __('inventory.transaction_created_successfully')); + } + + public function show(InventoryTransaction $inventoryTransaction) + { + $inventoryTransaction->load(['item', 'warehouse', 'project', 'currency', 'createdBy']); + + return view('inventory-transactions.show', compact('inventoryTransaction')); + } + + public function destroy(InventoryTransaction $inventoryTransaction) + { + $inventoryTransaction->delete(); + + return redirect() + ->route('inventory-transactions.index') + ->with('success', __('inventory.transaction_deleted_successfully')); + } + + public function currentStock(Request $request) + { + $request->validate([ + 'item_id' => 'required|exists:items,id', + 'warehouse_id' => 'required|exists:warehouses,id', + ]); + + $stock = $this->getCurrentStock($request->item_id, $request->warehouse_id); + + return response()->json([ + 'current_stock' => $stock, + ]); + } + + private function getCurrentStock(int $itemId, int $warehouseId): float + { + $totalIn = InventoryTransaction::where('item_id', $itemId) + ->where('warehouse_id', $warehouseId) + ->whereIn('type', ['in', 'return']) + ->sum('quantity'); + + $totalOut = InventoryTransaction::where('item_id', $itemId) + ->where('warehouse_id', $warehouseId) + ->where('type', 'out') + ->sum('quantity'); + + $adjustments = InventoryTransaction::where('item_id', $itemId) + ->where('warehouse_id', $warehouseId) + ->where('type', 'adjustment') + ->sum('quantity'); + + return (float) ($totalIn - $totalOut + $adjustments); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/LetterController.php b/app/Http/Controllers/LetterController.php new file mode 100644 index 00000000..4ee407a4 --- /dev/null +++ b/app/Http/Controllers/LetterController.php @@ -0,0 +1,258 @@ +authorize('viewAny', Letter::class); + + $query = Letter::with(['project', 'attachments']); + + if ($request->filled('type')) { + $query->byType($request->type); + } + + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('search')) { + $query->search($request->search); + } + + $letters = $query->orderBy('letter_date', 'desc') + ->orderBy('created_at', 'desc') + ->paginate(20) + ->appends($request->query()); + + $projects = Project::all(); + $types = ['incoming', 'outgoing']; + $statuses = ['draft', 'sent', 'received', 'archived']; + + return view('letters.index', compact('letters', 'projects', 'types', 'statuses')); + } + + /** + * Show the form for creating a new letter. + */ + public function create(Request $request) + { + $this->authorize('create', Letter::class); + + $projects = Project::all(); + $letters = Letter::orderBy('letter_date', 'desc')->limit(50)->get(); + $letter = null; + + // Pre-fill parent_letter_id if provided + $preselectedParent = $request->get('parent_letter_id'); + + return view('letters.form', compact('projects', 'letters', 'letter', 'preselectedParent')); + } + + /** + * Store a newly created letter in storage. + */ + public function store(Request $request) + { + $this->authorize('create', Letter::class); + + $validated = $request->validate([ + 'type' => 'required|in:incoming,outgoing', + 'subject' => 'required|string|max:255', + 'sender' => 'required|string|max:255', + 'recipient' => 'required|string|max:255', + 'letter_date' => 'required|date', + 'reference_number' => 'nullable|string|max:100', + 'project_id' => 'nullable|exists:projects,id', + 'parent_letter_id' => 'nullable|exists:letters,id', + 'status' => 'required|in:draft,sent,received,archived', + 'content' => 'nullable|string', + 'attachments.*' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:10240', + ]); + + // Convert jalali date if needed + if (!empty($validated['letter_date'])) { + $validated['letter_date'] = \App\Helpers\CalendarHelper::parseDate($validated['letter_date']); + } + + $validated['tenant_id'] = $this->getTenantId(); + + $letter = Letter::create($validated); + + // Handle file attachments + if ($request->hasFile('attachments')) { + foreach ($request->file('attachments') as $file) { + $this->storeAttachment($letter, $file); + } + } + + // Remove checked attachments (for edit, but safe to check) + if ($request->has('remove_attachments')) { + foreach ($request->remove_attachments as $attachmentId) { + $this->removeAttachment($attachmentId, $letter); + } + } + + return redirect() + ->route('letters.show', $letter) + ->with('success', __('letter.created_successfully')); + } + + /** + * Display the specified letter. + */ + public function show(Letter $letter) + { + $this->authorize('view', $letter); + + $letter->load(['project', 'parentLetter', 'replies', 'attachments']); + + return view('letters.show', compact('letter')); + } + + /** + * Show the form for editing the specified letter. + */ + public function edit(Letter $letter) + { + $this->authorize('update', $letter); + + $letter->load('attachments'); + + $projects = Project::all(); + $letters = Letter::where('id', '!=', $letter->id) + ->orderBy('letter_date', 'desc') + ->limit(50) + ->get(); + + return view('letters.form', compact('letter', 'projects', 'letters')); + } + + /** + * Update the specified letter in storage. + */ + public function update(Request $request, Letter $letter) + { + $this->authorize('update', $letter); + + $validated = $request->validate([ + 'type' => 'required|in:incoming,outgoing', + 'subject' => 'required|string|max:255', + 'sender' => 'required|string|max:255', + 'recipient' => 'required|string|max:255', + 'letter_date' => 'required|date', + 'reference_number' => 'nullable|string|max:100', + 'project_id' => 'nullable|exists:projects,id', + 'parent_letter_id' => 'nullable|exists:letters,id', + 'status' => 'required|in:draft,sent,received,archived', + 'content' => 'nullable|string', + 'attachments.*' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:10240', + ]); + + // Convert jalali date if needed + if (!empty($validated['letter_date'])) { + $validated['letter_date'] = \App\Helpers\CalendarHelper::parseDate($validated['letter_date']); + } + + $letter->update($validated); + + // Handle file attachments + if ($request->hasFile('attachments')) { + foreach ($request->file('attachments') as $file) { + $this->storeAttachment($letter, $file); + } + } + + // Remove checked attachments + if ($request->has('remove_attachments')) { + foreach ($request->remove_attachments as $attachmentId) { + $this->removeAttachment($attachmentId, $letter); + } + } + + return redirect() + ->route('letters.show', $letter) + ->with('success', __('letter.updated_successfully')); + } + + /** + * Remove the specified letter from storage. + */ + public function destroy(Letter $letter) + { + $this->authorize('delete', $letter); + + // Delete attachments from storage + foreach ($letter->attachments as $attachment) { + Storage::disk('public')->delete($attachment->file_path); + } + + $letter->delete(); + + return redirect() + ->route('letters.index') + ->with('success', __('letter.deleted_successfully')); + } + + /** + * Archive the specified letter. + */ + public function archive(Letter $letter) + { + $this->authorize('update', $letter); + + $letter->update(['status' => 'archived']); + + return response()->json([ + 'success' => true, + 'message' => __('letter.archived_successfully'), + ]); + } + + /** + * Store a file attachment for a letter. + */ + private function storeAttachment(Letter $letter, $file): void + { + $path = $file->store('letters/' . $letter->id, 'public'); + + LetterAttachment::create([ + 'letter_id' => $letter->id, + 'file_name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'file_size' => $file->getSize(), + 'mime_type' => $file->getMimeType(), + ]); + } + + /** + * Remove a letter attachment. + */ + private function removeAttachment(int $attachmentId, Letter $letter): void + { + $attachment = LetterAttachment::where('id', $attachmentId) + ->where('letter_id', $letter->id) + ->first(); + + if ($attachment) { + Storage::disk('public')->delete($attachment->file_path); + $attachment->delete(); + } + } +} diff --git a/app/Http/Controllers/PayrollController.php b/app/Http/Controllers/PayrollController.php new file mode 100644 index 00000000..1b883d5c --- /dev/null +++ b/app/Http/Controllers/PayrollController.php @@ -0,0 +1,467 @@ +year; + $currentMonth = now()->month; + + $year = $request->get('year', $currentYear); + $month = $request->get('month', $currentMonth); + + // Statistics for the selected month/year + $financials = EmployeeFinancial::with(['employee', 'currency']) + ->where('tenant_id', $this->getTenantId()) + ->where('month', $month) + ->where('year', $year) + ->where('is_settlement', false) + ->orderBy('employee_id') + ->orderBy('type') + ->paginate(30) + ->appends($request->query()); + + // Calculate totals for the month + $allMonthFinancials = EmployeeFinancial::where('tenant_id', $this->getTenantId()) + ->where('month', $month) + ->where('year', $year) + ->where('is_settlement', false) + ->get(); + + $totalGross = $allMonthFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); + $totalDeductions = $allMonthFinancials->where('type', 'deduction')->sum('amount'); + $totalAdvances = $allMonthFinancials->where('type', 'advance')->sum('amount'); + $totalNet = $totalGross - $totalDeductions - $totalAdvances; + $totalPaid = $allMonthFinancials->where('is_paid', true)->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); + $totalUnpaid = $totalGross - $totalPaid; + + // Employee count with financials this month + $activeEmployeeCount = $allMonthFinancials->pluck('employee_id')->unique()->count(); + $unpaidEmployeeCount = $allMonthFinancials->where('is_paid', false)->pluck('employee_id')->unique()->count(); + + $employees = Employee::where('tenant_id', $this->getTenantId()) + ->where('status', 'active') + ->get(); + $months = range(1, 12); + $years = range($currentYear - 2, $currentYear + 1); + + // Persian month names + $persianMonths = [ + 1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد', + 4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور', + 7 => 'مهر', 8 => 'آبان', 9 => 'آذر', + 10 => 'دی', 11 => 'بهمن', 12 => 'اسفند' + ]; + + return view('payroll.index', compact( + 'financials', 'employees', 'months', 'years', 'year', 'month', + 'totalGross', 'totalDeductions', 'totalAdvances', 'totalNet', + 'totalPaid', 'totalUnpaid', 'activeEmployeeCount', 'unpaidEmployeeCount', + 'persianMonths' + )); + } + + /** + * Show payroll calculation form. + */ + public function calculate(Request $request) + { + $employees = Employee::where('tenant_id', $this->getTenantId()) + ->where('status', 'active') + ->with(['financials' => function ($q) { + $q->where('month', now()->month)->where('year', now()->year); + }]) + ->get(); + $currencies = Currency::where('is_active', true)->get(); + + $currentMonth = now()->month; + $currentYear = now()->year; + + return view('payroll.calculate', compact('employees', 'currencies', 'currentMonth', 'currentYear')); + } + + /** + * Auto-calculate payroll for a month. + * Generates salary entries for all active employees based on base_salary + work logs. + */ + public function autoCalculate(Request $request) + { + $validated = $request->validate([ + 'month' => 'required|integer|min:1|max:12', + 'year' => 'required|integer|min:1400|max:1500', + 'employee_ids' => 'nullable|array', + 'employee_ids.*' => 'exists:employees,id', + 'include_worklogs' => 'nullable|boolean', + ]); + + $tenantId = $this->getTenantId(); + $month = $validated['month']; + $year = $validated['year']; + + // Get employees to process + $query = Employee::where('tenant_id', $tenantId)->where('status', 'active'); + if (!empty($validated['employee_ids'])) { + $query->whereIn('id', $validated['employee_ids']); + } + $employees = $query->get(); + + $created = 0; + $skipped = 0; + + foreach ($employees as $employee) { + // Check if salary already exists for this month + $existingSalary = EmployeeFinancial::where('tenant_id', $tenantId) + ->where('employee_id', $employee->id) + ->where('type', 'salary') + ->where('month', $month) + ->where('year', $year) + ->where('is_settlement', false) + ->first(); + + if ($existingSalary) { + $skipped++; + continue; + } + + // Calculate overtime from work logs + $overtimeHours = 0; + $regularHours = 0; + if (!empty($validated['include_worklogs'])) { + $workLogs = EmployeeWorkLog::where('employee_id', $employee->id) + ->whereMonth('date', $month) + ->whereYear('date', $year) + ->get(); + $regularHours = $workLogs->sum('hours'); + $overtimeHours = $workLogs->sum('overtime_hours'); + } + + // Get currency + $currencyId = $employee->currency_id ?? Currency::where('is_default', true)->value('id'); + + // Create salary entry + if ($employee->base_salary > 0) { + EmployeeFinancial::create([ + 'tenant_id' => $tenantId, + 'employee_id' => $employee->id, + 'type' => 'salary', + 'amount' => $employee->base_salary, + 'currency_id' => $currencyId, + 'effective_date' => now(), + 'month' => $month, + 'year' => $year, + 'is_paid' => false, + 'created_by' => Auth::id(), + 'description' => __('payroll.auto_salary_description', ['month' => $month, 'year' => $year]), + ]); + $created++; + } + + // Create overtime entry if there are overtime hours + if ($overtimeHours > 0 && $employee->base_salary > 0) { + $hourlyRate = $employee->base_salary / 200; // rough estimate: 200 working hours/month + $overtimeAmount = round($overtimeHours * $hourlyRate * 1.4); // 1.4x for overtime + + EmployeeFinancial::create([ + 'tenant_id' => $tenantId, + 'employee_id' => $employee->id, + 'type' => 'overtime', + 'amount' => $overtimeAmount, + 'currency_id' => $currencyId, + 'effective_date' => now(), + 'month' => $month, + 'year' => $year, + 'is_paid' => false, + 'created_by' => Auth::id(), + 'description' => __('payroll.auto_overtime_description', ['hours' => $overtimeHours]), + ]); + } + } + + $message = __('payroll.auto_calculated', ['created' => $created, 'skipped' => $skipped]); + + return redirect()->route('payroll.index', ['month' => $month, 'year' => $year]) + ->with('success', $message); + } + + /** + * Store a new payroll entry. + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:employees,id', + 'type' => 'required|in:salary,overtime,bonus,deduction,advance,allowance', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'nullable|exists:currencies,id', + 'description' => 'nullable|string', + 'effective_date' => 'required|date', + 'month' => 'required|integer|min:1|max:12', + 'year' => 'required|integer|min:1400|max:1500', + 'notes' => 'nullable|string', + ]); + + $validated['tenant_id'] = $this->getTenantId(); + $validated['created_by'] = Auth::id(); + $validated['is_paid'] = false; + + // Convert jalali date if needed + if (!empty($validated['effective_date'])) { + $validated['effective_date'] = \App\Helpers\CalendarHelper::parseDate($validated['effective_date']); + } + + EmployeeFinancial::create($validated); + + return redirect() + ->route('payroll.index', ['month' => $validated['month'], 'year' => $validated['year']]) + ->with('success', __('payroll.created_successfully')); + } + + /** + * Show employee payroll detail. + */ + public function show(Employee $employee, Request $request) + { + $year = $request->get('year', now()->year); + + $financials = EmployeeFinancial::with(['currency']) + ->where('employee_id', $employee->id) + ->where('year', $year) + ->orderBy('month') + ->orderBy('type') + ->get() + ->groupBy('month'); + + // Calculate yearly totals + $allYearFinancials = EmployeeFinancial::where('employee_id', $employee->id) + ->where('year', $year) + ->get(); + + $yearlyGross = $allYearFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); + $yearlyDeductions = $allYearFinancials->where('type', 'deduction')->sum('amount'); + $yearlyAdvances = $allYearFinancials->where('type', 'advance')->sum('amount'); + $yearlyNet = $yearlyGross - $yearlyDeductions - $yearlyAdvances; + $yearlyPaid = $allYearFinancials->where('is_paid', true)->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); + $yearlyUnpaid = $yearlyGross - $yearlyPaid; + + // Work logs for the year + $workLogs = EmployeeWorkLog::where('employee_id', $employee->id) + ->whereYear('date', $year) + ->get() + ->groupBy(function ($item) { + return \App\Helpers\CalendarHelper::formatDate($item->date, 'n'); + }); + + $persianMonths = [ + 1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد', + 4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور', + 7 => 'مهر', 8 => 'آبان', 9 => 'آذر', + 10 => 'دی', 11 => 'بهمن', 12 => 'اسفند' + ]; + + return view('payroll.show', compact( + 'employee', 'financials', 'year', 'persianMonths', + 'yearlyGross', 'yearlyDeductions', 'yearlyAdvances', 'yearlyNet', + 'yearlyPaid', 'yearlyUnpaid' + )); + } + + /** + * Mark a financial entry as paid. + */ + public function pay(EmployeeFinancial $financial) + { + $financial->update([ + 'is_paid' => true, + 'paid_at' => now(), + ]); + + return redirect()->back() + ->with('success', __('payroll.marked_as_paid')); + } + + /** + * Bulk pay financial entries. + */ + public function payBulk(Request $request) + { + $validated = $request->validate([ + 'financial_ids' => 'required|array', + 'financial_ids.*' => 'exists:employee_financials,id', + ]); + + $count = EmployeeFinancial::whereIn('id', $validated['financial_ids']) + ->where('is_paid', false) + ->update([ + 'is_paid' => true, + 'paid_at' => now(), + ]); + + return redirect()->back() + ->with('success', __('payroll.bulk_paid_successfully') . " ($count)"); + } + + /** + * Delete a financial entry. + */ + public function destroy(EmployeeFinancial $financial) + { + if ($financial->is_paid) { + return redirect()->back() + ->with('error', __('payroll.cannot_delete_paid')); + } + + $financial->delete(); + + return redirect()->back() + ->with('success', __('payroll.deleted_successfully')); + } + + /** + * Show settlement form. + */ + public function settlement() + { + $employees = Employee::where('tenant_id', $this->getTenantId()) + ->whereIn('status', ['active', 'inactive']) + ->get(); + $currencies = Currency::where('is_active', true)->get(); + + return view('payroll.settlement', compact('employees', 'currencies')); + } + + /** + * Preview settlement calculation. + */ + public function previewSettlement(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:employees,id', + 'settlement_type' => 'required|in:resignation,termination,end_of_contract', + 'settlement_date' => 'required|date', + 'last_working_date' => 'nullable|date', + ]); + + $employee = Employee::findOrFail($validated['employee_id']); + $settlementDate = \App\Helpers\CalendarHelper::parseDate($validated['settlement_date']); + + // Get all unpaid financials + $unpaidFinancials = EmployeeFinancial::where('employee_id', $employee->id) + ->where('is_paid', false) + ->with('currency') + ->get(); + + $totalPayable = $unpaidFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); + $totalDeductions = $unpaidFinancials->where('type', 'deduction')->sum('amount'); + $totalAdvances = $unpaidFinancials->where('type', 'advance')->sum('amount'); + $netPayable = $totalPayable - $totalDeductions - $totalAdvances; + + // Unused vacation days calculation (rough: 2.5 days per month) + $monthsWorked = 0; + if ($employee->hire_date) { + $hireDate = \Carbon\Carbon::parse($employee->hire_date); + $settlementCarbon = \Carbon\Carbon::parse($settlementDate); + $monthsWorked = $hireDate->diffInMonths($settlementCarbon); + } + $vacationDays = min($monthsWorked * 2.5, 26); // max 26 days + + return response()->json([ + 'employee' => $employee->only(['id', 'first_name', 'last_name', 'base_salary']), + 'unpaid_financials' => $unpaidFinancials->map(function ($f) { + return [ + 'id' => $f->id, + 'type' => $f->type, + 'amount' => $f->amount, + 'month' => $f->month, + 'year' => $f->year, + 'description' => $f->description, + ]; + }), + 'total_payable' => $totalPayable, + 'total_deductions' => $totalDeductions, + 'total_advances' => $totalAdvances, + 'net_payable' => $netPayable, + 'months_worked' => $monthsWorked, + 'vacation_days' => $vacationDays, + 'financial_count' => $unpaidFinancials->count(), + ]); + } + + /** + * Process settlement. + */ + public function processSettlement(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:employees,id', + 'settlement_type' => 'required|in:resignation,termination,end_of_contract', + 'settlement_date' => 'required|date', + 'last_working_date' => 'nullable|date', + 'notes' => 'nullable|string', + ]); + + $employee = Employee::findOrFail($validated['employee_id']); + $tenantId = $this->getTenantId(); + $settlementDate = \App\Helpers\CalendarHelper::parseDate($validated['settlement_date']); + + DB::transaction(function () use ($employee, $validated, $tenantId, $settlementDate) { + // Get all unpaid financials + $unpaidFinancials = EmployeeFinancial::where('employee_id', $employee->id) + ->where('is_paid', false) + ->get(); + + $totalPayable = $unpaidFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); + $totalDeductions = $unpaidFinancials->where('type', 'deduction')->sum('amount'); + $totalAdvances = $unpaidFinancials->where('type', 'advance')->sum('amount'); + $netPayable = $totalPayable - $totalDeductions - $totalAdvances; + + // Create settlement record + $settlement = \App\Models\EmployeeSettlement::create([ + 'tenant_id' => $tenantId, + 'employee_id' => $employee->id, + 'settlement_type' => $validated['settlement_type'], + 'settlement_date' => $settlementDate, + 'last_working_date' => !empty($validated['last_working_date']) + ? \App\Helpers\CalendarHelper::parseDate($validated['last_working_date']) + : null, + 'total_payable' => $totalPayable, + 'total_deductions' => $totalDeductions + $totalAdvances, + 'net_payable' => $netPayable, + 'currency_id' => $employee->currency_id, + 'notes' => $validated['notes'] ?? null, + 'processed_by' => Auth::id(), + 'processed_at' => now(), + ]); + + // Mark all unpaid as paid and link to settlement + $unpaidFinancials->each->update([ + 'is_paid' => true, + 'paid_at' => now(), + 'is_settlement' => true, + 'settlement_id' => $settlement->id, + ]); + + // Update employee status + $employee->update([ + 'status' => 'terminated', + 'termination_date' => $settlementDate, + ]); + }); + + return redirect()->route('payroll.index') + ->with('success', __('payroll.settlement_processed')); + } +} diff --git a/app/Http/Controllers/PettyCashController.php b/app/Http/Controllers/PettyCashController.php new file mode 100644 index 00000000..11723a9a --- /dev/null +++ b/app/Http/Controllers/PettyCashController.php @@ -0,0 +1,375 @@ +authorize('viewAny', PettyCash::class); + + $query = PettyCash::with(['project', 'currency', 'requestedBy', 'approvedBy']); + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + // Date range filter + if ($request->filled('from_date')) { + $query->where('request_date', '>=', \App\Helpers\CalendarHelper::parseDate($request->from_date)); + } + if ($request->filled('to_date')) { + $query->where('request_date', '<=', \App\Helpers\CalendarHelper::parseDate($request->to_date)); + } + + $query->orderBy('request_date', 'desc'); + + $pettyCashes = $query->paginate(20)->appends($request->query()); + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $statuses = ['pending', 'approved', 'rejected', 'settled']; + + // Summary statistics + $baseQuery = PettyCash::query(); + if ($request->filled('project_id')) { + $baseQuery->where('project_id', $request->project_id); + } + if ($request->filled('status')) { + $baseQuery->where('status', $request->status); + } + + $totalAmount = (clone $baseQuery)->where('status', '!=', 'rejected')->sum('amount'); + $totalExpenses = (clone $baseQuery)->where('status', '!=', 'rejected')->with('expenses') + ->get()->sum(function ($pc) { + return $pc->expenses->whereIn('status', ['approved', 'paid'])->sum('amount'); + }); + $totalRemaining = $totalAmount - $totalExpenses; + + return view('petty-cashes.index', compact('pettyCashes', 'projects', 'statuses', 'totalAmount', 'totalExpenses', 'totalRemaining')); + } + + /** + * Show the form for creating a new petty cash request. + */ + public function create() + { + $this->authorize('create', PettyCash::class); + + $pettyCash = null; + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('petty-cashes.form', compact('pettyCash', 'projects', 'currencies', 'baseCurrency')); + } + + /** + * Show the form for adding an expense to a petty cash. + */ + public function createExpense(PettyCash $pettyCash) + { + $this->authorize('update', $pettyCash); + + if ($pettyCash->status !== 'approved') { + return redirect()->route('petty-cashes.show', $pettyCash) + ->with('error', __('pettyCash.only_approved_can_add_expense')); + } + + $pettyCash->load(['project', 'currency']); + $currencies = Currency::where('is_active', true)->get(); + $categories = ['materials', 'labor', 'equipment', 'transport', 'services', 'food', 'accommodation', 'other']; + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('petty-cashes.expense-form', compact('pettyCash', 'currencies', 'categories', 'baseCurrency')); + } + + /** + * Store an expense linked to a petty cash. + */ + public function storeExpense(Request $request, PettyCash $pettyCash) + { + $this->authorize('update', $pettyCash); + + if ($pettyCash->status !== 'approved') { + return redirect()->route('petty-cashes.show', $pettyCash) + ->with('error', __('pettyCash.only_approved_can_add_expense')); + } + + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'required|exists:currencies,id', + 'category' => 'required|string', + 'expense_date' => 'required|date', + 'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + 'attachments' => 'nullable|array|max:5', + 'attachments.*' => 'file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + ]); + + $validated['tenant_id'] = $pettyCash->tenant_id; + $validated['project_id'] = $pettyCash->project_id; + $validated['petty_cash_id'] = $pettyCash->id; + $validated['requested_by'] = Auth::id(); + $validated['status'] = 'pending'; + + // Parse Jalali date + if (!empty($validated['expense_date'])) { + $validated['expense_date'] = \App\Helpers\CalendarHelper::parseDate($validated['expense_date']); + } + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($validated['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $validated['original_amount'] = $validated['amount']; + $validated['amount'] = CurrencyHelper::convert( + (float) $validated['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload + if ($request->hasFile('receipt')) { + $validated['receipt_path'] = $request->file('receipt')->store('receipts', 'public'); + } + + $expense = \App\Models\Expense::create($validated); + + // Handle multiple file attachments + if ($request->hasFile('attachments')) { + $expense->attachFiles($request->file('attachments'), 'receipt'); + } + + // Update petty cash remaining + $pettyCash->updateRemaining(); + + return redirect() + ->route('petty-cashes.show', $pettyCash) + ->with('success', __('pettyCash.expense_added_successfully')); + } + + /** + * Store a newly created petty cash request. + */ + public function store(Request $request) + { + $this->authorize('create', PettyCash::class); + + $validated = $request->validate([ + 'project_id' => 'required|exists:projects,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'required|exists:currencies,id', + 'request_date' => 'required|date', + 'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + 'attachments' => 'nullable|array|max:5', + 'attachments.*' => 'file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + ]); + + $validated['tenant_id'] = $this->getTenantId(); + $validated['requested_by'] = Auth::id(); + $validated['status'] = 'pending'; + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($validated['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $validated['original_amount'] = $validated['amount']; + $validated['amount'] = CurrencyHelper::convert( + (float) $validated['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload (legacy) + if ($request->hasFile('receipt')) { + $validated['receipt_path'] = $request->file('receipt')->store('receipts/petty-cash', 'public'); + } + + $pettyCash = PettyCash::create($validated); + + // Set initial remaining = amount + $pettyCash->remaining = $pettyCash->amount; + $pettyCash->saveQuietly(); + + // Handle multiple file attachments + if ($request->hasFile('attachments')) { + $pettyCash->attachFiles($request->file('attachments'), 'receipt'); + } + + return redirect() + ->route('petty-cashes.show', $pettyCash) + ->with('success', __('pettyCash.created_successfully')); + } + + /** + * Display the specified petty cash request. + */ + public function show(PettyCash $pettyCash) + { + $this->authorize('view', $pettyCash); + + $pettyCash->load(['project', 'currency', 'requestedBy', 'approvedBy', 'expenses', 'attachments']); + + return view('petty-cashes.show', compact('pettyCash')); + } + + /** + * Show the form for editing the specified petty cash request. + */ + public function edit(PettyCash $pettyCash) + { + $this->authorize('update', $pettyCash); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('petty-cashes.form', compact('pettyCash', 'projects', 'currencies', 'baseCurrency')); + } + + /** + * Update the specified petty cash request. + */ + public function update(Request $request, PettyCash $pettyCash) + { + $this->authorize('update', $pettyCash); + + $validated = $request->validate([ + 'project_id' => 'required|exists:projects,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'required|exists:currencies,id', + 'request_date' => 'required|date', + 'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + 'attachments' => 'nullable|array|max:5', + 'attachments.*' => 'file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + ]); + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($validated['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $validated['original_amount'] = $validated['amount']; + $validated['amount'] = CurrencyHelper::convert( + (float) $validated['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload + if ($request->hasFile('receipt')) { + // Delete old receipt if exists + if ($pettyCash->receipt_path) { + \Illuminate\Support\Facades\Storage::disk('public')->delete($pettyCash->receipt_path); + } + $validated['receipt_path'] = $request->file('receipt')->store('receipts/petty-cash', 'public'); + } + + $pettyCash->update($validated); + + // Handle multiple file attachments + if ($request->hasFile('attachments')) { + $pettyCash->attachFiles($request->file('attachments'), 'receipt'); + } + + return redirect() + ->route('petty-cashes.show', $pettyCash) + ->with('success', __('pettyCash.updated_successfully')); + } + + /** + * Remove the specified petty cash request. + */ + public function destroy(PettyCash $pettyCash) + { + $this->authorize('delete', $pettyCash); + + $pettyCash->delete(); + + return redirect() + ->route('petty-cashes.index') + ->with('success', __('pettyCash.deleted_successfully')); + } + + /** + * Approve a petty cash request. + */ + public function approve(PettyCash $pettyCash) + { + $this->authorize('approve', $pettyCash); + + $pettyCash->update([ + 'status' => 'approved', + 'approved_by' => Auth::id(), + 'approval_date' => now(), + ]); + + return response()->json([ + 'success' => true, + 'message' => __('pettyCash.approved_successfully'), + ]); + } + + /** + * Reject a petty cash request. + */ + public function reject(PettyCash $pettyCash) + { + $this->authorize('approve', $pettyCash); + + $pettyCash->update([ + 'status' => 'rejected', + 'approved_by' => Auth::id(), + 'approval_date' => now(), + ]); + + return response()->json([ + 'success' => true, + 'message' => __('pettyCash.rejected_successfully'), + ]); + } + + /** + * Settle a petty cash request (mark as settled after usage). + */ + public function settle(PettyCash $pettyCash) + { + $this->authorize('approve', $pettyCash); + + // Update remaining before settling + $pettyCash->updateRemaining(); + + $pettyCash->update([ + 'status' => 'settled', + ]); + + return response()->json([ + 'success' => true, + 'message' => __('pettyCash.settled_successfully'), + 'remaining' => $pettyCash->remaining, + ]); + } +} diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php new file mode 100644 index 00000000..21c66ad0 --- /dev/null +++ b/app/Http/Controllers/ProjectController.php @@ -0,0 +1,269 @@ +authorize('viewAny', Project::class); + + $query = Project::with(['manager', 'defaultCurrency', 'tasks']); + + // Filter by status + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + // Filter by search + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('code', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%"); + }); + } + + // Sort + $sortField = $request->get('sort', 'created_at'); + $sortDirection = $request->get('direction', 'desc'); + $allowedSorts = ['name', 'code', 'status', 'progress', 'budget', 'start_date', 'created_at']; + if (!in_array($sortField, $allowedSorts)) { + $sortField = 'created_at'; + } + if (!in_array($sortDirection, ['asc', 'desc'])) { + $sortDirection = 'desc'; + } + $query->orderBy($sortField, $sortDirection); + + $projects = $query->paginate(12)->appends($request->query()); + + return view('projects.index', compact('projects')); + } + + /** + * Show the form for creating a new project. + */ + public function create() + { + $this->authorize('create', Project::class); + + $managers = User::where('tenant_id', $this->getTenantId())->where('is_active', true)->get(); + $currencies = Currency::where('is_active', true)->get(); + $statuses = ['planning', 'in_progress', 'on_hold', 'completed', 'cancelled']; + $project = null; + + return view('projects.form', compact('project', 'managers', 'currencies', 'statuses')); + } + + /** + * Store a newly created project in storage. + */ + public function store(StoreProjectRequest $request) + { + $this->authorize('create', Project::class); + + $data = $request->validated(); + $data['tenant_id'] = $this->getTenantId(); + + $project = Project::create($data); + + // Attach manager as project member + if (!empty($data['manager_id'])) { + $project->users()->attach($data['manager_id'], ['role' => 'manager']); + } + + return redirect() + ->route('projects.show', $project) + ->with('success', __('project.created_successfully')); + } + + /** + * Display the specified project. + */ + public function show(Project $project) + { + $this->authorize('view', $project); + + $project->load([ + 'manager', + 'defaultCurrency', + 'tasks.assignee', + 'tasks' => fn($q) => $q->orderBy('id')->orderBy('due_date'), + //'employees', + 'expenses.currency', + 'users', + ]); + + $taskStats = [ + 'total' => $project->tasks->count(), + 'pending' => $project->tasks->where('status', 'pending')->count() + $project->tasks->where('status', 'todo')->count(), + 'in_progress' => $project->tasks->where('status', 'in_progress')->count(), + 'review' => $project->tasks->where('status', 'review')->count(), + 'done' => $project->tasks->where('status', 'done')->count(), + ]; + + return view('projects.show', compact('project', 'taskStats')); + } + + /** + * Show the form for editing the specified project. + */ + public function edit(Project $project) + { + $this->authorize('update', $project); + + $managers = User::where('tenant_id', $this->getTenantId())->where('is_active', true)->get(); + $currencies = Currency::where('is_active', true)->get(); + $statuses = ['planning', 'in_progress', 'on_hold', 'completed', 'cancelled']; + + return view('projects.form', compact('project', 'managers', 'currencies', 'statuses')); + } + + /** + * Update the specified project in storage. + */ + public function update(StoreProjectRequest $request, Project $project) + { + $this->authorize('update', $project); + + $project->update($request->validated()); + + return redirect() + ->route('projects.show', $project) + ->with('success', __('project.updated_successfully')); + } + + /** + * Soft-delete the specified project. + */ + public function destroy(Project $project) + { + $this->authorize('delete', $project); + + $project->delete(); + + return redirect() + ->route('projects.index') + ->with('success', __('project.deleted_successfully')); + } + + /** + * Update project status via AJAX. + */ + public function updateStatus(Request $request, Project $project) + { + $this->authorize('update', $project); + + $request->validate([ + 'status' => 'required|in:planning,in_progress,on_hold,completed,cancelled', + ]); + + $oldStatus = $project->status; + $project->status = $request->status; + + if ($request->status === 'completed') { + $project->progress = 100; + } + + $project->save(); + + return response()->json([ + 'success' => true, + 'message' => __('project.status_updated'), + 'status' => $project->status, + 'status_label' => $project->status_label, + ]); + } + + /** + * Export projects to Excel. + */ + public function exportExcel(Request $request) + { + $this->authorize('viewAny', Project::class); + + $query = Project::with(['manager', 'defaultCurrency']); + + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + $projects = $query->get(); + + // Simple CSV export + $filename = 'projects_' . now()->format('Y-m-d_His') . '.csv'; + $headers = [ + 'Content-Type' => 'text/csv; charset=UTF-8', + 'Content-Disposition' => "attachment; filename=\"{$filename}\"", + ]; + + $callback = function () use ($projects) { + $file = fopen('php://output', 'w'); + // BOM for UTF-8 + fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + fputcsv($file, [ + __('project.code'), + __('project.name'), + __('project.status'), + __('project.progress'), + __('project.budget'), + __('project.manager'), + __('project.start_date'), + __('project.end_date'), + ]); + + foreach ($projects as $project) { + fputcsv($file, [ + $project->code, + $project->name, + $project->status_label, + $project->progress . '%', + $project->budget, + $project->manager?->name, + $project->start_date?->format('Y-m-d'), + $project->end_date?->format('Y-m-d'), + ]); + } + + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } + + /** + * Export projects to PDF. + */ + public function exportPdf(Request $request) + { + $this->authorize('viewAny', Project::class); + + $query = Project::with(['manager', 'defaultCurrency', 'tasks']); + + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + $projects = $query->get(); + + $pdf = Pdf::loadView('projects.pdf', compact('projects')); + $filename = 'projects_' . now()->format('Y-m-d_His') . '.pdf'; + + return $pdf->download($filename); + } +} diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php new file mode 100644 index 00000000..7063121b --- /dev/null +++ b/app/Http/Controllers/ReportController.php @@ -0,0 +1,201 @@ +filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('from_date')) { + $query->where('expense_date', '>=', $request->from_date); + } + + if ($request->filled('to_date')) { + $query->where('expense_date', '<=', $request->to_date); + } + + $expenses = $query->orderBy('expense_date', 'desc')->get(); + + $totalAmount = $expenses->sum('amount'); + $approvedAmount = $expenses->where('status', 'approved')->sum('amount'); + $pendingAmount = $expenses->where('status', 'pending')->sum('amount'); + + $byProject = $expenses->groupBy('project_id')->map(function ($group) { + return [ + 'project' => $group->first()?->project?->name ?? '-', + 'total' => $group->sum('amount'), + 'count' => $group->count(), + ]; + }); + + $byCategory = $expenses->groupBy('category')->map(function ($group) { + return [ + 'category' => $group->first()->category ?? '-', + 'total' => $group->sum('amount'), + 'count' => $group->count(), + ]; + }); + + $projects = Project::all(); + + return view('reports.financial', compact('expenses', 'totalAmount', 'approvedAmount', 'pendingAmount', 'byProject', 'byCategory', 'projects')); + } + + /** + * Export financial report to Excel. + */ + public function financialExportExcel(Request $request) + { + // Stub - will implement with Excel export library + return back()->with('info', __('reports.export_coming_soon')); + } + + /** + * Export financial report to PDF. + */ + public function financialExportPdf(Request $request) + { + // Stub - will implement with PDF library + return back()->with('info', __('reports.export_coming_soon')); + } + + /** + * Display employee work-log report. + */ + public function employeeWorklog(Request $request) + { + $query = WorkLog::with(['employee', 'project', 'task']); + + if ($request->filled('employee_id')) { + $query->where('employee_id', $request->employee_id); + } + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('from_date')) { + $query->where('date', '>=', $request->from_date); + } + + if ($request->filled('to_date')) { + $query->where('date', '<=', $request->to_date); + } + + $workLogs = $query->orderBy('date', 'desc')->paginate(30)->appends($request->query()); + $employees = Employee::where('status', 'active')->get(); + $projects = Project::all(); + + return view('reports.employee-worklog', compact('workLogs', 'employees', 'projects')); + } + + /** + * Export employee work-log report to Excel. + */ + public function employeeWorklogExportExcel(Request $request) + { + return back()->with('info', __('reports.export_coming_soon')); + } + + /** + * Display inventory report. + */ + public function inventory(Request $request) + { + $query = InventoryTransaction::with(['item', 'warehouse', 'project']); + + if ($request->filled('warehouse_id')) { + $query->where('warehouse_id', $request->warehouse_id); + } + + if ($request->filled('type')) { + $query->where('type', $request->type); + } + + if ($request->filled('from_date')) { + $query->where('created_at', '>=', $request->from_date); + } + + if ($request->filled('to_date')) { + $query->where('created_at', '<=', $request->to_date); + } + + $transactions = $query->orderBy('created_at', 'desc')->paginate(30)->appends($request->query()); + + $items = Item::orderBy('name')->get(); + $warehouses = \App\Models\Warehouse::orderBy('name')->get(); + + return view('reports.inventory', compact('transactions', 'items', 'warehouses')); + } + + /** + * Export inventory report to Excel. + */ + public function inventoryExportExcel(Request $request) + { + return back()->with('info', __('reports.export_coming_soon')); + } + + /** + * Display payroll report. + */ + public function payroll(Request $request) + { + $query = EmployeeFinancial::with(['employee', 'currency']); + + if ($request->filled('month')) { + $query->where('month', $request->month); + } + + if ($request->filled('year')) { + $query->where('year', $request->year); + } + + if ($request->filled('type')) { + $query->byType($request->type); + } + + $financials = $query->orderBy('effective_date', 'desc')->paginate(30)->appends($request->query()); + + $employees = Employee::where('status', 'active')->get(); + $months = range(1, 12); + $currentYear = now()->year; + $years = range($currentYear - 2, $currentYear + 1); + + return view('reports.payroll', compact('financials', 'employees', 'months', 'years')); + } + + /** + * Export payroll report to Excel. + */ + public function payrollExportExcel(Request $request) + { + return back()->with('info', __('reports.export_coming_soon')); + } +} diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php new file mode 100644 index 00000000..48b81f34 --- /dev/null +++ b/app/Http/Controllers/SettingsController.php @@ -0,0 +1,178 @@ +getTenantId()); + $users = User::where('tenant_id', $this->getTenantId())->get(); + $currencies = Currency::all(); + + return view('settings.index', compact('tenant', 'users', 'currencies')); + } + + /** + * Update organization settings. + */ + public function updateOrganization(Request $request) + { + $tenant = Tenant::find($this->getTenantId()); + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'default_locale' => 'required|in:fa,en,ar', + 'default_calendar' => 'required|in:jalali,gregorian', + 'logo' => 'nullable|image|max:2048', + ]); + + if ($request->hasFile('logo')) { + // Delete old logo + if ($tenant->logo) { + Storage::disk('public')->delete($tenant->logo); + } + $validated['logo'] = $request->file('logo')->store('logos', 'public'); + } + + $tenant->update($validated); + + return redirect() + ->route('settings.index') + ->with('success', __('settings.organization_updated')); + } + + /** + * Store a new user. + */ + public function storeUser(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email', + 'password' => 'required|string|min:8|confirmed', + 'roles' => 'nullable|array', + ]); + + $validated['tenant_id'] = $this->getTenantId(); + $validated['password'] = Hash::make($validated['password']); + $validated['is_active'] = true; + + $roles = $validated['roles'] ?? []; + unset($validated['roles']); + + $user = User::create($validated); + + if (!empty($roles)) { + $user->assignRole($roles); + } + + return redirect() + ->route('settings.index') + ->with('success', __('settings.user_created')); + } + + /** + * Update a user. + */ + public function updateUser(Request $request, User $user) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email,' . $user->id, + 'password' => 'nullable|string|min:8|confirmed', + 'roles' => 'nullable|array', + ]); + + if (!empty($validated['password'])) { + $validated['password'] = Hash::make($validated['password']); + } else { + unset($validated['password']); + } + + $roles = $validated['roles'] ?? []; + unset($validated['roles']); + + $user->update($validated); + + $user->syncRoles($roles); + + return redirect() + ->route('settings.index') + ->with('success', __('settings.user_updated')); + } + + /** + * Toggle user active status. + */ + public function toggleUser(User $user) + { + $user->update(['is_active' => !$user->is_active]); + + return redirect() + ->route('settings.index') + ->with('success', __('settings.user_status_updated')); + } + + /** + * Store a new currency. + */ + public function storeCurrency(Request $request) + { + $validated = $request->validate([ + 'code' => 'required|string|max:10|unique:currencies,code', + 'name' => 'required|string|max:100', + 'symbol' => 'required|string|max:10', + 'decimal_places' => 'required|integer|min:0|max:6', + 'exchange_rate_to_usd' => 'required|numeric|min:0', + ]); + + Currency::create($validated); + + return redirect() + ->route('settings.index') + ->with('success', __('settings.currency_created')); + } + + /** + * Update a currency. + */ + public function updateCurrency(Request $request, Currency $currency) + { + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'symbol' => 'required|string|max:10', + 'decimal_places' => 'required|integer|min:0|max:6', + 'exchange_rate_to_usd' => 'required|numeric|min:0', + ]); + + $currency->update($validated); + + return redirect() + ->route('settings.index') + ->with('success', __('settings.currency_updated')); + } + + /** + * Toggle currency active status. + */ + public function toggleCurrency(Currency $currency) + { + $currency->update(['is_active' => !$currency->is_active]); + + return redirect() + ->route('settings.index') + ->with('success', __('settings.currency_status_updated')); + } +} diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php new file mode 100644 index 00000000..9d40f171 --- /dev/null +++ b/app/Http/Controllers/TaskController.php @@ -0,0 +1,362 @@ +authorize('viewAny', Task::class); + + $query = Task::with(['project', 'assignee', 'reporter']); + + // Filter by project + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + // Filter by status + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + // Filter by priority + if ($request->filled('priority')) { + $query->byPriority($request->priority); + } + + // Filter by assignee + if ($request->filled('assignee_id')) { + $query->where('assignee_id', $request->assignee_id); + } + + // Only top-level tasks by default + $query->topLevel(); + + $query->orderBy('order')->orderBy('due_date', 'asc'); + + $tasks = $query->paginate(20)->appends($request->query()); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $users = User::where('tenant_id', $this->getTenantId())->where('is_active', true)->get(); + $statuses = ['pending', 'in_progress', 'review', 'done', 'cancelled']; + $priorities = ['low', 'medium', 'high', 'urgent']; + + return view('tasks.index', compact('tasks', 'projects', 'users', 'statuses', 'priorities')); + } + + /** + * Show the form for creating a new task. + */ + public function create(Request $request) + { + $this->authorize('create', Task::class); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $users = User::where('tenant_id', $this->getTenantId())->where('is_active', true)->get(); + $statuses = ['pending', 'in_progress', 'review', 'done', 'cancelled']; + $priorities = ['low', 'medium', 'high', 'urgent']; + + $preselectedProject = $request->get('project_id'); + + return view('tasks.form', compact('projects', 'users', 'statuses', 'priorities', 'preselectedProject')); + } + + /** + * Store a newly created task in storage. + */ + public function store(StoreTaskRequest $request) + { + $this->authorize('create', Task::class); + + $data = $request->validated(); + $data['tenant_id'] = $this->getTenantId(); + $data['reporter_id'] = Auth::id(); + + // Convert jalali dates if needed + if (!empty($data['due_date'])) { + $data['due_date'] = \App\Helpers\CalendarHelper::parseDate($data['due_date']); + } + if (!empty($data['start_date'])) { + $data['start_date'] = \App\Helpers\CalendarHelper::parseDate($data['start_date']); + } + + $task = Task::create($data); + + // Log the creation + TaskLog::create([ + 'task_id' => $task->id, + 'user_id' => Auth::id(), + 'field_changed' => 'created', + 'old_value' => null, + 'new_value' => ['status' => $task->status], + 'comment' => __('task.log_created'), + ]); + + return redirect() + ->route('tasks.show', $task) + ->with('success', __('task.created_successfully')); + } + + /** + * Display the specified task with logs. + */ + public function show(Task $task) + { + $this->authorize('view', $task); + + $task->load([ + 'project', + 'assignee', + 'reporter', + 'logs.user', + 'children.assignee', + 'workLogs.employee', + ]); + + return view('tasks.show', compact('task')); + } + + /** + * Show the form for editing the specified task. + */ + public function edit(Task $task) + { + $this->authorize('update', $task); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $users = User::where('tenant_id', $this->getTenantId())->where('is_active', true)->get(); + $statuses = ['pending', 'in_progress', 'review', 'done', 'cancelled']; + $priorities = ['low', 'medium', 'high', 'urgent']; + + return view('tasks.form', compact('task', 'projects', 'users', 'statuses', 'priorities')); + } + + /** + * Update the specified task in storage. + */ + public function update(StoreTaskRequest $request, Task $task) + { + $this->authorize('update', $task); + + $data = $request->validated(); + + // Convert jalali dates if needed + if (!empty($data['due_date'])) { + $data['due_date'] = \App\Helpers\CalendarHelper::parseDate($data['due_date']); + } + if (!empty($data['start_date'])) { + $data['start_date'] = \App\Helpers\CalendarHelper::parseDate($data['start_date']); + } + + // Track changes for TaskLog + $changedFields = []; + $watchFields = ['status', 'priority', 'assignee_id', 'title', 'due_date', 'project_id']; + + foreach ($watchFields as $field) { + if (isset($data[$field]) && $task->$field != $data[$field]) { + $changedFields[$field] = [ + 'old' => $task->$field, + 'new' => $data[$field], + ]; + } + } + + $task->update($data); + + // Create TaskLog entries for changed fields + foreach ($changedFields as $field => $changes) { + TaskLog::create([ + 'task_id' => $task->id, + 'user_id' => Auth::id(), + 'field_changed' => $field, + 'old_value' => $changes['old'], + 'new_value' => $changes['new'], + ]); + } + + return redirect() + ->route('tasks.show', $task) + ->with('success', __('task.updated_successfully')); + } + + /** + * Remove the specified task from storage. + */ + public function destroy(Task $task) + { + $this->authorize('delete', $task); + + $task->delete(); + + return redirect() + ->route('tasks.index') + ->with('success', __('task.deleted_successfully')); + } + + /** + * Update task status via AJAX. + */ + public function updateStatus(Request $request, Task $task) + { + $this->authorize('updateStatus', $task); + + $request->validate([ + 'status' => 'required|in:pending,in_progress,review,done,cancelled', + ]); + + $oldStatus = $task->status; + $task->status = $request->status; + + if ($request->status === 'done') { + $task->completed_at = now(); + } elseif ($oldStatus === 'done' && $request->status !== 'done') { + $task->completed_at = null; + } + + $task->save(); + + // Log the status change + TaskLog::create([ + 'task_id' => $task->id, + 'user_id' => Auth::id(), + 'field_changed' => 'status', + 'old_value' => $oldStatus, + 'new_value' => $request->status, + ]); + + return response()->json([ + 'success' => true, + 'message' => __('task.status_updated'), + 'status' => $task->status, + 'status_label' => $task->status_label, + ]); + } + + /** + * Display tasks in Kanban board view. + */ + public function kanban(Request $request) + { + $this->authorize('viewAny', Task::class); + + $query = Task::with(['project', 'assignee']); + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + $query->topLevel(); + + $allTasks = $query->get(); + + $kanbanColumns = [ + 'pending' => $allTasks->where('status', 'pending'), + 'in_progress' => $allTasks->where('status', 'in_progress'), + 'review' => $allTasks->where('status', 'review'), + 'done' => $allTasks->where('status', 'done'), + ]; + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $selectedProject = $request->get('project_id'); + + return view('tasks.kanban', compact('kanbanColumns', 'projects', 'selectedProject')); + } + + /** + * Display tasks in Gantt chart view. + */ + public function gantt(Request $request, ?Task $task = null) +{ + $this->authorize('viewAny', Task::class); + + $query = Task::with(['project', 'assignee']); + + if ($task && $task->exists) { + $query->where('project_id', $task->project_id); + } + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + $tasks = $query->get(); + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + + // Group tasks by project for virtual project rows + $ganttData = []; + $projectIdCounter = 10000; + + $grouped = $tasks->groupBy('project_id'); + + foreach ($grouped as $projectId => $projectTasks) { + $project = $projectTasks->first()?->project; + if (!$project) continue; + + // Calculate project date range from tasks or use project dates + $minStart = $projectTasks->filter(fn($t) => $t->start_date)->min('start_date') + ?? $project->start_date + ?? now(); + $maxDue = $projectTasks->filter(fn($t) => $t->due_date)->max('due_date') + ?? $project->end_date + ?? now()->addDays(30); + + // Add virtual project row + $ganttData[] = [ + 'id' => $projectIdCounter + $projectId, + 'text' => $project->name, + 'start_date' => \Carbon\Carbon::parse($minStart)->format('d-m-Y'), + 'duration' => max(1, \Carbon\Carbon::parse($minStart)->diffInDays(\Carbon\Carbon::parse($maxDue))), + 'progress' => 0, + 'open' => true, + 'type' => 'project', + 'project_id' => $projectId, + ]; + + // Add task rows — handle missing dates + foreach ($projectTasks as $t) { + $startDate = $t->start_date + ? \Carbon\Carbon::parse($t->start_date) + : \Carbon\Carbon::parse($minStart); + $dueDate = $t->due_date + ? \Carbon\Carbon::parse($t->due_date) + : $startDate->copy()->addDays(7); + $duration = max(1, $startDate->diffInDays($dueDate)); + + $statusColor = match($t->status) { + 'done' => '#10b981', + 'in_progress' => '#3b82f6', + 'review' => '#f59e0b', + 'cancelled' => '#ef4444', + default => '#14b8a6', + }; + + $ganttData[] = [ + 'id' => $t->id, + 'text' => $t->title, + 'start_date' => $startDate->format('d-m-Y'), + 'duration' => $duration, + 'progress' => (float) ($t->progress ?? 0) / 100, + 'parent' => $projectIdCounter + $projectId, + 'color' => $statusColor, + 'project_id' => $projectId, + 'status' => $t->status, + 'assignee' => $t->assignee?->name ?? '', + ]; + } + } + + return view('tasks.gantt', compact('ganttData', 'projects')); + } +} diff --git a/app/Http/Controllers/WarehouseController.php b/app/Http/Controllers/WarehouseController.php new file mode 100644 index 00000000..ad858c72 --- /dev/null +++ b/app/Http/Controllers/WarehouseController.php @@ -0,0 +1,117 @@ +tenant_id; + $query = Warehouse::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->with(['project']); + + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('location', 'like', "%{$search}%"); + }); + } + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('is_active')) { + $query->where('is_active', $request->boolean('is_active')); + } + + $warehouses = $query->orderBy('name')->paginate(20)->appends($request->query()); + $projects = Project::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->whereIn('status', ['planning', 'in_progress'])->get(); + + return view('warehouses.index', compact('warehouses', 'projects')); + } + + public function create() + { + $tenantId = session('tenant_id') ?? Auth::user()?->tenant_id; + $projects = Project::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->whereIn('status', ['planning', 'in_progress'])->get(); + + return view('warehouses.form', compact('projects')); + } + + public function store(Request $request) + { + $tenantId = session('tenant_id') ?? Auth::user()?->tenant_id; + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'location' => 'nullable|string|max:255', + 'project_id' => 'nullable|exists:projects,id', + 'is_active' => 'boolean', + ]); + + $validated['tenant_id'] = $tenantId; + $validated['is_active'] = $request->has('is_active'); + + Warehouse::create($validated); + + return redirect() + ->route('warehouses.index') + ->with('success', __('warehouse.created_successfully')); + } + + public function show(Warehouse $warehouse) + { + $warehouse->load(['project']); + + return view('warehouses.show', compact('warehouse')); + } + + public function edit(Warehouse $warehouse) + { + $tenantId = session('tenant_id') ?? Auth::user()?->tenant_id; + $projects = Project::when($tenantId, function($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + })->whereIn('status', ['planning', 'in_progress'])->get(); + + return view('warehouses.form', compact('warehouse', 'projects')); + } + + public function update(Request $request, Warehouse $warehouse) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'location' => 'nullable|string|max:255', + 'project_id' => 'nullable|exists:projects,id', + 'is_active' => 'boolean', + ]); + + $validated['is_active'] = $request->has('is_active'); + + $warehouse->update($validated); + + return redirect() + ->route('warehouses.index') + ->with('success', __('warehouse.updated_successfully')); + } + + public function destroy(Warehouse $warehouse) + { + $warehouse->delete(); + + return redirect() + ->route('warehouses.index') + ->with('success', __('warehouse.deleted_successfully')); + } +} \ No newline at end of file diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 00000000..e2fb26b7 --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,67 @@ + [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\SetTenant::class, + \App\Http\Middleware\SetLocale::class, + ], + + 'api' => [ + \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\SetTenant::class, + \App\Http\Middleware\SetLocale::class, + ], + ]; + + protected $middlewareAliases = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, + 'signed' => \App\Http\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class, + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + 'tenant.scope' => \App\Http\Middleware\TenantScope::class, + 'audit.log' => \App\Http\Middleware\AuditLogMiddleware::class, + ]; + + 'web' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToclass, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\SetTenant::class, + \App\Http\Middleware\SetLocale::class, + \App\Http\Middleware\ContentSecurityPolicy::class, + ], +} diff --git a/app/Http/Middleware/AuditLogMiddleware.php b/app/Http/Middleware/AuditLogMiddleware.php new file mode 100644 index 00000000..f95e54e2 --- /dev/null +++ b/app/Http/Middleware/AuditLogMiddleware.php @@ -0,0 +1,187 @@ +method(); + $isMutating = in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE']); + + // For PUT/PATCH/DELETE, capture the current state before the request runs + $oldValues = null; + $modelType = null; + $modelId = null; + + if ($isMutating && in_array($method, ['PUT', 'PATCH', 'DELETE'])) { + $resolvedModel = $this->resolveRouteModel($request); + + if ($resolvedModel) { + $modelType = get_class($resolvedModel); + $modelId = $resolvedModel->getKey(); + + if ($method !== 'POST') { + $oldValues = $resolvedModel->toArray(); + } + } + } + + // Execute the request + $response = $next($request); + + // Only log if the request was successful (2xx status) + if ($isMutating && $response->getStatusCode() >= 200 && $response->getStatusCode() < 300) { + $action = $this->determineAction($method, $request); + $newValues = null; + + // For POST/PUT/PATCH, capture new values from the model + if (in_array($method, ['POST', 'PUT', 'PATCH'])) { + $resolvedModel = $this->resolveRouteModel($request); + + if ($resolvedModel) { + $modelType = get_class($resolvedModel); + $modelId = $resolvedModel->getKey(); + $newValues = $resolvedModel->toArray(); + } elseif ($method === 'POST') { + // For POST with resource creation, try to get the model from the response redirect + $createdModel = $this->extractCreatedModel($request, $response); + if ($createdModel) { + $modelType = get_class($createdModel); + $modelId = $createdModel->getKey(); + $newValues = $createdModel->toArray(); + } + } + } + + // Filter out sensitive fields from values + $sensitiveFields = ['password', 'password_hash', 'remember_token', 'national_code', 'bank_account', 'base_salary']; + $oldValues = $this->filterSensitiveFields($oldValues, $sensitiveFields); + $newValues = $this->filterSensitiveFields($newValues, $sensitiveFields); + + try { + AuditLog::create([ + 'user_id' => Auth::id(), + 'tenant_id' => session('tenant_id'), + 'model_type' => $modelType, + 'model_id' => $modelId, + 'action' => $action, + 'old_values' => $oldValues, + 'new_values' => $newValues, + 'ip_address' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'url' => $request->fullUrl(), + 'method' => $method, + ]); + } catch (\Throwable $e) { + // Silently fail — audit logging should never break the request + logger()->error('Audit log failed: ' . $e->getMessage()); + } + } + + return $response; + } + + /** + * Resolve the Eloquent model from the current route. + */ + protected function resolveRouteModel(Request $request): ?\Illuminate\Database\Eloquent\Model + { + $route = $request->route(); + + if (!$route) { + return null; + } + + foreach ($route->parameters() as $parameter) { + if ($parameter instanceof \Illuminate\Database\Eloquent\Model) { + return $parameter; + } + } + + return null; + } + + /** + * Determine the action label based on HTTP method. + */ + protected function determineAction(string $method, Request $request): string + { + // Check if the route name suggests an approve/reject action + $routeName = $request->route()?->getName() ?? ''; + + if (str_contains($routeName, 'approve')) { + return 'approve'; + } + if (str_contains($routeName, 'reject')) { + return 'reject'; + } + if (str_contains($routeName, 'settle')) { + return 'settle'; + } + if (str_contains($routeName, 'status')) { + return 'status_change'; + } + + return match ($method) { + 'POST' => 'create', + 'PUT', 'PATCH' => 'update', + 'DELETE' => 'delete', + default => strtolower($method), + }; + } + + /** + * Try to extract a newly created model from a redirect response. + */ + protected function extractCreatedModel(Request $request, $response): ?\Illuminate\Database\Eloquent\Model + { + // If the controller set a 'created_model' on the request, use it + if ($request->attributes->has('_audit_created_model')) { + return $request->attributes->get('_audit_created_model'); + } + + // If the response redirects to a show route, try to extract model from redirect target + if ($response instanceof \Illuminate\Http\RedirectResponse) { + $targetUrl = $response->getTargetUrl(); + $route = app('router')->getRoutes()->match( + \Illuminate\Http\Request::create($targetUrl) + ); + + foreach ($route->parameters() as $parameter) { + if ($parameter instanceof \Illuminate\Database\Eloquent\Model) { + return $parameter; + } + } + } + + return null; + } + + /** + * Filter out sensitive fields from an array of values. + */ + protected function filterSensitiveFields(?array $values, array $sensitiveFields): ?array + { + if ($values === null) { + return null; + } + + foreach ($sensitiveFields as $field) { + unset($values[$field]); + } + + return $values; + } +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 00000000..d4ef6447 --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,17 @@ +expectsJson() ? null : route('login'); + } +} diff --git a/app/Http/Middleware/ContentSecurityPolicy.php b/app/Http/Middleware/ContentSecurityPolicy.php new file mode 100644 index 00000000..95d51034 --- /dev/null +++ b/app/Http/Middleware/ContentSecurityPolicy.php @@ -0,0 +1,22 @@ +headers->set( + 'Content-Security-Policy', + "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.dhtmlx.com; style-src 'self' 'unsafe-inline' https://cdn.dhtmlx.com https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:;", + false + ); + + return $response; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 00000000..867695bd --- /dev/null +++ b/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/app/Http/Middleware/EnsureEmailIsVerified.php b/app/Http/Middleware/EnsureEmailIsVerified.php new file mode 100644 index 00000000..eafa2a60 --- /dev/null +++ b/app/Http/Middleware/EnsureEmailIsVerified.php @@ -0,0 +1,26 @@ +user() || + ($request->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && + ! $request->user()->hasVerifiedEmail())) { + return $request->expectsJson() + ? abort(403, 'Your email address is not verified.') + : redirect()->route('verification.notice'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 00000000..114ba6d2 --- /dev/null +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,27 @@ +guard($guard)->check()) { + return redirect(RouteServiceProvider::HOME); + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php new file mode 100644 index 00000000..0e22aab8 --- /dev/null +++ b/app/Http/Middleware/SetLocale.php @@ -0,0 +1,77 @@ + tenant preference > session > browser > config default. + */ + public function handle(Request $request, Closure $next): Response + { + $locale = null; + $calendar = null; + + // 1. Session preference (highest priority - user just changed it) + $locale = session('locale'); + $calendar = session('calendar'); + + // 2. Authenticated user preference (database stored preference) + if (!$locale && $request->user()) { + $locale = $request->user()->getLocale(); + $calendar = $request->user()->getCalendar(); + } + + // 3. Tenant preference + if (!$locale && session('tenant_id')) { + $tenant = \App\Models\Tenant::find(session('tenant_id')); + if ($tenant) { + $locale = $locale ?? $tenant->default_locale; + $calendar = $calendar ?? $tenant->default_calendar; + } + } + + // 4. Browser preference + if (!$locale) { + $browserLang = substr($request->getPreferredLanguage(), 0, 2); + $supportedLocales = config('projectra.supported_locales', ['fa', 'en']); + if (in_array($browserLang, $supportedLocales)) { + $locale = $browserLang; + } + } + + // 5. Config default (lowest priority) + if (!$locale) { + $locale = config('projectra.default_locale', 'fa'); + } + + if (!$calendar) { + $calendar = config('projectra.default_calendar', 'jalali'); + } + + // Set the application locale + App::setLocale($locale); + + // Store in session for subsequent requests + session(['locale' => $locale, 'calendar' => $calendar]); + + // Share locale, direction, and calendar with all views + $rtlLocales = config('projectra.rtl_locales', ['fa', 'ar', 'he']); + $direction = in_array($locale, $rtlLocales) ? 'rtl' : 'ltr'; + + view()->share([ + 'currentLocale' => $locale, + 'currentDirection' => $direction, + 'currentCalendar' => $calendar, + ]); + + return $next($request); + } +} diff --git a/app/Http/Middleware/SetTenant.php b/app/Http/Middleware/SetTenant.php new file mode 100644 index 00000000..751c5365 --- /dev/null +++ b/app/Http/Middleware/SetTenant.php @@ -0,0 +1,117 @@ +getHost(); + $parts = explode('.', $host); + + // Skip for the main domain (e.g., Vernova.test or www.Vernova.test) + $mainDomain = config('Vernova.main_domain', env('APP_DOMAIN')); + + if ($mainDomain && ($host === $mainDomain || $host === 'www.' . $mainDomain)) { + // Even on main domain, try to set tenant from authenticated user + $this->setTenantFromUser(); + return $next($request); + } + + // Try to resolve tenant from subdomain + if (count($parts) > 2 || (count($parts) === 2 && $parts[1] === $mainDomain)) { + $subdomain = $parts[0]; + + // Skip 'www' subdomain + if ($subdomain === 'www') { + $this->setTenantFromUser(); + return $next($request); + } + + $tenant = Tenant::where('slug', $subdomain) + ->where('is_active', true) + ->first(); + + if (!$tenant) { + abort(404, 'Tenant not found.'); + } + + session(['tenant_id' => $tenant->id]); + session(['tenant' => $tenant]); + + // Share tenant with all views + view()->share('currentTenant', $tenant); + + return $next($request); + } + + // Try to get tenant from session if already set + $tenantId = session('tenant_id'); + + if ($tenantId) { + $tenant = Tenant::find($tenantId); + + if ($tenant && $tenant->is_active) { + view()->share('currentTenant', $tenant); + return $next($request); + } + } + + // Try to resolve tenant from domain directly + $tenant = Tenant::where('domain', $host) + ->where('is_active', true) + ->first(); + + if ($tenant) { + session(['tenant_id' => $tenant->id]); + session(['tenant' => $tenant]); + view()->share('currentTenant', $tenant); + + return $next($request); + } + + // Fallback: try to set tenant from authenticated user + $this->setTenantFromUser(); + + return $next($request); + } + + /** + * Set tenant from the currently authenticated user. + * This is a fallback for localhost/XAMPP environments where + * subdomain-based tenant resolution doesn't work. + */ + protected function setTenantFromUser(): void + { + // Only if tenant_id is not already in session + if (session('tenant_id')) { + $tenant = Tenant::find(session('tenant_id')); + if ($tenant && $tenant->is_active) { + view()->share('currentTenant', $tenant); + return; + } + } + + $user = Auth::user(); + + if ($user && $user->tenant_id) { + $tenant = Tenant::find($user->tenant_id); + + if ($tenant && $tenant->is_active) { + session(['tenant_id' => $tenant->id]); + session(['tenant' => $tenant]); + view()->share('currentTenant', $tenant); + } + } + } +} diff --git a/app/Http/Middleware/TenantScope.php b/app/Http/Middleware/TenantScope.php new file mode 100644 index 00000000..a39d35fb --- /dev/null +++ b/app/Http/Middleware/TenantScope.php @@ -0,0 +1,61 @@ +instance('tenant_id', $tenantId); + } + + return $next($request); + } + + /** + * Run a callback without the tenant scope. + * Useful for super-admin operations that span all tenants. + */ + public static function withoutTenantScope(callable $callback): mixed + { + $previousTenantId = app('tenant_id'); + + // Clear the tenant context + app()->instance('tenant_id', null); + + try { + return $callback(); + } finally { + // Restore the previous tenant context + app()->instance('tenant_id', $previousTenantId); + } + } + + /** + * Get the current tenant ID. + */ + public static function getCurrentTenantId(): ?int + { + return app('tenant_id'); + } + + /** + * Check if a tenant scope is currently active. + */ + public static function hasTenantScope(): bool + { + return app('tenant_id') !== null; + } +} diff --git a/app/Http/Middleware/ValidateSignature.php b/app/Http/Middleware/ValidateSignature.php new file mode 100644 index 00000000..453b7d54 --- /dev/null +++ b/app/Http/Middleware/ValidateSignature.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 00000000..9e865217 --- /dev/null +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/app/Http/Requests/StoreEmployeeRequest.php b/app/Http/Requests/StoreEmployeeRequest.php new file mode 100644 index 00000000..059aa8db --- /dev/null +++ b/app/Http/Requests/StoreEmployeeRequest.php @@ -0,0 +1,58 @@ + 'required|string|max:100', + 'last_name' => 'required|string|max:100', + 'national_code' => 'nullable|string|max:20', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:255', + 'contract_type' => 'required|in:full_time,part_time,contractor,intern,consultant', + 'base_salary' => 'nullable|numeric|min:0', + 'currency_id' => 'nullable|exists:currencies,id', + 'bank_account' => 'nullable|string|max:50', + 'hire_date' => 'required|date', + 'termination_date' => 'nullable|date|after_or_equal:hire_date', + 'position' => 'nullable|string|max:100', + 'department' => 'nullable|string|max:100', + 'project_id' => 'nullable|exists:projects,id', + 'notes' => 'nullable|string', + 'status' => 'nullable|in:active,inactive,terminated,on_leave', + 'job_title' => 'nullable|string|max:100', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'first_name.required' => __('employee.validation_first_name_required'), + 'last_name.required' => __('employee.validation_last_name_required'), + 'contract_type.required' => __('employee.validation_contract_type_required'), + 'contract_type.in' => __('employee.validation_contract_type_invalid'), + 'hire_date.required' => __('employee.validation_hire_date_required'), + 'base_salary.numeric' => __('employee.validation_base_salary_numeric'), + 'termination_date.after_or_equal' => __('employee.validation_termination_date_after'), + ]; + } +} diff --git a/app/Http/Requests/StoreExpenseRequest.php b/app/Http/Requests/StoreExpenseRequest.php new file mode 100644 index 00000000..dde3852a --- /dev/null +++ b/app/Http/Requests/StoreExpenseRequest.php @@ -0,0 +1,54 @@ + 'required|exists:projects,id', + 'petty_cash_id' => 'nullable|exists:petty_cashes,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'required|exists:currencies,id', + 'status' => 'nullable|in:pending,approved,rejected,paid', + 'category' => 'required|in:materials,labor,equipment,transport,services,food,accommodation,other', + 'expense_date' => 'required|date', + 'invoice_number' => 'nullable|string|max:100', + 'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:5120', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'project_id.required' => __('expense.validation_project_required'), + 'amount.required' => __('expense.validation_amount_required'), + 'amount.numeric' => __('expense.validation_amount_numeric'), + 'currency_id.required' => __('expense.validation_currency_required'), + 'currency_id.exists' => __('expense.validation_currency_exists'), + 'expense_date.required' => __('expense.validation_date_required'), + 'expense_date.date' => __('expense.validation_date_invalid'), + 'category.required' => __('expense.validation_category_required'), + 'category.in' => __('expense.validation_category_invalid'), + ]; + } +} diff --git a/app/Http/Requests/StoreProjectRequest.php b/app/Http/Requests/StoreProjectRequest.php new file mode 100644 index 00000000..70c5e25c --- /dev/null +++ b/app/Http/Requests/StoreProjectRequest.php @@ -0,0 +1,51 @@ + 'required|string|max:255', + 'code' => 'required|string|max:50|unique:projects,code,' . $this->project?->id, + 'description' => 'nullable|string', + 'status' => 'nullable|in:planning,active,on_hold,completed,cancelled', + 'budget' => 'nullable|numeric|min:0', + 'default_currency_id' => 'nullable|exists:currencies,id', + 'manager_id' => 'nullable|exists:users,id', + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'location' => 'nullable|string|max:255', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'name.required' => __('project.validation_name_required'), + 'code.required' => __('project.validation_code_required'), + 'code.unique' => __('project.validation_code_unique'), + 'status.in' => __('project.validation_status_invalid'), + 'budget.numeric' => __('project.validation_budget_numeric'), + 'end_date.after_or_equal' => __('project.validation_end_date_after'), + 'manager_id.exists' => __('project.validation_manager_exists'), + ]; + } +} diff --git a/app/Http/Requests/StoreTaskRequest.php b/app/Http/Requests/StoreTaskRequest.php new file mode 100644 index 00000000..94bed113 --- /dev/null +++ b/app/Http/Requests/StoreTaskRequest.php @@ -0,0 +1,52 @@ + 'required|string|max:255', + 'project_id' => 'required|exists:projects,id', + 'description' => 'nullable|string', + 'status' => 'nullable|in:pending,in_progress,review,done,cancelled', + 'priority' => 'nullable|in:low,medium,high,urgent', + 'assignee_id' => 'nullable|exists:users,id', + 'parent_id' => 'nullable|exists:tasks,id', + 'start_date' => 'nullable|date', + 'due_date' => 'nullable|date', + 'estimated_hours' => 'nullable|integer|min:0', + 'actual_hours' => 'nullable|integer|min:0', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'title.required' => __('task.validation_title_required'), + 'project_id.required' => __('task.validation_project_required'), + 'project_id.exists' => __('task.validation_project_exists'), + 'status.in' => __('task.validation_status_invalid'), + 'priority.in' => __('task.validation_priority_invalid'), + 'due_date.date' => __('task.validation_due_date_invalid'), + 'assignee_id.exists' => __('task.validation_assignee_exists'), + ]; + } +} diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php new file mode 100644 index 00000000..e4079529 --- /dev/null +++ b/app/Models/Attachment.php @@ -0,0 +1,119 @@ + 'integer', + ]; + + /** + * Get the parent attachable model. + */ + public function attachable() + { + return $this->morphTo(); + } + + /** + * Get the user who uploaded the attachment. + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get the tenant. + */ + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the public URL for the file. + */ + public function getUrlAttribute(): string + { + return Storage::url($this->file_path); + } + + /** + * Get human-readable file size. + */ + public function getFormattedSizeAttribute(): string + { + $bytes = $this->file_size; + if ($bytes >= 1048576) { + return number_format($bytes / 1048576, 1) . ' MB'; + } + if ($bytes >= 1024) { + return number_format($bytes / 1024, 1) . ' KB'; + } + return $bytes . ' B'; + } + + /** + * Check if the file is an image. + */ + public function getIsImageAttribute(): bool + { + return str_starts_with($this->mime_type ?? '', 'image/'); + } + + /** + * Check if the file is a PDF. + */ + public function getIsPdfAttribute(): bool + { + return $this->mime_type === 'application/pdf'; + } + + /** + * Get the file icon based on mime type. + */ + public function getIconAttribute(): string + { + return match (true) { + $this->is_image => 'image', + $this->is_pdf => 'pdf', + str_contains($this->mime_type ?? '', 'word') || str_contains($this->mime_type ?? '', 'document') => 'doc', + str_contains($this->mime_type ?? '', 'sheet') || str_contains($this->mime_type ?? '', 'excel') => 'xls', + default => 'file', + }; + } + + /** + * Delete the file from storage when model is deleted. + */ + protected static function booted(): void + { + static::deleting(function (Attachment $attachment) { + if ($attachment->file_path && Storage::disk('public')->exists($attachment->file_path)) { + Storage::disk('public')->delete($attachment->file_path); + } + }); + } +} diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php new file mode 100644 index 00000000..9d6dec66 --- /dev/null +++ b/app/Models/AuditLog.php @@ -0,0 +1,49 @@ + 'array', + 'new_values' => 'array', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get the auditable model (polymorphic-like relationship using model_type/model_id). + */ + public function subject() + { + if ($this->model_type && class_exists($this->model_type)) { + return $this->morphTo(null, 'model_type', 'model_id'); + } + + return null; + } +} diff --git a/app/Models/Concerns/HasAttachments.php b/app/Models/Concerns/HasAttachments.php new file mode 100644 index 00000000..115d37b8 --- /dev/null +++ b/app/Models/Concerns/HasAttachments.php @@ -0,0 +1,135 @@ +morphMany(Attachment::class, 'attachable'); + } + + /** + * Get receipts only. + */ + public function receipts() + { + return $this->morphMany(Attachment::class, 'attachable') + ->where('category', 'receipt'); + } + + /** + * Get documents only. + */ + public function documents() + { + return $this->morphMany(Attachment::class, 'attachable') + ->where('category', 'document'); + } + + /** + * Upload and attach a file. + */ + public function attachFile( + UploadedFile $file, + string $category = 'receipt', + ?string $description = null, + ?string $folder = null + ): Attachment { + $folder = $folder ?? $this->getAttachmentFolder(); + $filePath = $file->store($folder, 'public'); + + return $this->attachments()->create([ + 'tenant_id' => session('tenant_id') ?? $this->tenant_id, + 'user_id' => Auth::id(), + 'file_name' => $file->getClientOriginalName(), + 'file_path' => $filePath, + 'mime_type' => $file->getMimeType(), + 'file_size' => $file->getSize(), + 'category' => $category, + 'description' => $description, + ]); + } + + /** + * Upload multiple files at once. + */ + public function attachFiles(array $files, string $category = 'receipt', ?string $folder = null): array + { + $attachments = []; + foreach ($files as $file) { + if ($file instanceof UploadedFile && $file->isValid()) { + $attachments[] = $this->attachFile($file, $category, null, $folder); + } + } + return $attachments; + } + + /** + * Detach (soft delete) an attachment by ID. + */ + public function detachFile(int $attachmentId): bool + { + $attachment = $this->attachments()->find($attachmentId); + + if ($attachment) { + $attachment->delete(); + return true; + } + + return false; + } + + /** + * Migrate legacy receipt_path to the attachment system. + */ + public function migrateLegacyReceipt(): ?Attachment + { + $receiptPath = $this->getOriginal('receipt_path') ?? $this->receipt_path ?? null; + + if (!$receiptPath) { + return null; + } + + // Check if already migrated + $exists = $this->attachments() + ->where('file_path', $receiptPath) + ->exists(); + + if ($exists) { + return null; + } + + if (!Storage::disk('public')->exists($receiptPath)) { + return null; + } + + return $this->attachments()->create([ + 'tenant_id' => $this->tenant_id, + 'user_id' => null, + 'file_name' => basename($receiptPath), + 'file_path' => $receiptPath, + 'mime_type' => Storage::disk('public')->mimeType($receiptPath), + 'file_size' => Storage::disk('public')->size($receiptPath), + 'category' => 'receipt', + 'description' => null, + ]); + } + + /** + * Get the default storage folder for this model's attachments. + */ + protected function getAttachmentFolder(): string + { + $className = strtolower(class_basename($this)); + return "attachments/{$className}"; + } +} diff --git a/app/Models/Currency.php b/app/Models/Currency.php new file mode 100644 index 00000000..53add68d --- /dev/null +++ b/app/Models/Currency.php @@ -0,0 +1,30 @@ + 'boolean', + 'decimal_places' => 'integer', + ]; + + public function tenants() + { + return $this->hasMany(Tenant::class, 'base_currency_id'); + } +} diff --git a/app/Models/Document.php b/app/Models/Document.php new file mode 100644 index 00000000..ba7a489f --- /dev/null +++ b/app/Models/Document.php @@ -0,0 +1,82 @@ +belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function uploader() + { + return $this->belongsTo(User::class, 'uploaded_by'); + } + + public function scopeSearch($query, $term) + { + return $query->where(function ($q) use ($term) { + $q->where('title', 'like', "%{$term}%") + ->orWhere('description', 'like', "%{$term}%"); + }); + } + + public function getFileSizeAttribute(): ?int + { + $path = storage_path('app/public/' . $this->file_path); + if (file_exists($path)) { + return filesize($path); + } + // Try the local disk + $path = storage_path('app/' . $this->file_path); + if (file_exists($path)) { + return filesize($path); + } + return null; + } + + public function getMimeTypeAttribute(): ?string + { + $path = storage_path('app/public/' . $this->file_path); + if (file_exists($path)) { + return mime_content_type($path); + } + $path = storage_path('app/' . $this->file_path); + if (file_exists($path)) { + return mime_content_type($path); + } + return null; + } + + public function getFileIconAttribute(): string + { + return match($this->file_type) { + 'pdf' => 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', + 'image' => 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', + 'word', 'excel', 'text' => 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', + default => 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z', + }; + } +} diff --git a/app/Models/Employee.php b/app/Models/Employee.php new file mode 100644 index 00000000..f720dd39 --- /dev/null +++ b/app/Models/Employee.php @@ -0,0 +1,92 @@ + 'date', + 'termination_date' => 'date', + 'national_code' => 'encrypted', + 'bank_account' => 'encrypted', + 'base_salary' => 'decimal:0', // ✅ جایگزین + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + public function workLogs() + { + return $this->hasMany(WorkLog::class); + } + + public function financials() + { + return $this->hasMany(EmployeeFinancial::class); + } + + public function expenses() + { + return $this->hasMany(Expense::class, 'employee_id'); + } + + public function getFullNameAttribute() + { + return $this->first_name . ' ' . $this->last_name; + } + + public function getContractTypeLabelAttribute() + { + return [ + 'full_time' => __('employee.contract_full_time'), + 'part_time' => __('employee.contract_part_time'), + 'contractor' => __('employee.contract_contractor'), + 'daily' => __('employee.contract_daily'), + ][$this->contract_type] ?? $this->contract_type; + } + + public function getStatusLabelAttribute() + { + return [ + 'active' => __('employee.active'), + 'inactive' => __('employee.inactive'), + 'terminated' => __('employee.terminated'), + ][$this->status] ?? $this->status; + } +} diff --git a/app/Models/EmployeeFinancial.php b/app/Models/EmployeeFinancial.php new file mode 100644 index 00000000..cd6bb72b --- /dev/null +++ b/app/Models/EmployeeFinancial.php @@ -0,0 +1,57 @@ + 'decimal:2', + 'original_amount' => 'decimal:2', + 'effective_date' => 'date', + 'is_paid' => 'boolean', + 'paid_at' => 'datetime', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function employee() + { + return $this->belongsTo(Employee::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + /** + * Scope: filter by type + */ + public function scopeByType($query, $type) + { + return $query->where('type', $type); + } +} diff --git a/app/Models/EmployeeSettlement.php b/app/Models/EmployeeSettlement.php new file mode 100644 index 00000000..33f721db --- /dev/null +++ b/app/Models/EmployeeSettlement.php @@ -0,0 +1,70 @@ + 'date', + 'last_working_date' => 'date', + 'processed_at' => 'datetime', + 'total_payable' => 'decimal:0', + 'total_deductions' => 'decimal:0', + 'net_payable' => 'decimal:0', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function employee() + { + return $this->belongsTo(Employee::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + public function processedBy() + { + return $this->belongsTo(User::class, 'processed_by'); + } + + public function financials() + { + return $this->hasMany(EmployeeFinancial::class, 'settlement_id'); + } + + public function getSettlementTypeLabelAttribute() + { + return match($this->settlement_type) { + 'resignation' => 'استعفا', + 'termination' => 'اخراج', + 'end_of_contract' => 'پایان قرارداد', + default => $this->settlement_type, + }; + } +} diff --git a/app/Models/Expense.php b/app/Models/Expense.php new file mode 100644 index 00000000..33d3976b --- /dev/null +++ b/app/Models/Expense.php @@ -0,0 +1,116 @@ + 'decimal:2', + 'amount_usd' => 'decimal:2', + 'original_amount' => 'decimal:2', + 'expense_date' => 'date', + 'approval_date' => 'datetime', + 'approved_at' => 'datetime', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function pettyCash() + { + return $this->belongsTo(PettyCash::class); + } + + public function employee() + { + return $this->belongsTo(Employee::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + public function requestedBy() + { + return $this->belongsTo(User::class, 'requested_by'); + } + + public function approvedBy() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Alias for approvedBy (backward compatibility). + */ + public function approver() + { + return $this->approvedBy(); + } + + /** + * Scope: filter by category. + */ + public function scopeByCategory($query, $category) + { + return $query->where('category', $category); + } + + /** + * Scope: filter by status. + */ + public function scopeByStatus($query, $status) + { + return $query->where('status', $status); + } + + /** + * Get the status label for display. + */ + public function getStatusLabelAttribute(): string + { + return [ + 'pending' => __('expense.pending'), + 'approved' => __('expense.approved'), + 'rejected' => __('expense.rejected'), + 'paid' => __('expense.paid'), + ][$this->status] ?? $this->status; + } +} diff --git a/app/Models/InventoryTransaction.php b/app/Models/InventoryTransaction.php new file mode 100644 index 00000000..4ba9809e --- /dev/null +++ b/app/Models/InventoryTransaction.php @@ -0,0 +1,75 @@ + 'decimal:2', + 'unit_price' => 'decimal:2', + 'total_price' => 'decimal:2', + 'total_price_usd' => 'decimal:2', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function item() + { + return $this->belongsTo(Item::class); + } + + public function warehouse() + { + return $this->belongsTo(Warehouse::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + public function createdBy() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Scope: filter by type + */ + public function scopeByType($query, $type) + { + return $query->where('type', $type); + } +} diff --git a/app/Models/Item.php b/app/Models/Item.php new file mode 100644 index 00000000..5982ed79 --- /dev/null +++ b/app/Models/Item.php @@ -0,0 +1,38 @@ + 'boolean', + 'min_stock' => 'decimal:2', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function inventoryTransactions() + { + return $this->hasMany(InventoryTransaction::class); + } +} diff --git a/app/Models/Letter.php b/app/Models/Letter.php new file mode 100644 index 00000000..c9a5915a --- /dev/null +++ b/app/Models/Letter.php @@ -0,0 +1,66 @@ + 'date', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function parentLetter() + { + return $this->belongsTo(Letter::class, 'parent_id'); + } + + public function replies() + { + return $this->hasMany(Letter::class, 'parent_id'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function attachments() + { + return $this->hasMany(LetterAttachment::class); + } + + public function createdBy() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/LetterAttachment.php b/app/Models/LetterAttachment.php new file mode 100644 index 00000000..4cddeeea --- /dev/null +++ b/app/Models/LetterAttachment.php @@ -0,0 +1,83 @@ + 'integer', + ]; + + public function letter() + { + return $this->belongsTo(Letter::class); + } + + /** + * Get the URL for the attachment. + */ + public function getUrlAttribute(): string + { + return Storage::disk('public')->url($this->file_path); + } + + /** + * Check if the attachment is an image. + */ + public function getIsImageAttribute(): bool + { + return str_starts_with($this->mime_type ?? '', 'image/'); + } + + /** + * Check if the attachment is a PDF. + */ + public function getIsPdfAttribute(): bool + { + return ($this->mime_type ?? '') === 'application/pdf'; + } + + /** + * Get the formatted file size. + */ + public function getFormattedSizeAttribute(): string + { + $bytes = $this->file_size ?? 0; + + if ($bytes >= 1048576) { + return round($bytes / 1048576, 1) . ' MB'; + } + + if ($bytes >= 1024) { + return round($bytes / 1024, 1) . ' KB'; + } + + return $bytes . ' B'; + } + + protected static function booted() + { + static::creating(function ($attachment) { + if (empty($attachment->created_at)) { + $attachment->created_at = now(); + } + }); + } +} diff --git a/app/Models/PayrollPeriod.php b/app/Models/PayrollPeriod.php new file mode 100644 index 00000000..813bdc78 --- /dev/null +++ b/app/Models/PayrollPeriod.php @@ -0,0 +1,58 @@ + 'datetime', + 'total_gross' => 'decimal:0', + 'total_deductions' => 'decimal:0', + 'total_net' => 'decimal:0', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function confirmedBy() + { + return $this->belongsTo(User::class, 'confirmed_by'); + } + + public function createdBy() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function getPeriodNameAttribute() + { + $persianMonths = [ + 1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد', + 4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور', + 7 => 'مهر', 8 => 'آبان', 9 => 'آذر', + 10 => 'دی', 11 => 'بهمن', 12 => 'اسفند' + ]; + return ($persianMonths[$this->month] ?? $this->month) . ' ' . $this->year; + } +} diff --git a/app/Models/Permission.php b/app/Models/Permission.php new file mode 100644 index 00000000..f591fc1a --- /dev/null +++ b/app/Models/Permission.php @@ -0,0 +1,15 @@ + 'decimal:2', + 'original_amount' => 'decimal:2', + 'amount_usd' => 'decimal:2', + 'remaining' => 'decimal:2', + 'request_date' => 'date', + 'approval_date' => 'datetime', + 'approved_at' => 'datetime', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function currency() + { + return $this->belongsTo(Currency::class); + } + + public function requestedBy() + { + return $this->belongsTo(User::class, 'requested_by'); + } + + public function approvedBy() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function expenses() + { + return $this->hasMany(Expense::class); + } + + public function scopeByStatus($query, $status) + { + return $query->where('status', $status); + } + + public function getStatusLabelAttribute() + { + return [ + 'pending' => __('pettyCash.pending'), + 'approved' => __('pettyCash.approved'), + 'rejected' => __('pettyCash.rejected'), + 'settled' => __('pettyCash.settled'), + ][$this->status] ?? $this->status; + } + + /** + * Calculate remaining amount based on approved expenses. + */ + public function calculateRemaining(): float + { + $totalExpenses = $this->expenses() + ->whereIn('status', ['approved', 'paid']) + ->sum('amount'); + + return (float) $this->amount - (float) $totalExpenses; + } + + /** + * Update the remaining amount. + */ + public function updateRemaining(): void + { + $this->remaining = $this->calculateRemaining(); + $this->saveQuietly(); + } + + /** + * Get total approved expenses. + */ + public function getTotalExpensesAttribute(): float + { + return (float) $this->expenses() + ->whereIn('status', ['approved', 'paid']) + ->sum('amount'); + } + + /** + * Get utilization percentage. + */ + public function getUtilizationAttribute(): float + { + if ($this->amount <= 0) return 0; + return min(100, round(($this->total_expenses / (float) $this->amount) * 100, 1)); + } +} diff --git a/app/Models/Project.php b/app/Models/Project.php new file mode 100644 index 00000000..c7001439 --- /dev/null +++ b/app/Models/Project.php @@ -0,0 +1,103 @@ + 'date', + 'end_date' => 'date', + 'progress' => 'integer', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function manager() + { + return $this->belongsTo(User::class, 'manager_id'); + } + + public function currency() + { + return $this->belongsTo(Currency::class, 'default_currency_id'); + } + + public function users() + { + return $this->belongsToMany(User::class, 'project_users') + ->withPivot('role') + ->withTimestamps(); + } + + public function tasks() + { + return $this->hasMany(Task::class); + } + + public function expenses() + { + return $this->hasMany(Expense::class); + } + + public function pettyCashes() + { + return $this->hasMany(PettyCash::class); + } + + public function workLogs() + { + return $this->hasMany(WorkLog::class); + } + + public function scopeTenantScope($query) + { + return $query->where('tenant_id', session('tenant_id')); + } + + public function getStatusLabelAttribute() + { + return [ + 'planning' => __('project.status_planning'), + 'active' => __('project.status_active'), + 'on_hold' => __('project.status_on_hold'), + 'completed' => __('project.status_completed'), + 'cancelled' => __('project.status_cancelled'), + ][$this->status] ?? $this->status; + } + + public function defaultCurrency() + { + return $this->belongsTo(Currency::class, 'default_currency_id'); + } + + public function employees() + { + return $this->belongsToMany(Employee::class, 'employee_project', 'project_id', 'employee_id') + ->withPivot('role') + ->withTimestamps(); + } +} diff --git a/app/Models/ProjectUser.php b/app/Models/ProjectUser.php new file mode 100644 index 00000000..5d92362d --- /dev/null +++ b/app/Models/ProjectUser.php @@ -0,0 +1,27 @@ +belongsTo(Project::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Role.php b/app/Models/Role.php new file mode 100644 index 00000000..69ed0849 --- /dev/null +++ b/app/Models/Role.php @@ -0,0 +1,28 @@ +belongsTo(Tenant::class); + } +} diff --git a/app/Models/Task.php b/app/Models/Task.php new file mode 100644 index 00000000..e4953566 --- /dev/null +++ b/app/Models/Task.php @@ -0,0 +1,180 @@ + 'date', + 'due_date' => 'date', + 'completed_at' => 'datetime', + 'progress' => 'integer', + 'estimated_hours' => 'decimal:2', + 'order' => 'integer', + ]; + + // ─── Basic Relationships ─────────────────────────────────────────── + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function parent() + { + return $this->belongsTo(Task::class, 'parent_id'); + } + + public function children() + { + return $this->hasMany(Task::class, 'parent_id'); + } + + public function assignee() + { + return $this->belongsTo(User::class, 'assignee_id'); + } + + public function reporter() + { + return $this->belongsTo(User::class, 'reporter_id'); + } + + public function logs() + { + return $this->hasMany(TaskLog::class); + } + + public function workLogs() + { + return $this->hasMany(WorkLog::class); + } + + // ─── Dependency Relationships ────────────────────────────────────── + + /** + * Tasks that this task depends on (predecessors). + * e.g., "Task A depends on Task B" means B must finish before A starts. + */ + public function dependencies() + { + return $this->belongsToMany(Task::class, 'task_dependencies', 'task_id', 'depends_on_task_id') + ->withPivot('dependency_type') + ->withTimestamps(); + } + + /** + * Tasks that depend on this task (successors). + * e.g., "Task A blocks Task C" means C cannot start until A finishes. + */ + public function dependents() + { + return $this->belongsToMany(Task::class, 'task_dependencies', 'depends_on_task_id', 'task_id') + ->withPivot('dependency_type') + ->withTimestamps(); + } + + // ─── Scopes ──────────────────────────────────────────────────────── + + public function scopeByStatus($query, $status) + { + return $query->where('status', $status); + } + + public function scopeByPriority($query, $priority) + { + return $query->where('priority', $priority); + } + + public function scopeTopLevel($query) + { + return $query->whereNull('parent_id'); + } + + public function scopeWithDependencies($query) + { + return $query->with(['dependencies', 'dependents', 'assignee', 'project']); + } + + // ─── Accessors ───────────────────────────────────────────────────── + + public function getStatusLabelAttribute() + { + return [ + 'pending' => __('task.status_pending'), + 'todo' => __('task.status_pending'), + 'in_progress' => __('task.status_in_progress'), + 'review' => __('task.status_review'), + 'done' => __('task.status_done'), + 'cancelled' => __('task.status_cancelled'), + ][$this->status] ?? $this->status; + } + + /** + * Check if all dependency tasks are completed. + */ + public function getCanStartAttribute(): bool + { + if ($this->dependencies->isEmpty()) { + return true; + } + + return $this->dependencies->every(fn ($dep) => $dep->status === 'done'); + } + + /** + * Check if this task is blocked by incomplete dependencies. + */ + public function getIsBlockedAttribute(): bool + { + return !$this->canStart; + } + + /** + * Get the blocking tasks (incomplete dependencies). + */ + public function getBlockingTasksAttribute() + { + return $this->dependencies->filter(fn ($dep) => $dep->status !== 'done'); + } + + /** + * Calculate duration in days. + */ + public function getDurationDaysAttribute(): ?int + { + if (!$this->start_date || !$this->due_date) { + return null; + } + return $this->start_date->diffInDays($this->due_date) + 1; + } +} diff --git a/app/Models/TaskLog.php b/app/Models/TaskLog.php new file mode 100644 index 00000000..c2029107 --- /dev/null +++ b/app/Models/TaskLog.php @@ -0,0 +1,39 @@ + 'array', + 'new_value' => 'array', + ]; + + public function task() + { + return $this->belongsTo(Task::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php new file mode 100644 index 00000000..74e438b3 --- /dev/null +++ b/app/Models/Tenant.php @@ -0,0 +1,66 @@ + 'boolean', + 'allow_multi_currency' => 'boolean', + 'settings' => 'array', + ]; + + protected static function booted() + { + static::creating(function ($tenant) { + if (empty($tenant->slug)) { + $tenant->slug = Str::slug($tenant->name); + } + }); + } + + public function baseCurrency() + { + return $this->belongsTo(Currency::class, 'base_currency_id'); + } + + public function users() + { + return $this->hasMany(User::class); + } + + public function projects() + { + return $this->hasMany(Project::class); + } + + public function employees() + { + return $this->hasMany(Employee::class); + } + + public function expenses() + { + return $this->hasMany(Expense::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index def621f4..64b063ca 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,46 +2,71 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Spatie\Permission\Traits\HasRoles; +use Illuminate\Database\Eloquent\SoftDeletes; class User extends Authenticatable { - use HasFactory, Notifiable; + use HasFactory, Notifiable, HasRoles, SoftDeletes; - /** - * The attributes that are mass assignable. - * - * @var array - */ protected $fillable = [ + 'tenant_id', 'name', 'email', 'password', + 'locale', + 'preferred_calendar', + 'is_active', ]; - /** - * The attributes that should be hidden for serialization. - * - * @var array - */ protected $hidden = [ 'password', 'remember_token', ]; - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array + protected $casts = [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'is_active' => 'boolean', + ]; + + public function tenant() { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; + return $this->belongsTo(Tenant::class); + } + + public function managedProjects() + { + return $this->hasMany(Project::class, 'manager_id'); + } + + public function projects() + { + return $this->belongsToMany(Project::class, 'project_users') + ->withPivot('role') + ->withTimestamps(); + } + + public function assignedTasks() + { + return $this->hasMany(Task::class, 'assignee_id'); + } + + public function getLocale(): string + { + return $this->locale ?? $this->tenant?->default_locale ?? config('Vernova.default_locale', 'fa'); + } + + public function getCalendar(): string + { + return $this->preferred_calendar ?? $this->tenant?->default_calendar ?? config('Vernova.default_calendar', 'jalali'); + } + + public function getDirection(): string + { + return in_array($this->getLocale(), config('Vernova.rtl_locales', [])) ? 'rtl' : 'ltr'; } } diff --git a/app/Models/Warehouse.php b/app/Models/Warehouse.php new file mode 100644 index 00000000..7ef36b27 --- /dev/null +++ b/app/Models/Warehouse.php @@ -0,0 +1,39 @@ + 'boolean', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function inventoryTransactions() + { + return $this->hasMany(InventoryTransaction::class); + } +} diff --git a/app/Models/WorkLog.php b/app/Models/WorkLog.php new file mode 100644 index 00000000..9ed29ca5 --- /dev/null +++ b/app/Models/WorkLog.php @@ -0,0 +1,66 @@ + 'date', + 'hours_worked' => 'decimal:2', + 'overtime_hours' => 'decimal:2', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function employee() + { + return $this->belongsTo(Employee::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function task() + { + return $this->belongsTo(Task::class); + } + + /** + * Scope: filter by date range + */ + public function scopeDateRange($query, $from, $to) + { + return $query->whereBetween('date', [$from, $to]); + } + + /** + * Scope: overtime logs (overtime_hours > 0) + */ + public function scopeOvertime($query) + { + return $query->where('overtime_hours', '>', 0); + } +} diff --git a/app/Policies/AttachmentPolicy.php b/app/Policies/AttachmentPolicy.php new file mode 100644 index 00000000..682c8355 --- /dev/null +++ b/app/Policies/AttachmentPolicy.php @@ -0,0 +1,68 @@ +hasRole('admin')) { + return true; + } + + // Check tenant scope + if ($attachment->tenant_id && $attachment->tenant_id !== session('tenant_id')) { + return false; + } + + // Check if user can view the parent model + if ($attachment->attachable) { + try { + return $user->can('view', $attachment->attachable); + } catch (\Throwable $e) { + return true; // If no policy exists for parent model, allow + } + } + + // User who uploaded can view + return $attachment->user_id === $user->id; + } + + /** + * Determine whether the user can delete the attachment. + */ + public function delete(User $user, Attachment $attachment): bool + { + // Admin can delete all + if ($user->hasRole('admin')) { + return true; + } + + // Check tenant scope + if ($attachment->tenant_id && $attachment->tenant_id !== session('tenant_id')) { + return false; + } + + // Check if user can update the parent model + if ($attachment->attachable) { + try { + return $user->can('update', $attachment->attachable); + } catch (\Throwable $e) { + return false; + } + } + + // User who uploaded can delete + return $attachment->user_id === $user->id; + } +} diff --git a/app/Policies/DocumentPolicy.php b/app/Policies/DocumentPolicy.php new file mode 100644 index 00000000..974b4daa --- /dev/null +++ b/app/Policies/DocumentPolicy.php @@ -0,0 +1,40 @@ +can('documents.view') || $user->can('documents.manage'); + } + + public function view(User $user, Document $document): bool + { + return $user->tenant_id === $document->tenant_id + && ($user->can('documents.view') || $user->can('documents.manage')); + } + + public function create(User $user): bool + { + return $user->can('documents.create') || $user->can('documents.manage'); + } + + public function update(User $user, Document $document): bool + { + return $user->tenant_id === $document->tenant_id + && ($user->can('documents.update') || $user->can('documents.manage')); + } + + public function delete(User $user, Document $document): bool + { + return $user->tenant_id === $document->tenant_id + && ($user->can('documents.delete') || $user->can('documents.manage')); + } +} diff --git a/app/Policies/EmployeePolicy.php b/app/Policies/EmployeePolicy.php new file mode 100644 index 00000000..1b53e6df --- /dev/null +++ b/app/Policies/EmployeePolicy.php @@ -0,0 +1,76 @@ +tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasRole('hr') + || $user->hasPermissionTo('view employees')); + } + + /** + * Determine whether the user can view the employee. + */ + public function view(User $user, Employee $employee): bool + { + return $user->tenant_id === $employee->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasRole('hr') + || $user->hasPermissionTo('view employees')); + } + + /** + * Determine whether the user can create employees. + */ + public function create(User $user): bool + { + return $user->tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('hr') + || $user->hasPermissionTo('create employees')); + } + + /** + * Determine whether the user can update the employee. + */ + public function update(User $user, Employee $employee): bool + { + return $user->tenant_id === $employee->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('hr') + || $user->hasPermissionTo('edit employees')); + } + + /** + * Determine whether the user can delete the employee. + */ + public function delete(User $user, Employee $employee): bool + { + return $user->tenant_id === $employee->tenant_id + && ($user->hasRole('admin') + || $user->hasPermissionTo('delete employees')); + } + + /** + * Determine whether the user can view employee financials. + */ + public function viewFinancials(User $user, Employee $employee): bool + { + return $user->tenant_id === $employee->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('hr') + || $user->hasPermissionTo('view employee financials')); + } +} diff --git a/app/Policies/ExpensePolicy.php b/app/Policies/ExpensePolicy.php new file mode 100644 index 00000000..08cbed1c --- /dev/null +++ b/app/Policies/ExpensePolicy.php @@ -0,0 +1,92 @@ +tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasRole('accountant') + || $user->hasPermissionTo('view expenses')); + } + + /** + * Determine whether the user can view the expense. + */ + public function view(User $user, Expense $expense): bool + { + if ($user->tenant_id !== $expense->tenant_id) { + return false; + } + + if ($user->hasRole('admin') || $user->hasRole('manager') || $user->hasRole('accountant')) { + return true; + } + + // Users can view their own expenses + return $expense->requested_by === $user->id; + } + + /** + * Determine whether the user can create expenses. + */ + public function create(User $user): bool + { + return $user->tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasRole('accountant') + || $user->hasPermissionTo('create expenses')); + } + + /** + * Determine whether the user can update the expense. + */ + public function update(User $user, Expense $expense): bool + { + if ($user->tenant_id !== $expense->tenant_id) { + return false; + } + + // Only pending expenses can be updated + if ($expense->status !== 'pending') { + return false; + } + + if ($user->hasRole('admin') || $user->hasRole('manager') || $user->hasRole('accountant')) { + return true; + } + + return $expense->requested_by === $user->id; + } + + /** + * Determine whether the user can delete the expense. + */ + public function delete(User $user, Expense $expense): bool + { + return $user->tenant_id === $expense->tenant_id + && ($user->hasRole('admin') + || $user->hasPermissionTo('delete expenses')); + } + + /** + * Determine whether the user can approve/reject expenses. + */ + public function approve(User $user, Expense $expense): bool + { + return $user->tenant_id === $expense->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('approve expenses')); + } +} diff --git a/app/Policies/InventoryTransactionPolicy.php b/app/Policies/InventoryTransactionPolicy.php new file mode 100644 index 00000000..2e9d9635 --- /dev/null +++ b/app/Policies/InventoryTransactionPolicy.php @@ -0,0 +1,55 @@ +can('inventory.view') || $user->can('inventory.update'); + } + + /** + * Determine whether the user can view the inventory transaction. + */ + public function view(User $user, InventoryTransaction $inventoryTransaction): bool + { + return $user->tenant_id === $inventoryTransaction->tenant_id + && ($user->can('inventory.view') || $user->can('inventory.update')); + } + + /** + * Determine whether the user can create inventory transactions. + */ + public function create(User $user): bool + { + return $user->can('inventory.create'); + } + + /** + * Determine whether the user can update the inventory transaction. + */ + public function update(User $user, InventoryTransaction $inventoryTransaction): bool + { + return $user->tenant_id === $inventoryTransaction->tenant_id + && $user->can('inventory.update'); + } + + /** + * Determine whether the user can delete the inventory transaction. + */ + public function delete(User $user, InventoryTransaction $inventoryTransaction): bool + { + return $user->tenant_id === $inventoryTransaction->tenant_id + && $user->can('inventory.delete'); + } +} diff --git a/app/Policies/ItemPolicy.php b/app/Policies/ItemPolicy.php new file mode 100644 index 00000000..0ef57a23 --- /dev/null +++ b/app/Policies/ItemPolicy.php @@ -0,0 +1,55 @@ +can('inventory.view') || $user->can('inventory.update'); + } + + /** + * Determine whether the user can view the item. + */ + public function view(User $user, Item $item): bool + { + return $user->tenant_id === $item->tenant_id + && ($user->can('inventory.view') || $user->can('inventory.update')); + } + + /** + * Determine whether the user can create items. + */ + public function create(User $user): bool + { + return $user->can('inventory.create'); + } + + /** + * Determine whether the user can update the item. + */ + public function update(User $user, Item $item): bool + { + return $user->tenant_id === $item->tenant_id + && $user->can('inventory.update'); + } + + /** + * Determine whether the user can delete the item. + */ + public function delete(User $user, Item $item): bool + { + return $user->tenant_id === $item->tenant_id + && $user->can('inventory.delete'); + } +} diff --git a/app/Policies/LetterPolicy.php b/app/Policies/LetterPolicy.php new file mode 100644 index 00000000..9db68c86 --- /dev/null +++ b/app/Policies/LetterPolicy.php @@ -0,0 +1,55 @@ +can('letters.view') || $user->can('letters.update'); + } + + /** + * Determine whether the user can view the letter. + */ + public function view(User $user, Letter $letter): bool + { + return $user->tenant_id === $letter->tenant_id + && ($user->can('letters.view') || $user->can('letters.update')); + } + + /** + * Determine whether the user can create letters. + */ + public function create(User $user): bool + { + return $user->can('letters.create'); + } + + /** + * Determine whether the user can update the letter. + */ + public function update(User $user, Letter $letter): bool + { + return $user->tenant_id === $letter->tenant_id + && $user->can('letters.update'); + } + + /** + * Determine whether the user can delete the letter. + */ + public function delete(User $user, Letter $letter): bool + { + return $user->tenant_id === $letter->tenant_id + && $user->can('letters.delete'); + } +} diff --git a/app/Policies/PettyCashPolicy.php b/app/Policies/PettyCashPolicy.php new file mode 100644 index 00000000..1f65c42c --- /dev/null +++ b/app/Policies/PettyCashPolicy.php @@ -0,0 +1,86 @@ +can('petty-cash.view') || $user->can('petty-cash.update'); + } + + /** + * Determine whether the user can view the petty cash request. + */ + public function view(User $user, PettyCash $pettyCash): bool + { + if ($user->tenant_id !== $pettyCash->tenant_id) { + return false; + } + + if ($user->can('petty-cash.view') || $user->can('petty-cash.update')) { + return true; + } + + // Users can view their own petty cash requests + return $pettyCash->requested_by === $user->id; + } + + /** + * Determine whether the user can create petty cash requests. + */ + public function create(User $user): bool + { + return $user->can('petty-cash.create'); + } + + /** + * Determine whether the user can update the petty cash request. + */ + public function update(User $user, PettyCash $pettyCash): bool + { + if ($user->tenant_id !== $pettyCash->tenant_id) { + return false; + } + + // Only pending requests can be updated + if ($pettyCash->status !== 'pending') { + return false; + } + + if ($user->can('petty-cash.update')) { + return true; + } + + // Users can update their own pending requests + return $pettyCash->requested_by === $user->id && $user->can('petty-cash.create'); + } + + /** + * Determine whether the user can delete the petty cash request. + */ + public function delete(User $user, PettyCash $pettyCash): bool + { + return $user->tenant_id === $pettyCash->tenant_id + && $user->can('petty-cash.delete'); + } + + /** + * Determine whether the user can approve the petty cash request. + * Only users with approve permission can approve. + */ + public function approve(User $user, PettyCash $pettyCash): bool + { + return $user->tenant_id === $pettyCash->tenant_id + && $user->can('petty-cash.approve'); + } +} diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php new file mode 100644 index 00000000..6a680b6d --- /dev/null +++ b/app/Policies/ProjectPolicy.php @@ -0,0 +1,76 @@ +tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('view projects')); + } + + /** + * Determine whether the user can view the project. + */ + public function view(User $user, Project $project): bool + { + return $user->tenant_id === $project->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('view projects') + || $project->users()->where('user_id', $user->id)->exists()); + } + + /** + * Determine whether the user can create projects. + */ + public function create(User $user): bool + { + return $user->tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('create projects')); + } + + /** + * Determine whether the user can update the project. + */ + public function update(User $user, Project $project): bool + { + return $user->tenant_id === $project->tenant_id + && ($user->hasRole('admin') + || ($user->hasRole('manager') && $project->manager_id === $user->id) + || $user->hasPermissionTo('edit projects')); + } + + /** + * Determine whether the user can delete the project. + */ + public function delete(User $user, Project $project): bool + { + return $user->tenant_id === $project->tenant_id + && ($user->hasRole('admin') + || $user->hasPermissionTo('delete projects')); + } + + /** + * Determine whether the user can manage project members. + */ + public function manageMembers(User $user, Project $project): bool + { + return $user->tenant_id === $project->tenant_id + && ($user->hasRole('admin') + || ($user->hasRole('manager') && $project->manager_id === $user->id) + || $user->hasPermissionTo('manage project members')); + } +} diff --git a/app/Policies/TaskPolicy.php b/app/Policies/TaskPolicy.php new file mode 100644 index 00000000..85e30454 --- /dev/null +++ b/app/Policies/TaskPolicy.php @@ -0,0 +1,96 @@ +tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('view tasks')); + } + + /** + * Determine whether the user can view the task. + */ + public function view(User $user, Task $task): bool + { + if ($user->tenant_id !== $task->tenant_id) { + return false; + } + + // Admin and managers can view all + if ($user->hasRole('admin') || $user->hasRole('manager')) { + return true; + } + + // Check if user is a member of the project + return $task->project + && $task->project->users()->where('user_id', $user->id)->exists(); + } + + /** + * Determine whether the user can create tasks. + */ + public function create(User $user): bool + { + return $user->tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('create tasks')); + } + + /** + * Determine whether the user can update the task. + */ + public function update(User $user, Task $task): bool + { + if ($user->tenant_id !== $task->tenant_id) { + return false; + } + + if ($user->hasRole('admin') || $user->hasRole('manager')) { + return true; + } + + // User can update if they are the assignee or reporter + return $task->assignee_id === $user->id + || $task->reporter_id === $user->id; + } + + /** + * Determine whether the user can delete the task. + */ + public function delete(User $user, Task $task): bool + { + return $user->tenant_id === $task->tenant_id + && ($user->hasRole('admin') + || $user->hasPermissionTo('delete tasks')); + } + + /** + * Determine whether the user can update the task status. + */ + public function updateStatus(User $user, Task $task): bool + { + if ($user->tenant_id !== $task->tenant_id) { + return false; + } + + if ($user->hasRole('admin') || $user->hasRole('manager')) { + return true; + } + + // User can update status if they are a member of the project + return $task->project + && $task->project->users()->where('user_id', $user->id)->exists(); + } +} diff --git a/app/Policies/WarehousePolicy.php b/app/Policies/WarehousePolicy.php new file mode 100644 index 00000000..2798af05 --- /dev/null +++ b/app/Policies/WarehousePolicy.php @@ -0,0 +1,63 @@ +tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can view the warehouse. + */ + public function view(User $user, Warehouse $warehouse): bool + { + return $user->tenant_id === $warehouse->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can create warehouses. + */ + public function create(User $user): bool + { + return $user->tenant_id === session('tenant_id') + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can update the warehouse. + */ + public function update(User $user, Warehouse $warehouse): bool + { + return $user->tenant_id === $warehouse->tenant_id + && ($user->hasRole('admin') + || $user->hasRole('manager') + || $user->hasPermissionTo('manage inventory')); + } + + /** + * Determine whether the user can delete the warehouse. + */ + public function delete(User $user, Warehouse $warehouse): bool + { + return $user->tenant_id === $warehouse->tenant_id + && ($user->hasRole('admin') + || $user->hasPermissionTo('manage inventory')); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b65..ac191cc7 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,22 +3,56 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Session; +use Illuminate\Support\Facades\Gate; +use App\Models\Item; +use App\Models\PettyCash; +use App\Models\InventoryTransaction; +use App\Models\Expense; +use App\Models\Employee; +use App\Models\Task; +use App\Models\Project; +use App\Models\Attachment; +use App\Models\Letter; +use App\Policies\ItemPolicy; +use App\Policies\PettyCashPolicy; +use App\Policies\InventoryTransactionPolicy; +use App\Policies\ExpensePolicy; +use App\Policies\EmployeePolicy; +use App\Policies\TaskPolicy; +use App\Policies\ProjectPolicy; +use App\Policies\AttachmentPolicy; +use App\Policies\LetterPolicy; class AppServiceProvider extends ServiceProvider { - /** - * Register any application services. - */ public function register(): void { // } - /** - * Bootstrap any application services. - */ public function boot(): void { - // + if ($locale = Session::get('locale')) { + if (in_array($locale, array_keys(config('Vernova.supported_locales')))) { + App::setLocale($locale); + } + } + + if (app()->environment('production')) { + \URL::forceScheme('https'); + } + + // Register all policies explicitly + Gate::policy(Item::class, ItemPolicy::class); + Gate::policy(PettyCash::class, PettyCashPolicy::class); + Gate::policy(InventoryTransaction::class, InventoryTransactionPolicy::class); + Gate::policy(Expense::class, ExpensePolicy::class); + Gate::policy(Employee::class, EmployeePolicy::class); + Gate::policy(Task::class, TaskPolicy::class); + Gate::policy(Project::class, ProjectPolicy::class); + Gate::policy(Attachment::class, AttachmentPolicy::class); + Gate::policy(Letter::class, LetterPolicy::class); } } diff --git a/app/Providers/BladeServiceProvider.php b/app/Providers/BladeServiceProvider.php new file mode 100644 index 00000000..e6aa4cbe --- /dev/null +++ b/app/Providers/BladeServiceProvider.php @@ -0,0 +1,40 @@ +"; + }); + + Blade::directive('localizeDate', function ($expression) { + return ""; + }); + + Blade::directive('localizeCurrency', function ($expression) { + return ""; + }); + + Blade::directive('direction', function () { + return "getLocale(), config('Vernova.rtl_locales')) ? 'rtl' : 'ltr'; ?>"; + }); + + Blade::if('isRtl', function () { + return in_array(app()->getLocale(), config('Vernova.rtl_locales')); + }); + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php new file mode 100644 index 00000000..a5b39e70 --- /dev/null +++ b/app/Providers/RouteServiceProvider.php @@ -0,0 +1,36 @@ +by($request->user()?->id ?: $request->ip()); + }); + + $this->routes(function () { + Route::middleware('api') + ->prefix('api') + ->group(base_path('routes/api.php')); + + Route::middleware('web') + ->group(base_path('routes/web.php')); + }); + } +} diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 00000000..b79715a0 --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,67 @@ +format($format); + } catch (\Exception $e) { + return $date->format($format); + } + } + + return $date->format($format); + } +} + +if (!function_exists('persian_num')) { + function persian_num($number) + { + $persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; + $english = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + return str_replace($english, $persian, (string) $number); + } +} + +if (!function_exists('format_currency')) { + function format_currency($amount, $currency = 'IRR') + { + $formatted = number_format((float) $amount, $currency === 'IRR' ? 0 : 2); + + if (app()->getLocale() === 'fa') { + $formatted = persian_num($formatted); + } + + $symbol = ''; + switch ($currency) { + case 'IRR': $symbol = 'ریال'; break; + case 'USD': $symbol = '$'; break; + case 'EUR': $symbol = '€'; break; + case 'AED': $symbol = 'د.إ'; break; + } + + return $formatted . ' ' . $symbol; + } +} + +if (!function_exists('get_direction')) { + function get_direction() + { + return app()->getLocale() === 'fa' ? 'rtl' : 'ltr'; + } +} + +if (!function_exists('is_rtl')) { + function is_rtl() + { + return app()->getLocale() === 'fa'; + } +} \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index 7b162dac..e571865b 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,11 +7,22 @@ use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { - // + $middleware->web(append: [ + \App\Http\Middleware\SetTenant::class, + \App\Http\Middleware\SetLocale::class, + ]); + + $middleware->alias([ + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + 'tenant.scope' => \App\Http\Middleware\TenantScope::class, + ]); }) ->withExceptions(function (Exceptions $exceptions) { // diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d1..b2405fe4 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,4 +2,6 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\RouteServiceProvider::class, + App\Providers\BladeServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 7033b06f..f2e67e65 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,36 @@ { - "name": "laravel/laravel", + "name": "vernova/vernova", "type": "project", - "description": "The skeleton application for the Laravel framework.", - "keywords": ["laravel", "framework"], - "license": "MIT", + "description": "Vernova - Multi-tenant Project Management Platform", + "keywords": ["laravel", "project-management", "multi-tenant", "saas"], + "license": "proprietary", "require": { "php": "^8.2", "laravel/framework": "^11.0", - "laravel/tinker": "^2.9" + "laravel/sanctum": "^4.0", + "laravel/tinker": "^2.9", + "spatie/laravel-permission": "^6.0", + "morilog/jalali": "^3.0", + "maatwebsite/excel": "^1.1", + "barryvdh/laravel-dompdf": "^2.0", + "guzzlehttp/guzzle": "^7.8" }, "require-dev": { "fakerphp/faker": "^1.23", - "laravel/pint": "^1.13", - "laravel/sail": "^1.26", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.0", - "phpunit/phpunit": "^10.5", - "spatie/laravel-ignition": "^2.4" + "phpunit/phpunit": "^11.0", + "pestphp/pest": "^2.0" }, "autoload": { "psr-4": { "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "app/Helpers/helpers.php" + ] }, "autoload-dev": { "psr-4": { @@ -37,24 +44,8 @@ ], "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force" - ], - "post-root-package-install": [ - "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" - ], - "post-create-project-cmd": [ - "@php artisan key:generate --ansi", - "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", - "@php artisan migrate --ansi" ] }, - "extra": { - "branch-alias": { - "dev-master": "11.x-dev" - }, - "laravel": { - "dont-discover": [] - } - }, "config": { "optimize-autoloader": true, "preferred-install": "dist", diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..cc919eae --- /dev/null +++ b/composer.lock @@ -0,0 +1,9844 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "60f91fa5fd51fa595004e621385b3753", + "packages": [ + { + "name": "barryvdh/laravel-dompdf", + "version": "v3.1.2", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^3.0", + "illuminate/support": "^9|^10|^11|^12|^13.0", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.7|^3.0", + "orchestra/testbench": "^7|^8|^9.16|^10|^11.0", + "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.2" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2026-02-21T08:51:10+00:00" + }, + { + "name": "beberlei/assert", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=6.0.0", + "yoast/phpunit-polyfills": "^0.1.0" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Assert/functions.php" + ], + "psr-4": { + "Assert\\": "lib/Assert" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "support": { + "issues": "https://github.com/beberlei/assert/issues", + "source": "https://github.com/beberlei/assert/tree/v3.3.3" + }, + "time": "2024-07-15T13:18:35+00:00" + }, + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "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.14.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+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": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dompdf/dompdf", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "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.5" + }, + "time": "2026-03-03T13:54:37+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12" + }, + "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.2" + }, + "time": "2026-01-20T14:10:26+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4 || ^9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" + }, + "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.2" + }, + "time": "2026-01-02T16:01:13+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^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.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-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.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "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.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7c8d84b39e680315f687e8662a9d6fb0865c5148", + "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.4", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.5" + }, + "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": "2026-05-27T11:53:46+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "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.4.1" + }, + "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": "2026-05-20T22:57:30+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.10.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "d2a1a094e396da8957e797489fddaf860c340cfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/d2a1a094e396da8957e797489fddaf860c340cfc", + "reference": "d2a1a094e396da8957e797489fddaf860c340cfc", + "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": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "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.10.4" + }, + "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": "2026-05-29T12:59:07+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.6", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "eef7f87bab6f204eba3c39224d8075c70c637946" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/eef7f87bab6f204eba3c39224d8075c70c637946", + "reference": "eef7f87bab6f204eba3c39224d8075c70c637946", + "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.52 || ^9.6.34", + "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.6" + }, + "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": "2026-05-23T22:00:21+00:00" + }, + { + "name": "laravel/framework", + "version": "v11.54.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "4e7ae67eedd803eaea8ceb5f7e113f495d2c0c58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/4e7ae67eedd803eaea8ceb5f7e113f495d2c0c58", + "reference": "4e7ae67eedd803eaea8ceb5f7e113f495d2c0c58", + "shasum": "" + }, + "require": { + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.1.18|^0.2.0|^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": "^2.72.6|^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.0.3", + "symfony/error-handler": "^7.0.3", + "symfony/finder": "^7.0.3", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.0.3", + "symfony/mailer": "^7.0.3", + "symfony/mime": "^7.0.3", + "symfony/polyfill-php83": "^1.31", + "symfony/process": "^7.0.3", + "symfony/routing": "^7.0.3", + "symfony/uid": "^7.0.3", + "symfony/var-dumper": "^7.0.3", + "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": "^9.18.0", + "pda/pheanstalk": "^5.0.6", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "2.1.41", + "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", + "predis/predis": "^2.3", + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.0.3", + "symfony/http-client": "^7.0.3", + "symfony/psr-http-message-bridge": "^7.0.3", + "symfony/translation": "^7.0.3" + }, + "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.3.6|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3).", + "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.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.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": "2026-05-26T23:41:51+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.18", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a19af51bb144bf87f08397921fa619f85c7d4e72", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.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|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "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.18" + }, + "time": "2026-05-19T00:47:18+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.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": "2026-04-16T14:03:50+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "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|^8.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.11.1" + }, + "time": "2026-02-06T14:12:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "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 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-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": "2026-03-19T13:16:38+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.34.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e", + "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e", + "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.34.0" + }, + "time": "2026-05-14T10:28:08+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "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.31.0" + }, + "time": "2026-01-23T15:30:45+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.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "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", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "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": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "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.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.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", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "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 tools for parsing and resolving RFC3987/RFC3986 URI", + "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.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "maatwebsite/excel", + "version": "3.1.69", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760", + "reference": "ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "ext-json": "*", + "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0||^13.0", + "php": "^7.0||^8.0", + "phpoffice/phpspreadsheet": "^1.30.4", + "psr/simple-cache": "^1.0||^2.0||^3.0" + }, + "require-dev": { + "laravel/scout": "^7.0||^8.0||^9.0||^10.0||^11.0", + "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0||^11.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + }, + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.69" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2026-04-30T20:03:58+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": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "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 || ^2.0", + "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.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "morilog/jalali", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/morilog/jalali.git", + "reference": "40b906c405b72edd20e73797b40be1e650e872c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/morilog/jalali/zipball/40b906c405b72edd20e73797b40be1e650e872c1", + "reference": "40b906c405b72edd20e73797b40be1e650e872c1", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.0", + "nesbot/carbon": "^1.21 || ^2.0 || ^3.0", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Morilog\\Jalali\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Milad Rey", + "email": "miladr@gmail.com" + }, + { + "name": "Morteza Parvini", + "email": "m.parvini@outlook.com" + } + ], + "description": "This Package helps developers to easily work with Jalali (Shamsi or Iranian) dates in PHP applications, based on Jalali (Shamsi) DateTime class.", + "keywords": [ + "Jalali", + "date", + "datetime", + "laravel", + "morilog" + ], + "support": { + "issues": "https://github.com/morilog/jalali/issues", + "source": "https://github.com/morilog/jalali/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://issuehunt.io/r/morilog", + "type": "issuehunt" + } + ], + "time": "2025-12-25T07:12:28+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.4", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.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://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "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": "2026-04-07T09:57:54+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette 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.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.4" + }, + "time": "2026-05-11T20:49:54+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "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": "It's 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.4.0" + }, + "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": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.4", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9", + "reference": "02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": ">=7.4.0 <8.5.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "doctrine/instantiator": "^1.5", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + }, + { + "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/1.30.4" + }, + "time": "2026-04-19T06:00:39+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+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.23", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^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", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" + }, + "time": "2026-05-23T13:41:31+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.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.3.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.32 || 2.1.32", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.2.8", + "rector/type-perfect": "1.0.0 || 2.1.0", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.4.x-dev" + } + }, + "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], + "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/v9.3.0" + }, + "time": "2026-03-03T17:31:43+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "6.25.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "d7d4cb0d58616722f1afc90e0484e4825155b9b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/d7d4cb0d58616722f1afc90e0484e4825155b9b3", + "reference": "d7d4cb0d58616722f1afc90e0484e4825155b9b3", + "shasum": "" + }, + "require": { + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/passport": "^11.0|^12.0|^13.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^2.0|^3.0|^4.0" + }, + "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.25.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-03-17T22:46:46+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "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.4.8" + }, + "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": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "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|^8.0" + }, + "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|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.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.4.13" + }, + "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": "2026-05-24T08:56:14+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b75663ed96cf4756e28e3105476f220f92886cc4", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4", + "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.4.9" + }, + "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": "2026-04-18T13:18:21+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-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.7.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": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.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.4.8" + }, + "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": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "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|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.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.4.9" + }, + "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": "2026-04-18T13:18:21+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "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.7-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.7.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": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e0be088d22278583a82da281886e8c3592fbf149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.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.4.8" + }, + "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": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "bc354f47c62301e990b7874fa662326368508e2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", + "reference": "bc354f47c62301e990b7874fa662326368508e2c", + "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.13" + }, + "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": "2026-05-24T11:20:33+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "9df847980c436451f4f51d1284491bb4356dd989" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", + "reference": "9df847980c436451f4f51d1284491bb4356dd989", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "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/flex": "<2.10", + "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|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.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.4.13" + }, + "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": "2026-05-27T08:31:43+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", + "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|^8.0", + "symfony/mime": "^7.2|^8.0", + "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|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.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.4.12" + }, + "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": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", + "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": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "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": "^5.2|^6.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.13" + }, + "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": "2026-05-23T16:22:37+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "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.37.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": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "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.38.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": "2026-05-26T05:58:03+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "dc21118016c039a66235cf93d96b435ffb282412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "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.38.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": "2026-05-25T15:22:23+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "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.38.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": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "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.38.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": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "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.37.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": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "8339098cae28673c15cce00d80734af0453054e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2", + "reference": "8339098cae28673c15cce00d80734af0453054e2", + "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.38.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": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.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": "2026-05-26T02:25:22+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "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.37.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": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "f5804be144caceb570f6747519999636b664f24c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", + "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.4.13" + }, + "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": "2026-05-23T16:05:06+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", + "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|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.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.4.13" + }, + "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": "2026-05-24T11:20:33+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "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.7-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.7.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": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.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.4.13" + }, + "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": "2026-05-23T15:23:29+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "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|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.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.4.10" + }, + "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": "2026-05-06T11:19:24+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-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.7.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": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2676b524340abcfe4d6151ec698463cebafee439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", + "reference": "2676b524340abcfe4d6151ec698463cebafee439", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.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.4.9" + }, + "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": "2026-04-30T15:19:22+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.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.4.8" + }, + "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": "2026-03-30T13:44:50+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.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.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "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.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "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.1.1" + }, + "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": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.3" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-04-20T15:26:14+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.61.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "68ef35015630fe510432e63e11e21749006df688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/68ef35015630fe510432e63e11e21749006df688", + "reference": "68ef35015630fe510432e63e11e21749006df688", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "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": "2026-05-23T23:33:57+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.5.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "f5c101b929c958e849a633283adff296ed5f38f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f5c101b929c958e849a633283adff296ed5f38f5", + "reference": "f5c101b929c958e849a633283adff296ed5f38f5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.16.0", + "nunomaduro/termwind": "^2.1.0", + "php": "^8.2.0", + "symfony/console": "^7.1.5" + }, + "conflict": { + "laravel/framework": "<11.0.0 || >=12.0.0", + "phpunit/phpunit": "<10.5.1 || >=12.0.0" + }, + "require-dev": { + "larastan/larastan": "^2.9.8", + "laravel/framework": "^11.28.0", + "laravel/pint": "^1.18.1", + "laravel/sail": "^1.36.0", + "laravel/sanctum": "^4.0.3", + "laravel/tinker": "^2.10.0", + "orchestra/testbench-core": "^9.5.3", + "pestphp/pest": "^2.36.0 || ^3.4.0", + "sebastian/environment": "^6.1.0 || ^7.2.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", + "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": "2024-10-15T16:06:32+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "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": "10.1.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/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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": "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/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "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": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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": "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/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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", + "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", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.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/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.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/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.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", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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" + } + ], + "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", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "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" + }, + { + "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/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-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/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "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" + }, + { + "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/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "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" + } + ], + "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/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "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" + }, + { + "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/5.1.4" + }, + "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/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.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": "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/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.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/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.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" + } + ], + "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", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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" + } + ], + "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", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "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" + }, + { + "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/5.0.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/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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": "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", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "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 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", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "spatie/backtrace", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/backtrace.git", + "reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/backtrace/zipball/8ffe78be5ed355b5009e3dd989d183433e9a5adc", + "reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "laravel/serializable-closure": "^1.3 || ^2.0", + "phpunit/phpunit": "^9.3 || ^11.4.3", + "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6", + "symfony/var-dumper": "^5.1|^6.0|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Backtrace\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van de Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A better backtrace", + "homepage": "https://github.com/spatie/backtrace", + "keywords": [ + "Backtrace", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/backtrace/issues", + "source": "https://github.com/spatie/backtrace/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spatie", + "type": "github" + }, + { + "url": "https://spatie.be/open-source/support-us", + "type": "other" + } + ], + "time": "2026-03-11T13:48:28+00:00" + }, + { + "name": "spatie/error-solutions", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/spatie/error-solutions.git", + "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/error-solutions/zipball/e495d7178ca524f2dd0fe6a1d99a1e608e1c9936", + "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "illuminate/broadcasting": "^10.0|^11.0|^12.0", + "illuminate/cache": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "livewire/livewire": "^2.11|^3.5.20", + "openai-php/client": "^0.10.1", + "orchestra/testbench": "8.22.3|^9.0|^10.0", + "pestphp/pest": "^2.20|^3.0", + "phpstan/phpstan": "^2.1", + "psr/simple-cache": "^3.0", + "psr/simple-cache-implementation": "^3.0", + "spatie/ray": "^1.28", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "legacy/ignition", + "Spatie\\ErrorSolutions\\": "src", + "Spatie\\LaravelIgnition\\": "legacy/laravel-ignition" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "This is my package error-solutions", + "homepage": "https://github.com/spatie/error-solutions", + "keywords": [ + "error-solutions", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/error-solutions/issues", + "source": "https://github.com/spatie/error-solutions/tree/1.1.3" + }, + "funding": [ + { + "url": "https://github.com/Spatie", + "type": "github" + } + ], + "time": "2025-02-14T12:29:50+00:00" + }, + { + "name": "spatie/flare-client-php", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/flare-client-php.git", + "reference": "53f41b08a27cc039e1a8ed2be9a202e924f31bad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/53f41b08a27cc039e1a8ed2be9a202e924f31bad", + "reference": "53f41b08a27cc039e1a8ed2be9a202e924f31bad", + "shasum": "" + }, + "require": { + "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "spatie/backtrace": "^1.6.1", + "symfony/http-foundation": "^5.2|^6.0|^7.0|^8.0", + "symfony/mime": "^5.2|^6.0|^7.0|^8.0", + "symfony/process": "^5.2|^6.0|^7.0|^8.0", + "symfony/var-dumper": "^5.2|^6.0|^7.0|^8.0" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.5.0", + "pestphp/pest": "^1.20|^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/pest-plugin-snapshots": "^1.0|^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\FlareClient\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Send PHP errors to Flare", + "homepage": "https://github.com/spatie/flare-client-php", + "keywords": [ + "exception", + "flare", + "reporting", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/flare-client-php/issues", + "source": "https://github.com/spatie/flare-client-php/tree/1.11.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-15T09:31:32+00:00" + }, + { + "name": "spatie/ignition", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/ignition.git", + "reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/ignition/zipball/b59385bb7aa24dae81bcc15850ebecfda7b40838", + "reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.0", + "spatie/backtrace": "^1.7.1", + "spatie/error-solutions": "^1.1.2", + "spatie/flare-client-php": "^1.9", + "symfony/console": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/http-foundation": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/mime": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/var-dumper": "^5.4.42|^6.0|^7.0|^8.0" + }, + "require-dev": { + "illuminate/cache": "^9.52|^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.4", + "pestphp/pest": "^1.20|^2.0|^3.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "psr/simple-cache-implementation": "*", + "symfony/cache": "^5.4.38|^6.0|^7.0|^8.0", + "symfony/process": "^5.4.35|^6.0|^7.0|^8.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spatie", + "email": "info@spatie.be", + "role": "Developer" + } + ], + "description": "A beautiful error page for PHP applications.", + "homepage": "https://flareapp.io/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/spatie/ignition/issues", + "source": "https://github.com/spatie/ignition" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-03-17T10:51:08+00:00" + }, + { + "name": "spatie/laravel-ignition", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-ignition.git", + "reference": "45b3b6e1e73fc161cba2149972698644b99594ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/45b3b6e1e73fc161cba2149972698644b99594ee", + "reference": "45b3b6e1e73fc161cba2149972698644b99594ee", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/support": "^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.72|^3.0", + "php": "^8.2", + "spatie/ignition": "^1.16", + "symfony/console": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "require-dev": { + "livewire/livewire": "^3.7.0|^4.0|dev-josh/v3-laravel-13-support", + "mockery/mockery": "^1.6.12", + "openai-php/client": "^0.10.3|^0.19", + "orchestra/testbench": "^v9.16.0|^10.6|^11.0", + "pestphp/pest": "^3.7|^4.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.8", + "vlucas/phpdotenv": "^5.6.2" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "psr/simple-cache-implementation": "Needed to cache solutions from OpenAI" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Flare": "Spatie\\LaravelIgnition\\Facades\\Flare" + }, + "providers": [ + "Spatie\\LaravelIgnition\\IgnitionServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\LaravelIgnition\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spatie", + "email": "info@spatie.be", + "role": "Developer" + } + ], + "description": "A beautiful error page for Laravel applications.", + "homepage": "https://flareapp.io/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/spatie/laravel-ignition/issues", + "source": "https://github.com/spatie/laravel-ignition" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-03-17T12:20:04+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.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.4.13" + }, + "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": "2026-05-25T06:06:12+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "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.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/config/app.php b/config/app.php index f4672673..83860e06 100644 --- a/config/app.php +++ b/config/app.php @@ -13,7 +13,7 @@ return [ | */ - 'name' => env('APP_NAME', 'Laravel'), + 'name' => env('APP_NAME', 'Vernova'), /* |-------------------------------------------------------------------------- diff --git a/config/database.php b/config/database.php index 3ddc3968..5dff41e6 100644 --- a/config/database.php +++ b/config/database.php @@ -49,7 +49,7 @@ return [ 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => env('DB_CHARSET', 'utf8mb4'), - 'collation' => env('DB_COLLATION', 'utf8mb4_0900_ai_ci'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 00000000..79645058 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,119 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission model" here, but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement + * `Spatie\Permission\Contracts\Permission`. + */ + + 'permission' => \App\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role model" here, but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement + * `Spatie\Permission\Contracts\Role`. + */ + + 'role' => \App\Models\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you can easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you can easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you can easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you can easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you can easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, //default 'role_id', + 'permission_pivot_key' => null, //default 'permission_id', + + /* + * Change this if you want to name the related model foreign key other than + * defaults + */ + 'model_morph_key' => 'model_id', + 'model_key' => null, // default 'id', + + /* + * Change this if you want to use the teams feature. The default is to + * use the 'team_id' column. If you are using a custom column name, set + * it here. + */ + 'team_foreign_key' => 'tenant_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the + * gate. Set this to false if you want to implement custom logic for checking + * permissions. + */ + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Passport\PassportServiceProvider will be registered + * automatically. Set this to false if you don't use Passport or want to + * register it yourself. + */ + 'register_passport_client_scopes' => false, + + /* + * When set to true the package will cache the permissions and roles for each + * user. This can improve performance but will use more memory. + */ + 'cache' => [ + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + 'key' => 'spatie.permission.cache', + 'store' => 'file', + ], +]; diff --git a/config/projectra.php b/config/projectra.php new file mode 100644 index 00000000..2c54c7da --- /dev/null +++ b/config/projectra.php @@ -0,0 +1,30 @@ + [ + 'fa' => 'فارسی', + 'en' => 'English', + 'ar' => 'العربية', + ], + + 'supported_calendars' => ['gregorian', 'jalali', 'hijri'], + + 'supported_currencies' => ['IRR', 'USD', 'EUR', 'AED'], + + 'default_locale' => env('DEFAULT_LOCALE', 'fa'), + + 'default_calendar' => env('DEFAULT_CALENDAR', 'jalali'), + + 'default_currency' => env('DEFAULT_CURRENCY', 'IRR'), + + 'currency_decimal_places' => [ + 'IRR' => 0, + 'USD' => 2, + 'EUR' => 2, + 'AED' => 2, + ], + + 'rtl_locales' => ['fa', 'ar'], + + 'exchange_rate_cache_hours' => 24, +]; diff --git a/config/projectra_app_additions.php b/config/projectra_app_additions.php new file mode 100644 index 00000000..f073945b --- /dev/null +++ b/config/projectra_app_additions.php @@ -0,0 +1,32 @@ + [ + // Add custom providers here + ], + + /* + |-------------------------------------------------------------------------- + | Application Aliases + |-------------------------------------------------------------------------- + | + | Additional facade aliases for the Vernova application. + | + */ + 'aliases' => [ + 'JalaliHelper' => App\Helpers\JalaliHelper::class, + 'NumberHelper' => App\Helpers\NumberHelper::class, + 'CurrencyHelper' => App\Helpers\CurrencyHelper::class, + 'CalendarHelper' => App\Helpers\CalendarHelper::class, + ], + +]; diff --git a/database/.gitignore b/database/.gitignore deleted file mode 100644 index 9b19b93c..00000000 --- a/database/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.sqlite* diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php deleted file mode 100644 index 584104c9..00000000 --- a/database/factories/UserFactory.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class UserFactory extends Factory -{ - /** - * The current password being used by the factory. - */ - protected static ?string $password; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'name' => fake()->name(), - 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), - ]; - } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } -} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php deleted file mode 100644 index 05fb5d9e..00000000 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ /dev/null @@ -1,49 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php deleted file mode 100644 index b9c106be..00000000 --- a/database/migrations/0001_01_01_000001_create_cache_table.php +++ /dev/null @@ -1,35 +0,0 @@ -string('key')->primary(); - $table->mediumText('value'); - $table->integer('expiration'); - }); - - Schema::create('cache_locks', function (Blueprint $table) { - $table->string('key')->primary(); - $table->string('owner'); - $table->integer('expiration'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('cache'); - Schema::dropIfExists('cache_locks'); - } -}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php deleted file mode 100644 index 425e7058..00000000 --- a/database/migrations/0001_01_01_000002_create_jobs_table.php +++ /dev/null @@ -1,57 +0,0 @@ -id(); - $table->string('queue')->index(); - $table->longText('payload'); - $table->unsignedTinyInteger('attempts'); - $table->unsignedInteger('reserved_at')->nullable(); - $table->unsignedInteger('available_at'); - $table->unsignedInteger('created_at'); - }); - - Schema::create('job_batches', function (Blueprint $table) { - $table->string('id')->primary(); - $table->string('name'); - $table->integer('total_jobs'); - $table->integer('pending_jobs'); - $table->integer('failed_jobs'); - $table->longText('failed_job_ids'); - $table->mediumText('options')->nullable(); - $table->integer('cancelled_at')->nullable(); - $table->integer('created_at'); - $table->integer('finished_at')->nullable(); - }); - - Schema::create('failed_jobs', function (Blueprint $table) { - $table->id(); - $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); - $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('jobs'); - Schema::dropIfExists('job_batches'); - Schema::dropIfExists('failed_jobs'); - } -}; diff --git a/database/migrations/2024_01_01_000001_create_currencies_table.php b/database/migrations/2024_01_01_000001_create_currencies_table.php new file mode 100644 index 00000000..a66741d8 --- /dev/null +++ b/database/migrations/2024_01_01_000001_create_currencies_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('code', 3)->unique(); + $table->string('name', 50); + $table->string('symbol', 10); + $table->tinyInteger('decimal_places')->default(2); + $table->decimal('exchange_rate_to_usd', 15, 6)->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('currencies'); + } +}; diff --git a/database/migrations/2024_01_01_000002_create_tenants_table.php b/database/migrations/2024_01_01_000002_create_tenants_table.php new file mode 100644 index 00000000..514fa413 --- /dev/null +++ b/database/migrations/2024_01_01_000002_create_tenants_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->string('domain')->nullable(); + $table->boolean('is_active')->default(true); + $table->string('default_locale', 10)->default('fa'); + $table->enum('default_calendar', ['gregorian', 'jalali', 'hijri'])->default('jalali'); + $table->foreignId('base_currency_id')->nullable()->constrained('currencies')->nullOnDelete(); + $table->boolean('allow_multi_currency')->default(false); + $table->string('logo_path')->nullable(); + $table->json('settings')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('tenants'); + } +}; diff --git a/database/migrations/2024_01_01_000003_create_users_table.php b/database/migrations/2024_01_01_000003_create_users_table.php new file mode 100644 index 00000000..efaf137f --- /dev/null +++ b/database/migrations/2024_01_01_000003_create_users_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->string('locale', 10)->nullable(); + $table->enum('preferred_calendar', ['gregorian', 'jalali', 'hijri'])->nullable(); + $table->boolean('is_active')->default(true); + $table->rememberToken(); + $table->timestamps(); + $table->softDeletes(); + $table->index('tenant_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('users'); + } +}; diff --git a/database/migrations/2024_01_01_000004_create_roles_permissions_tables.php b/database/migrations/2024_01_01_000004_create_roles_permissions_tables.php new file mode 100644 index 00000000..61801c21 --- /dev/null +++ b/database/migrations/2024_01_01_000004_create_roles_permissions_tables.php @@ -0,0 +1,57 @@ +id(); + $table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete(); + $table->string('name'); + $table->string('guard_name')->default('web'); + $table->timestamps(); + }); + + Schema::create('permissions', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('guard_name')->default('web'); + $table->timestamps(); + }); + + Schema::create('model_has_roles', function (Blueprint $table) { + $table->id(); + $table->foreignId('role_id')->constrained()->cascadeOnDelete(); + $table->string('model_type'); + $table->unsignedBigInteger('model_id'); + $table->index(['model_type', 'model_id']); + }); + + Schema::create('model_has_permissions', function (Blueprint $table) { + $table->id(); + $table->foreignId('permission_id')->constrained()->cascadeOnDelete(); + $table->string('model_type'); + $table->unsignedBigInteger('model_id'); + $table->index(['model_type', 'model_id']); + }); + + Schema::create('role_has_permissions', function (Blueprint $table) { + $table->id(); + $table->foreignId('permission_id')->constrained()->cascadeOnDelete(); + $table->foreignId('role_id')->constrained()->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists('role_has_permissions'); + Schema::dropIfExists('model_has_permissions'); + Schema::dropIfExists('model_has_roles'); + Schema::dropIfExists('permissions'); + Schema::dropIfExists('roles'); + } +}; diff --git a/database/migrations/2024_01_01_000005_create_projects_table.php b/database/migrations/2024_01_01_000005_create_projects_table.php new file mode 100644 index 00000000..c5333165 --- /dev/null +++ b/database/migrations/2024_01_01_000005_create_projects_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('code', 20); + $table->text('description')->nullable(); + $table->enum('status', ['planning', 'in_progress', 'on_hold', 'completed', 'cancelled'])->default('planning'); + $table->date('start_date'); + $table->date('end_date'); + $table->tinyInteger('progress')->default(0); + $table->decimal('budget', 15, 0)->default(0); + $table->foreignId('default_currency_id')->nullable()->constrained('currencies')->nullOnDelete(); + $table->decimal('budget_usd', 15, 2)->default(0); + $table->foreignId('manager_id')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + $table->softDeletes(); + $table->index(['tenant_id', 'code']); + $table->index('status'); + }); + } + + public function down(): void + { + Schema::dropIfExists('projects'); + } +}; diff --git a/database/migrations/2024_01_01_000006_create_project_users_table.php b/database/migrations/2024_01_01_000006_create_project_users_table.php new file mode 100644 index 00000000..cc98c12a --- /dev/null +++ b/database/migrations/2024_01_01_000006_create_project_users_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('project_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('role'); + $table->timestamps(); + + $table->unique(['project_id', 'user_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('project_users'); + } +}; diff --git a/database/migrations/2024_01_01_000007_create_employees_table.php b/database/migrations/2024_01_01_000007_create_employees_table.php new file mode 100644 index 00000000..9dd4101d --- /dev/null +++ b/database/migrations/2024_01_01_000007_create_employees_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->nullable()->constrained()->nullOnDelete(); + $table->string('national_code'); + $table->string('first_name'); + $table->string('last_name'); + $table->string('phone')->nullable(); + $table->string('job_title'); + $table->enum('contract_type', ['full_time', 'part_time', 'contractor', 'intern'])->default('full_time'); + $table->date('hire_date'); + $table->date('termination_date')->nullable(); + $table->enum('status', ['active', 'inactive', 'terminated'])->default('active'); + $table->decimal('base_salary', 15, 0)->default(0); + $table->foreignId('currency_id')->nullable()->constrained('currencies')->nullOnDelete(); + $table->string('bank_account')->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + $table->index('project_id'); + $table->index('status'); + }); + } + + public function down(): void + { + Schema::dropIfExists('employees'); + } +}; diff --git a/database/migrations/2024_01_01_000008_create_work_logs_table.php b/database/migrations/2024_01_01_000008_create_work_logs_table.php new file mode 100644 index 00000000..ad362426 --- /dev/null +++ b/database/migrations/2024_01_01_000008_create_work_logs_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('employee_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->nullable()->constrained()->nullOnDelete(); + $table->date('date'); + $table->decimal('hours_worked', 5, 2); + $table->text('description')->nullable(); + $table->boolean('is_overtime')->default(false); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('employee_id'); + $table->index('project_id'); + $table->index('date'); + }); + } + + public function down(): void + { + Schema::dropIfExists('work_logs'); + } +}; diff --git a/database/migrations/2024_01_01_000009_create_petty_cashes_table.php b/database/migrations/2024_01_01_000009_create_petty_cashes_table.php new file mode 100644 index 00000000..cbbd8dd1 --- /dev/null +++ b/database/migrations/2024_01_01_000009_create_petty_cashes_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->constrained()->cascadeOnDelete(); + $table->decimal('amount', 15, 0); + $table->foreignId('currency_id')->nullable()->constrained('currencies')->nullOnDelete(); + $table->decimal('original_amount', 15, 0)->nullable(); + $table->text('description')->nullable(); + $table->date('date'); + $table->string('recipient')->nullable(); + $table->enum('status', ['pending', 'approved', 'rejected', 'settled'])->default('pending'); + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('project_id'); + $table->index('status'); + }); + } + + public function down(): void + { + Schema::dropIfExists('petty_cashes'); + } +}; diff --git a/database/migrations/2024_01_01_000010_create_expenses_table.php b/database/migrations/2024_01_01_000010_create_expenses_table.php new file mode 100644 index 00000000..877cb287 --- /dev/null +++ b/database/migrations/2024_01_01_000010_create_expenses_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->constrained()->cascadeOnDelete(); + $table->foreignId('petty_cash_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('employee_id')->nullable()->constrained()->nullOnDelete(); + $table->decimal('amount', 15, 0); + $table->foreignId('currency_id')->constrained('currencies')->cascadeOnDelete(); + $table->decimal('original_amount', 15, 0)->nullable(); + $table->text('description')->nullable(); + $table->date('expense_date'); + $table->string('category')->nullable(); + $table->string('receipt_path')->nullable(); + $table->enum('status', ['pending', 'approved', 'rejected'])->default('pending'); + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + $table->index('project_id'); + $table->index('status'); + $table->index('expense_date'); + }); + } + + public function down(): void + { + Schema::dropIfExists('expenses'); + } +}; diff --git a/database/migrations/2024_01_01_000011_create_employee_financials_table.php b/database/migrations/2024_01_01_000011_create_employee_financials_table.php new file mode 100644 index 00000000..3b0e5a4b --- /dev/null +++ b/database/migrations/2024_01_01_000011_create_employee_financials_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('employee_id')->constrained()->cascadeOnDelete(); + $table->enum('type', ['salary', 'bonus', 'deduction', 'advance', 'overtime']); + $table->decimal('amount', 15, 0); + $table->foreignId('currency_id')->constrained('currencies')->cascadeOnDelete(); + $table->decimal('original_amount', 15, 0)->nullable(); + $table->text('description')->nullable(); + $table->date('date'); + $table->tinyInteger('month'); + $table->unsignedInteger('year'); + $table->boolean('is_paid')->default(false); + $table->timestamp('paid_at')->nullable(); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('employee_id'); + $table->index(['month', 'year']); + $table->index('type'); + }); + } + + public function down(): void + { + Schema::dropIfExists('employee_financials'); + } +}; diff --git a/database/migrations/2024_01_01_000012_create_items_table.php b/database/migrations/2024_01_01_000012_create_items_table.php new file mode 100644 index 00000000..f8fa02be --- /dev/null +++ b/database/migrations/2024_01_01_000012_create_items_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('code')->nullable(); + $table->string('unit', 30); + $table->string('category')->nullable(); + $table->decimal('min_stock', 15, 2)->default(0); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('code'); + }); + } + + public function down(): void + { + Schema::dropIfExists('items'); + } +}; diff --git a/database/migrations/2024_01_01_000013_create_warehouses_table.php b/database/migrations/2024_01_01_000013_create_warehouses_table.php new file mode 100644 index 00000000..c8ca78c5 --- /dev/null +++ b/database/migrations/2024_01_01_000013_create_warehouses_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->nullable()->constrained()->nullOnDelete(); + $table->string('name'); + $table->string('location')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('project_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('warehouses'); + } +}; diff --git a/database/migrations/2024_01_01_000014_create_inventory_transactions_table.php b/database/migrations/2024_01_01_000014_create_inventory_transactions_table.php new file mode 100644 index 00000000..3e698ad4 --- /dev/null +++ b/database/migrations/2024_01_01_000014_create_inventory_transactions_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('item_id')->constrained()->cascadeOnDelete(); + $table->foreignId('warehouse_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->nullable()->constrained()->nullOnDelete(); + $table->enum('type', ['in', 'out', 'transfer']); + $table->decimal('quantity', 15, 2); + $table->decimal('unit_price', 15, 0)->nullable(); + $table->foreignId('currency_id')->nullable()->constrained('currencies')->nullOnDelete(); + $table->string('reference')->nullable(); + $table->text('notes')->nullable(); + $table->date('date'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + $table->index('item_id'); + $table->index('warehouse_id'); + $table->index('project_id'); + $table->index('type'); + }); + } + + public function down(): void + { + Schema::dropIfExists('inventory_transactions'); + } +}; diff --git a/database/migrations/2024_01_01_000015_create_tasks_table.php b/database/migrations/2024_01_01_000015_create_tasks_table.php new file mode 100644 index 00000000..dcea68a1 --- /dev/null +++ b/database/migrations/2024_01_01_000015_create_tasks_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->constrained()->cascadeOnDelete(); + $table->foreignId('parent_id')->nullable()->constrained('tasks')->nullOnDelete(); + $table->string('title'); + $table->text('description')->nullable(); + $table->enum('status', ['todo', 'in_progress', 'review', 'done', 'cancelled'])->default('todo'); + $table->enum('priority', ['low', 'medium', 'high', 'urgent'])->default('medium'); + $table->foreignId('assignee_id')->nullable()->constrained('users')->nullOnDelete(); + $table->date('due_date')->nullable(); + $table->tinyInteger('progress')->default(0); + $table->decimal('estimated_hours', 5, 2)->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + $table->index('project_id'); + $table->index('parent_id'); + $table->index('status'); + $table->index('priority'); + $table->index('assignee_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('tasks'); + } +}; diff --git a/database/migrations/2024_01_01_000016_create_task_logs_table.php b/database/migrations/2024_01_01_000016_create_task_logs_table.php new file mode 100644 index 00000000..eb5518e4 --- /dev/null +++ b/database/migrations/2024_01_01_000016_create_task_logs_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('task_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('action'); + $table->text('description')->nullable(); + $table->json('old_value')->nullable(); + $table->json('new_value')->nullable(); + $table->timestamp('created_at'); + + $table->index('task_id'); + $table->index('user_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('task_logs'); + } +}; diff --git a/database/migrations/2024_01_01_000017_create_letters_table.php b/database/migrations/2024_01_01_000017_create_letters_table.php new file mode 100644 index 00000000..36e93419 --- /dev/null +++ b/database/migrations/2024_01_01_000017_create_letters_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->nullable()->constrained()->nullOnDelete(); + $table->enum('type', ['incoming', 'outgoing']); + $table->string('subject'); + $table->text('content')->nullable(); + $table->string('sender'); + $table->string('recipient'); + $table->date('letter_date'); + $table->string('reference_number')->nullable(); + $table->foreignId('parent_letter_id')->nullable()->constrained('letters')->nullOnDelete(); + $table->enum('status', ['draft', 'sent', 'received', 'archived'])->default('draft'); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('project_id'); + $table->index('type'); + $table->index('status'); + $table->index('letter_date'); + }); + } + + public function down(): void + { + Schema::dropIfExists('letters'); + } +}; diff --git a/database/migrations/2024_01_01_000018_create_letter_attachments_table.php b/database/migrations/2024_01_01_000018_create_letter_attachments_table.php new file mode 100644 index 00000000..a80c5141 --- /dev/null +++ b/database/migrations/2024_01_01_000018_create_letter_attachments_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('letter_id')->constrained()->cascadeOnDelete(); + $table->string('file_path'); + $table->string('file_name'); + $table->unsignedInteger('file_size'); + $table->string('mime_type'); + $table->timestamp('created_at'); + + $table->index('letter_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('letter_attachments'); + } +}; diff --git a/database/migrations/2024_01_01_000019_create_documents_table.php b/database/migrations/2024_01_01_000019_create_documents_table.php new file mode 100644 index 00000000..0f17a49c --- /dev/null +++ b/database/migrations/2024_01_01_000019_create_documents_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('project_id')->nullable()->constrained()->nullOnDelete(); + $table->string('title'); + $table->text('description')->nullable(); + $table->string('file_path'); + $table->string('file_type', 50); + $table->foreignId('uploaded_by')->constrained('users')->cascadeOnDelete(); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('project_id'); + $table->index('uploaded_by'); + }); + } + + public function down(): void + { + Schema::dropIfExists('documents'); + } +}; diff --git a/database/migrations/2024_01_01_000020_create_audit_logs_table.php b/database/migrations/2024_01_01_000020_create_audit_logs_table.php new file mode 100644 index 00000000..f73ace1a --- /dev/null +++ b/database/migrations/2024_01_01_000020_create_audit_logs_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->string('model_type'); + $table->unsignedBigInteger('model_id'); + $table->string('action'); + $table->json('old_values')->nullable(); + $table->json('new_values')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->timestamp('created_at'); + + $table->index('tenant_id'); + $table->index('user_id'); + $table->index(['model_type', 'model_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('audit_logs'); + } +}; diff --git a/database/migrations/2024_01_01_000021_add_missing_columns_to_tables.php b/database/migrations/2024_01_01_000021_add_missing_columns_to_tables.php new file mode 100644 index 00000000..9407ef4c --- /dev/null +++ b/database/migrations/2024_01_01_000021_add_missing_columns_to_tables.php @@ -0,0 +1,148 @@ +string('title')->nullable()->after('project_id'); + } + if (!Schema::hasColumn('petty_cashes', 'remaining')) { + $table->decimal('remaining', 15, 0)->nullable()->after('original_amount'); + } + if (!Schema::hasColumn('petty_cashes', 'amount_usd')) { + $table->decimal('amount_usd', 15, 2)->nullable()->after('amount'); + } + if (!Schema::hasColumn('petty_cashes', 'approval_date')) { + $table->timestamp('approval_date')->nullable()->after('approved_by'); + } + if (!Schema::hasColumn('petty_cashes', 'requested_by')) { + $table->foreignId('requested_by')->nullable()->after('approval_date')->constrained('users')->nullOnDelete(); + } + if (!Schema::hasColumn('petty_cashes', 'approved_at')) { + $table->timestamp('approved_at')->nullable()->after('requested_by'); + } + if (!Schema::hasColumn('petty_cashes', 'deleted_at')) { + $table->softDeletes(); + } + }); + + // Rename petty_cashes.date → request_date + if (Schema::hasColumn('petty_cashes', 'date') && !Schema::hasColumn('petty_cashes', 'request_date')) { + Schema::table('petty_cashes', function (Blueprint $table) { + $table->renameColumn('date', 'request_date'); + }); + } + + // ============================================================ + // 2. WORK LOGS — add missing columns + // ============================================================ + Schema::table('work_logs', function (Blueprint $table) { + if (!Schema::hasColumn('work_logs', 'overtime_hours')) { + $table->decimal('overtime_hours', 5, 2)->default(0)->after('hours_worked'); + } + if (!Schema::hasColumn('work_logs', 'task_id')) { + $table->foreignId('task_id')->nullable()->after('project_id')->constrained('tasks')->nullOnDelete(); + } + if (!Schema::hasColumn('work_logs', 'deleted_at')) { + $table->softDeletes(); + } + }); + + // Drop old is_overtime (replaced by overtime_hours) + if (Schema::hasColumn('work_logs', 'is_overtime') && Schema::hasColumn('work_logs', 'overtime_hours')) { + Schema::table('work_logs', function (Blueprint $table) { + $table->dropColumn('is_overtime'); + }); + } + + // ============================================================ + // 3. EMPLOYEE FINANCIALS — rename date → effective_date + // ============================================================ + if (Schema::hasColumn('employee_financials', 'date') && !Schema::hasColumn('employee_financials', 'effective_date')) { + Schema::table('employee_financials', function (Blueprint $table) { + $table->renameColumn('date', 'effective_date'); + }); + } + + // ============================================================ + // 4. INVENTORY TRANSACTIONS — add missing columns + // ============================================================ + Schema::table('inventory_transactions', function (Blueprint $table) { + if (!Schema::hasColumn('inventory_transactions', 'created_by')) { + $table->foreignId('created_by')->nullable()->after('currency_id')->constrained('users')->nullOnDelete(); + } + if (!Schema::hasColumn('inventory_transactions', 'notes')) { + $table->text('notes')->nullable()->after('description'); + } + if (!Schema::hasColumn('inventory_transactions', 'reference_number')) { + $table->string('reference_number', 100)->nullable()->after('reference'); + } + }); + } + + public function down(): void + { + // 4. Inventory Transactions + Schema::table('inventory_transactions', function (Blueprint $table) { + if (Schema::hasColumn('inventory_transactions', 'created_by')) { + $table->dropConstrainedForeignId('created_by'); + } + if (Schema::hasColumn('inventory_transactions', 'notes')) { + $table->dropColumn('notes'); + } + if (Schema::hasColumn('inventory_transactions', 'reference_number')) { + $table->dropColumn('reference_number'); + } + }); + + // 3. Employee Financials + if (Schema::hasColumn('employee_financials', 'effective_date') && !Schema::hasColumn('employee_financials', 'date')) { + Schema::table('employee_financials', function (Blueprint $table) { + $table->renameColumn('effective_date', 'date'); + }); + } + + // 2. Work Logs + Schema::table('work_logs', function (Blueprint $table) { + if (!Schema::hasColumn('work_logs', 'is_overtime')) { + $table->boolean('is_overtime')->default(false); + } + if (Schema::hasColumn('work_logs', 'task_id')) { + $table->dropConstrainedForeignId('task_id'); + } + if (Schema::hasColumn('work_logs', 'overtime_hours')) { + $table->dropColumn('overtime_hours'); + } + if (Schema::hasColumn('work_logs', 'deleted_at')) { + $table->dropSoftDeletes(); + } + }); + + // 1. Petty Cashes + if (Schema::hasColumn('petty_cashes', 'request_date') && !Schema::hasColumn('petty_cashes', 'date')) { + Schema::table('petty_cashes', function (Blueprint $table) { + $table->renameColumn('request_date', 'date'); + }); + } + Schema::table('petty_cashes', function (Blueprint $table) { + if (Schema::hasColumn('petty_cashes', 'requested_by')) { + $table->dropConstrainedForeignId('requested_by'); + } + $cols = array_filter(['title', 'remaining', 'amount_usd', 'approval_date', 'approved_at', 'deleted_at'], function($col) { + return Schema::hasColumn('petty_cashes', $col); + }); + if (!empty($cols)) { + $table->dropColumn($cols); + } + }); + } +}; diff --git a/database/migrations/2024_01_01_000022_add_expense_and_pettycash_missing_columns.php b/database/migrations/2024_01_01_000022_add_expense_and_pettycash_missing_columns.php new file mode 100644 index 00000000..2c4f61ae --- /dev/null +++ b/database/migrations/2024_01_01_000022_add_expense_and_pettycash_missing_columns.php @@ -0,0 +1,94 @@ +foreignId('requested_by')->nullable()->after('receipt_path')->constrained('users')->nullOnDelete(); + } + if (!Schema::hasColumn('expenses', 'approval_date')) { + $table->timestamp('approval_date')->nullable()->after('approved_by'); + } + if (!Schema::hasColumn('expenses', 'amount_usd')) { + $table->decimal('amount_usd', 15, 2)->nullable()->after('amount'); + } + if (!Schema::hasColumn('expenses', 'original_amount')) { + $table->decimal('original_amount', 15, 0)->nullable()->after('amount_usd'); + } + }); + + // Add 'paid' to expenses status enum if needed + // Note: MySQL requires altering the enum column directly + if (Schema::hasColumn('expenses', 'status')) { + \DB::statement("ALTER TABLE expenses MODIFY COLUMN status ENUM('pending','approved','rejected','paid') DEFAULT 'pending'"); + } + + // ============================================================ + // 2. PETTY CASHES — add receipt_path column + // ============================================================ + Schema::table('petty_cashes', function (Blueprint $table) { + if (!Schema::hasColumn('petty_cashes', 'receipt_path')) { + $table->string('receipt_path')->nullable()->after('request_date'); + } + }); + + // ============================================================ + // 3. AUDIT LOGS — add url and method columns (for middleware) + // ============================================================ + Schema::table('audit_logs', function (Blueprint $table) { + if (!Schema::hasColumn('audit_logs', 'url')) { + $table->string('url')->nullable()->after('user_agent'); + } + if (!Schema::hasColumn('audit_logs', 'method')) { + $table->string('method', 10)->nullable()->after('url'); + } + }); + } + + public function down(): void + { + // 3. Audit Logs + Schema::table('audit_logs', function (Blueprint $table) { + if (Schema::hasColumn('audit_logs', 'method')) { + $table->dropColumn('method'); + } + if (Schema::hasColumn('audit_logs', 'url')) { + $table->dropColumn('url'); + } + }); + + // 2. Petty Cashes + Schema::table('petty_cashes', function (Blueprint $table) { + if (Schema::hasColumn('petty_cashes', 'receipt_path')) { + $table->dropColumn('receipt_path'); + } + }); + + // 1. Expenses — revert status enum + \DB::statement("ALTER TABLE expenses MODIFY COLUMN status ENUM('pending','approved','rejected') DEFAULT 'pending'"); + + Schema::table('expenses', function (Blueprint $table) { + if (Schema::hasColumn('expenses', 'original_amount')) { + $table->dropColumn('original_amount'); + } + if (Schema::hasColumn('expenses', 'amount_usd')) { + $table->dropColumn('amount_usd'); + } + if (Schema::hasColumn('expenses', 'approval_date')) { + $table->dropColumn('approval_date'); + } + if (Schema::hasColumn('expenses', 'requested_by')) { + $table->dropConstrainedForeignId('requested_by'); + } + }); + } +}; diff --git a/database/migrations/2024_01_01_000023_create_attachments_table.php b/database/migrations/2024_01_01_000023_create_attachments_table.php new file mode 100644 index 00000000..7b0958d3 --- /dev/null +++ b/database/migrations/2024_01_01_000023_create_attachments_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->morphs('attachable'); + $table->string('file_name'); + $table->string('file_path'); + $table->string('mime_type')->nullable(); + $table->unsignedBigInteger('file_size')->default(0); + $table->string('category')->default('receipt'); // receipt, document, photo, other + $table->text('description')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['tenant_id', 'attachable_type', 'attachable_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('attachments'); + } +}; diff --git a/database/migrations/2024_01_01_000024_create_task_dependencies_table.php b/database/migrations/2024_01_01_000024_create_task_dependencies_table.php new file mode 100644 index 00000000..88593bd5 --- /dev/null +++ b/database/migrations/2024_01_01_000024_create_task_dependencies_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('task_id')->constrained('tasks')->cascadeOnDelete(); + $table->foreignId('depends_on_task_id')->constrained('tasks')->cascadeOnDelete(); + $table->enum('dependency_type', ['finish_to_start', 'start_to_start', 'finish_to_finish', 'start_to_finish'])->default('finish_to_start'); + $table->timestamps(); + + // Prevent duplicate dependencies + $table->unique(['task_id', 'depends_on_task_id']); + + // Prevent self-dependency (handled at app level too) + $table->index('task_id'); + $table->index('depends_on_task_id'); + }); + + // Note: Adding columns to tasks table is now handled by the dedicated migration: + // 2024_01_01_000025_add_missing_columns_to_tasks_table.php + // This avoids the issue of ->after('completed_at') when completed_at doesn't exist yet + } + + public function down(): void + { + Schema::dropIfExists('task_dependencies'); + } +}; diff --git a/database/migrations/2024_01_01_000025_add_missing_columns_to_tasks_table.php b/database/migrations/2024_01_01_000025_add_missing_columns_to_tasks_table.php new file mode 100644 index 00000000..4290f933 --- /dev/null +++ b/database/migrations/2024_01_01_000025_add_missing_columns_to_tasks_table.php @@ -0,0 +1,66 @@ +date('start_date')->nullable()->after('due_date'); + } + }); + + Schema::table('tasks', function (Blueprint $table) { + if (!Schema::hasColumn('tasks', 'reporter_id')) { + $table->foreignId('reporter_id')->nullable()->after('assignee_id')->constrained('users')->nullOnDelete(); + } + }); + + Schema::table('tasks', function (Blueprint $table) { + if (!Schema::hasColumn('tasks', 'completed_at')) { + $table->timestamp('completed_at')->nullable()->after('estimated_hours'); + } + }); + + Schema::table('tasks', function (Blueprint $table) { + if (!Schema::hasColumn('tasks', 'order')) { + $table->integer('order')->default(0)->after('completed_at'); + } + }); + } + + public function down(): void + { + Schema::table('tasks', function (Blueprint $table) { + if (Schema::hasColumn('tasks', 'order')) { + $table->dropColumn('order'); + } + }); + + Schema::table('tasks', function (Blueprint $table) { + if (Schema::hasColumn('tasks', 'completed_at')) { + $table->dropColumn('completed_at'); + } + }); + + Schema::table('tasks', function (Blueprint $table) { + if (Schema::hasColumn('tasks', 'reporter_id')) { + $table->dropConstrainedForeignId('reporter_id'); + } + }); + + Schema::table('tasks', function (Blueprint $table) { + if (Schema::hasColumn('tasks', 'start_date')) { + $table->dropColumn('start_date'); + } + }); + } +}; diff --git a/database/migrations/2026_05_30_225223_add_created_by_to_tasks_table.php b/database/migrations/2026_05_30_225223_add_created_by_to_tasks_table.php new file mode 100644 index 00000000..29808b7e --- /dev/null +++ b/database/migrations/2026_05_30_225223_add_created_by_to_tasks_table.php @@ -0,0 +1,28 @@ +foreignId('requested_by')->nullable()->after('employee_id')->constrained('users')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('expenses', function (Blueprint $table) { + $table->dropForeign(['requested_by']); + $table->dropColumn('requested_by'); + }); + } +}; diff --git a/database/migrations/2026_06_04_143839_add_requested_by_and_request_date_to_petty_cashes_table.php b/database/migrations/2026_06_04_143839_add_requested_by_and_request_date_to_petty_cashes_table.php new file mode 100644 index 00000000..e6e85040 --- /dev/null +++ b/database/migrations/2026_06_04_143839_add_requested_by_and_request_date_to_petty_cashes_table.php @@ -0,0 +1,24 @@ +foreignId('requested_by')->nullable()->after('tenant_id')->constrained('users')->nullOnDelete(); + $table->date('request_date')->nullable()->after('description'); + }); + } + + public function down(): void + { + Schema::table('petty_cashes', function (Blueprint $table) { + $table->dropForeign(['requested_by']); + $table->dropColumn(['requested_by', 'request_date']); + }); + } +}; \ No newline at end of file diff --git a/database/seeders/CurrencySeeder.php b/database/seeders/CurrencySeeder.php new file mode 100644 index 00000000..f36bfc62 --- /dev/null +++ b/database/seeders/CurrencySeeder.php @@ -0,0 +1,66 @@ + 'IRR', + 'name' => 'Iranian Rial', + 'symbol' => '﷼', + 'decimal_places' => 0, + 'exchange_rate_to_usd' => 42000.000000, + 'is_active' => true, + ], + [ + 'code' => 'USD', + 'name' => 'US Dollar', + 'symbol' => '$', + 'decimal_places' => 2, + 'exchange_rate_to_usd' => 1.000000, + 'is_active' => true, + ], + [ + 'code' => 'EUR', + 'name' => 'Euro', + 'symbol' => '€', + 'decimal_places' => 2, + 'exchange_rate_to_usd' => 0.920000, + 'is_active' => true, + ], + [ + 'code' => 'AED', + 'name' => 'UAE Dirham', + 'symbol' => 'د.إ', + 'decimal_places' => 2, + 'exchange_rate_to_usd' => 3.670000, + 'is_active' => true, + ], + ]; + + foreach ($currencies as $currency) { + Currency::create($currency); + } + + $this->command->info('Currencies seeded: ' . count($currencies) . ' records created.'); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef2..0730fd87 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,22 +2,34 @@ namespace Database\Seeders; -use App\Models\User; -// use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. + * + * Execution order is critical due to foreign key dependencies: + * 1. Currencies — no dependencies (platform-level data) + * 2. Roles & Perms — no dependencies (Spatie is self-contained) + * 3. Tenants — depends on currencies (base_currency_id) + * 4. Users — depends on tenants & roles + * 5. Projects — depends on tenants, users, currencies + * 6. Employees — depends on tenants, projects, currencies + * 7. Tasks — depends on tenants, projects, users + * 8. Expenses — depends on tenants, projects, employees, currencies, users */ public function run(): void { - // User::factory(10)->create(); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $this->call([ + CurrencySeeder::class, + RolePermissionSeeder::class, + TenantSeeder::class, + UserSeeder::class, + ProjectSeeder::class, + EmployeeSeeder::class, + TaskSeeder::class, + ExpenseSeeder::class, ]); } } diff --git a/database/seeders/EmployeeSeeder.php b/database/seeders/EmployeeSeeder.php new file mode 100644 index 00000000..ec1c5141 --- /dev/null +++ b/database/seeders/EmployeeSeeder.php @@ -0,0 +1,297 @@ +firstOrFail(); + $tenant2 = Tenant::where('slug', 'global')->firstOrFail(); + + $irr = Currency::where('code', 'IRR')->firstOrFail(); + $usd = Currency::where('code', 'USD')->firstOrFail(); + + $tenant1Projects = $tenant1->projects()->orderBy('id')->get(); + $tenant2Projects = $tenant2->projects()->orderBy('id')->get(); + + // ───────────────────────────────────────────── + // Tenant 1 — Sepideh Employees (Persian) + // ───────────────────────────────────────────── + + $tenant1Employees = [ + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, // ساختمان مسکونی گلستان + 'national_code' => '0012345678', + 'first_name' => 'حسین', + 'last_name' => 'محمدی', + 'phone' => '09121234567', + 'job_title' => 'مهندس عمران', + 'contract_type' => 'permanent', + 'hire_date' => '2022-03-01', + 'status' => 'active', + 'base_salary' => 85000000, // 85M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-1234-5678', + 'notes' => null, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, + 'national_code' => '0023456789', + 'first_name' => 'رضا', + 'last_name' => 'کریمی', + 'phone' => '09132345678', + 'job_title' => 'کارگر ساختمانی', + 'contract_type' => 'temporary', + 'hire_date' => '2024-04-01', + 'status' => 'active', + 'base_salary' => 45000000, // 45M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-2345-6789', + 'notes' => null, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[1]->id, // پل عابر سپید + 'national_code' => '0034567890', + 'first_name' => 'مهدی', + 'last_name' => 'حسینی', + 'phone' => '09143456789', + 'job_title' => 'مهندس راه و ترابری', + 'contract_type' => 'permanent', + 'hire_date' => '2023-06-15', + 'status' => 'active', + 'base_salary' => 92000000, // 92M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-3456-7890', + 'notes' => null, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[2]->id, // مجتمع تجاری مروارید + 'national_code' => '0045678901', + 'first_name' => 'فاطمه', + 'last_name' => 'احمدی', + 'phone' => '09154567890', + 'job_title' => 'حسابدار', + 'contract_type' => 'permanent', + 'hire_date' => '2022-09-01', + 'status' => 'active', + 'base_salary' => 78000000, // 78M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-4567-8901', + 'notes' => null, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[2]->id, + 'national_code' => '0056789012', + 'first_name' => 'امیر', + 'last_name' => 'نجفی', + 'phone' => '09165678901', + 'job_title' => 'تاسیساتکار', + 'contract_type' => 'contractor', + 'hire_date' => '2024-01-10', + 'status' => 'active', + 'base_salary' => 65000000, // 65M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-5678-9012', + 'notes' => 'پیمانکار تاسیسات مکانیکی', + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[3]->id, // استخر و سونا سلامت + 'national_code' => '0067890123', + 'first_name' => 'سعید', + 'last_name' => 'موسوی', + 'phone' => '09176789012', + 'job_title' => 'برقکار', + 'contract_type' => 'temporary', + 'hire_date' => '2023-08-20', + 'status' => 'terminated', + 'termination_date' => '2024-05-30', + 'base_salary' => 55000000, // 55M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-6789-0123', + 'notes' => 'پروژه به اتمام رسیده — تسویه شده', + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[4]->id, // بازسازی بیمارستان شفا + 'national_code' => '0078901234', + 'first_name' => 'زهرا', + 'last_name' => 'صادقی', + 'phone' => '09187890123', + 'job_title' => 'معمار', + 'contract_type' => 'permanent', + 'hire_date' => '2023-01-15', + 'status' => 'on_leave', + 'base_salary' => 88000000, // 88M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-7890-1234', + 'notes' => 'مرخصی استعلاجی از ۱۴۰۳/۰۴/۰۱ تا ۱۴۰۳/۰۶/۳۱', + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[4]->id, + 'national_code' => '0089012345', + 'first_name' => 'محمد', + 'last_name' => 'جعفری', + 'phone' => '09198901234', + 'job_title' => 'نجار', + 'contract_type' => 'contractor', + 'hire_date' => '2024-03-01', + 'status' => 'active', + 'base_salary' => 50000000, // 50M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-8901-2345', + 'notes' => 'پیمانکار کارهای چوب و دکوراسیون', + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => null, + 'national_code' => '0090123456', + 'first_name' => 'علی', + 'last_name' => 'عباسی', + 'phone' => '09209012345', + 'job_title' => 'راننده', + 'contract_type' => 'temporary', + 'hire_date' => '2024-02-15', + 'status' => 'active', + 'base_salary' => 40000000, // 40M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-9012-3456', + 'notes' => 'راننده کامیون حمل مصالح', + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, + 'national_code' => '0101234567', + 'first_name' => 'مریم', + 'last_name' => 'رحیمی', + 'phone' => '09210123456', + 'job_title' => 'مسئول انبار', + 'contract_type' => 'permanent', + 'hire_date' => '2023-11-01', + 'status' => 'active', + 'base_salary' => 52000000, // 52M IRR + 'currency_id' => $irr->id, + 'bank_account' => '6037-9975-0123-4567', + 'notes' => null, + ], + ]; + + foreach ($tenant1Employees as $empData) { + Employee::create($empData); + } + + // ───────────────────────────────────────────── + // Tenant 2 — Global Employees (English) + // ───────────────────────────────────────────── + + $tenant2Employees = [ + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[0]->id, // Riverside Tower + 'national_code' => '1000000001', + 'first_name' => 'David', + 'last_name' => 'Johnson', + 'phone' => '+1-555-0101', + 'job_title' => 'Civil Engineer', + 'contract_type' => 'permanent', + 'hire_date' => '2023-01-15', + 'status' => 'active', + 'base_salary' => 8500, // 8,500 USD + 'currency_id' => $usd->id, + 'bank_account' => 'US12-3456-7890-1234', + 'notes' => null, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[0]->id, + 'national_code' => '1000000002', + 'first_name' => 'Sarah', + 'last_name' => 'Williams', + 'phone' => '+1-555-0102', + 'job_title' => 'Project Coordinator', + 'contract_type' => 'permanent', + 'hire_date' => '2023-06-01', + 'status' => 'active', + 'base_salary' => 6200, + 'currency_id' => $usd->id, + 'bank_account' => 'US12-3456-7890-5678', + 'notes' => null, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[1]->id, // Industrial Warehouse + 'national_code' => '1000000003', + 'first_name' => 'Michael', + 'last_name' => 'Brown', + 'phone' => '+1-555-0103', + 'job_title' => 'Steel Erector', + 'contract_type' => 'contractor', + 'hire_date' => '2024-03-10', + 'status' => 'active', + 'base_salary' => 5500, + 'currency_id' => $usd->id, + 'bank_account' => 'US12-3456-7890-9012', + 'notes' => 'Subcontractor — structural steelwork', + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => null, + 'national_code' => '1000000004', + 'first_name' => 'Emily', + 'last_name' => 'Davis', + 'phone' => '+1-555-0104', + 'job_title' => 'Accountant', + 'contract_type' => 'permanent', + 'hire_date' => '2022-09-15', + 'status' => 'active', + 'base_salary' => 5800, + 'currency_id' => $usd->id, + 'bank_account' => 'US12-3456-7890-3456', + 'notes' => null, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[2]->id, // Greenfield School + 'national_code' => '1000000005', + 'first_name' => 'Robert', + 'last_name' => 'Taylor', + 'phone' => '+1-555-0105', + 'job_title' => 'Electrician', + 'contract_type' => 'temporary', + 'hire_date' => '2024-01-20', + 'status' => 'terminated', + 'termination_date' => '2024-07-15', + 'base_salary' => 4500, + 'currency_id' => $usd->id, + 'bank_account' => 'US12-3456-7890-7890', + 'notes' => 'Project completed — contract ended', + ], + ]; + + foreach ($tenant2Employees as $empData) { + Employee::create($empData); + } + + $total = count($tenant1Employees) + count($tenant2Employees); + $this->command->info("Employees seeded: {$total} records created."); + } +} diff --git a/database/seeders/ExpenseSeeder.php b/database/seeders/ExpenseSeeder.php new file mode 100644 index 00000000..d3bf02b9 --- /dev/null +++ b/database/seeders/ExpenseSeeder.php @@ -0,0 +1,376 @@ +firstOrFail(); + $tenant2 = Tenant::where('slug', 'global')->firstOrFail(); + + $irr = Currency::where('code', 'IRR')->firstOrFail(); + $usd = Currency::where('code', 'USD')->firstOrFail(); + + $tenant1Projects = $tenant1->projects()->orderBy('id')->get(); + $tenant2Projects = $tenant2->projects()->orderBy('id')->get(); + + $tenant1Employees = $tenant1->employees()->orderBy('id')->get(); + $tenant2Employees = $tenant2->employees()->orderBy('id')->get(); + + $admin1 = User::where('email', 'admin@sepideh.ir')->firstOrFail(); + $manager = User::where('email', 'manager@sepideh.ir')->firstOrFail(); + $admin2 = User::where('email', 'admin@global.com')->firstOrFail(); + + // ───────────────────────────────────────────── + // Tenant 1 — Sepideh Expenses (IRR) + // ───────────────────────────────────────────── + + $tenant1Expenses = [ + // Project: ساختمان مسکونی گلستان + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, + 'petty_cash_id' => null, + 'employee_id' => $tenant1Employees[0]->id ?? null, + 'amount' => 2500000000, // 2.5B IRR — میلگرد + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'خرید میلگرد آج از ذوب آهن اصفهان — ۵۰ تن', + 'expense_date' => '2024-05-10', + 'category' => 'materials', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin1->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, + 'petty_cash_id' => null, + 'employee_id' => $tenant1Employees[1]->id ?? null, + 'amount' => 850000000, // 850M IRR — سیمان + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'خرید سیمان تهران — ۲۰۰ تن تیپ ۲ و ۵', + 'expense_date' => '2024-05-25', + 'category' => 'materials', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin1->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 320000000, // 320M IRR — اجاره جرثقیل + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'اجاره جرثقیل ۵۰ تنی برای مدت ۲۰ روز', + 'expense_date' => '2024-06-15', + 'category' => 'equipment', + 'receipt_path' => null, + 'status' => 'pending', + 'approved_by' => null, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 45000000, // 45M IRR — حمل و نقل + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'هزینه حمل مصالح از کارخانه تا محل پروژه — ۳ سفر کامیون', + 'expense_date' => '2024-06-20', + 'category' => 'transport', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $manager->id, + ], + // Project: مجتمع تجاری مروارید + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[2]->id, + 'petty_cash_id' => null, + 'employee_id' => $tenant1Employees[4]->id ?? null, + 'amount' => 1800000000, // 1.8B IRR — چیلر + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'خرید چیلر ۵۰۰ تنی و نصب سیستم تهویه', + 'expense_date' => '2024-07-05', + 'category' => 'equipment', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin1->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[2]->id, + 'petty_cash_id' => null, + 'employee_id' => $tenant1Employees[3]->id ?? null, + 'amount' => 120000000, // 120M IRR — خوراک + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'وعده غذایی کارگران — ۳ ماهه دوم ۱۴۰۳', + 'expense_date' => '2024-07-15', + 'category' => 'food', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $manager->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[2]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 600000000, // 600M IRR — نیروی کار + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'حقوق کارگران ساختمانی پروژه مروارید — تیرماه ۱۴۰۳', + 'expense_date' => '2024-07-22', + 'category' => 'labor', + 'receipt_path' => null, + 'status' => 'pending', + 'approved_by' => null, + ], + // Project: بازسازی بیمارستان شفا + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[4]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 950000000, // 950M IRR — مصالح + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'خرید مصالح بهداشتی بیمارستانی — کفپوش، رنگ ضدعفونی‌کننده و درب‌های اتوماتیک', + 'expense_date' => '2024-06-10', + 'category' => 'materials', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin1->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[4]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 200000000, // 200M IRR — تجهیزات + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'تأمین و نصب ژنراتور اضطراری ۵۰۰ کیلووات', + 'expense_date' => '2024-07-01', + 'category' => 'equipment', + 'receipt_path' => null, + 'status' => 'rejected', + 'approved_by' => $admin1->id, + ], + // Project: استخر و سونا سلامت + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[3]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 350000000, // 350M IRR — مصالح + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'خرید سرامیک و موزاییک استخر — مارک آلفا', + 'expense_date' => '2024-02-15', + 'category' => 'materials', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin1->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[3]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 75000000, // 75M IRR — حمل و نقل + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'هزینه حمل و نصب سیستم فیلتراسیون آب استخر', + 'expense_date' => '2024-03-20', + 'category' => 'transport', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $manager->id, + ], + // Project: پل عابر سپید + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[1]->id, + 'petty_cash_id' => null, + 'employee_id' => $tenant1Employees[2]->id ?? null, + 'amount' => 150000000, // 150M IRR — گمانه‌بوری + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'هزینه گمانه‌بوری و مطالعات ژئوتکنیک پل عابر', + 'expense_date' => '2024-07-10', + 'category' => 'other', + 'receipt_path' => null, + 'status' => 'pending', + 'approved_by' => null, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[1]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 280000000, // 280M IRR — تجهیزات + 'currency_id' => $irr->id, + 'original_amount' => null, + 'description' => 'خرید و نصب داربست و قالب فلزی برای ساخت پایه‌های پل', + 'expense_date' => '2024-07-18', + 'category' => 'equipment', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin1->id, + ], + ]; + + foreach ($tenant1Expenses as $expenseData) { + Expense::create($expenseData); + } + + // ───────────────────────────────────────────── + // Tenant 2 — Global Expenses (USD) + // ───────────────────────────────────────────── + + $tenant2Expenses = [ + // Project: Riverside Tower + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[0]->id, + 'petty_cash_id' => null, + 'employee_id' => $tenant2Employees[0]->id ?? null, + 'amount' => 1250000, // $1.25M — concrete + 'currency_id' => $usd->id, + 'original_amount' => null, + 'description' => 'Ready-mix concrete supply for foundation pouring — 3,000 cubic yards', + 'expense_date' => '2024-04-15', + 'category' => 'materials', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin2->id, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[0]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 340000, // $340K — steel + 'currency_id' => $usd->id, + 'original_amount' => null, + 'description' => 'Structural steel reinforcement bars — Grade 60, 500 tons', + 'expense_date' => '2024-05-20', + 'category' => 'materials', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin2->id, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[0]->id, + 'petty_cash_id' => null, + 'employee_id' => $tenant2Employees[2]->id ?? null, + 'amount' => 85000, // $85K — equipment rental + 'currency_id' => $usd->id, + 'original_amount' => null, + 'description' => 'Tower crane rental — 3 months', + 'expense_date' => '2024-06-01', + 'category' => 'equipment', + 'receipt_path' => null, + 'status' => 'pending', + 'approved_by' => null, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[0]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 22500, // $22.5K — labor + 'currency_id' => $usd->id, + 'original_amount' => null, + 'description' => 'Labor wages for concrete finishing crew — June 2024', + 'expense_date' => '2024-06-30', + 'category' => 'labor', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin2->id, + ], + // Project: Industrial Warehouse Complex + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[1]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 45000, // $45K — site survey + 'currency_id' => $usd->id, + 'original_amount' => null, + 'description' => 'Geotechnical survey and soil testing across the warehouse site', + 'expense_date' => '2024-07-10', + 'category' => 'other', + 'receipt_path' => null, + 'status' => 'pending', + 'approved_by' => null, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[1]->id, + 'petty_cash_id' => null, + 'employee_id' => null, + 'amount' => 15000, // $15K — transport + 'currency_id' => $usd->id, + 'original_amount' => null, + 'description' => 'Transportation of heavy equipment to warehouse site', + 'expense_date' => '2024-07-15', + 'category' => 'transport', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin2->id, + ], + // Project: Greenfield School Campus + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[2]->id, + 'petty_cash_id' => null, + 'employee_id' => $tenant2Employees[4]->id ?? null, + 'amount' => 62000, // $62K — electrical + 'currency_id' => $usd->id, + 'original_amount' => null, + 'description' => 'Electrical wiring, panels and fire alarm system for Building A', + 'expense_date' => '2024-04-10', + 'category' => 'equipment', + 'receipt_path' => null, + 'status' => 'approved', + 'approved_by' => $admin2->id, + ], + ]; + + foreach ($tenant2Expenses as $expenseData) { + Expense::create($expenseData); + } + + $total = count($tenant1Expenses) + count($tenant2Expenses); + $this->command->info("Expenses seeded: {$total} records created."); + } +} diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php new file mode 100644 index 00000000..caf5b799 --- /dev/null +++ b/database/seeders/PermissionSeeder.php @@ -0,0 +1,55 @@ + $permission, 'guard_name' => 'web']); + } + + // Assign all permissions to admin role + $adminRole = Role::where('name', 'admin')->first(); + if ($adminRole) { + $adminRole->syncPermissions(Permission::all()); + } + + // Assign basic permissions to manager role + $managerRole = Role::where('name', 'manager')->first(); + if ($managerRole) { + $managerRole->syncPermissions([ + 'manage projects', + 'manage expenses', + 'manage petty cash', + 'manage employees', + 'manage tasks', + 'manage inventory', + 'manage attachments', + 'view reports', + 'view dashboard', + ]); + } + + $this->command->info('Permissions seeded successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/ProjectSeeder.php b/database/seeders/ProjectSeeder.php new file mode 100644 index 00000000..e8b950af --- /dev/null +++ b/database/seeders/ProjectSeeder.php @@ -0,0 +1,169 @@ +firstOrFail(); + $tenant2 = Tenant::where('slug', 'global')->firstOrFail(); + + $irr = Currency::where('code', 'IRR')->firstOrFail(); + $usd = Currency::where('code', 'USD')->firstOrFail(); + + $tenant1Manager = User::where('email', 'manager@sepideh.ir')->firstOrFail(); + $tenant1Admin = User::where('email', 'admin@sepideh.ir')->firstOrFail(); + $tenant2Admin = User::where('email', 'admin@global.com')->firstOrFail(); + + // ───────────────────────────────────────────── + // Tenant 1 — Sepideh (Persian construction projects) + // ───────────────────────────────────────────── + + $persianProjects = [ + [ + 'tenant_id' => $tenant1->id, + 'name' => 'ساختمان مسکونی گلستان', + 'code' => 'SPD-1403-001', + 'description' => 'ساخت مجتمع مسکونی ۱۲ طبقه شامل ۹۶ واحد در منطقه ۲ تهران', + 'status' => 'in_progress', + 'start_date' => '2024-04-20', + 'end_date' => '2025-10-20', + 'progress' => 45, + 'budget' => 850000000000, // 850 billion IRR + 'default_currency_id' => $irr->id, + 'budget_usd' => 20238095.24, + 'manager_id' => $tenant1Manager->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'name' => 'پل عابر سپید', + 'code' => 'SPD-1403-002', + 'description' => 'ساخت پل عابر پیاده با طول ۴۵ متر و عرض ۵ متر در بزرگراه همت', + 'status' => 'planning', + 'start_date' => '2024-09-01', + 'end_date' => '2025-06-30', + 'progress' => 10, + 'budget' => 120000000000, // 120 billion IRR + 'default_currency_id' => $irr->id, + 'budget_usd' => 2857142.86, + 'manager_id' => $tenant1Admin->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'name' => 'مجتمع تجاری مروارید', + 'code' => 'SPD-1403-003', + 'description' => 'ساخت مجتمع تجاری ۸ طبقه شامل ۱۲۰ واحد تجاری و پارکینگ ۳ طبقه', + 'status' => 'in_progress', + 'start_date' => '2024-01-15', + 'end_date' => '2025-12-31', + 'progress' => 62, + 'budget' => 1200000000000, // 1.2 trillion IRR + 'default_currency_id' => $irr->id, + 'budget_usd' => 28571428.57, + 'manager_id' => $tenant1Manager->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'name' => 'استخر و سونا سلامت', + 'code' => 'SPD-1403-004', + 'description' => 'ساخت مجموعه ورزشی شامل استخر المپیک، سونا خشک و بخار و سالن بدنسازی', + 'status' => 'completed', + 'start_date' => '2023-06-01', + 'end_date' => '2024-05-30', + 'progress' => 100, + 'budget' => 350000000000, // 350 billion IRR + 'default_currency_id' => $irr->id, + 'budget_usd' => 8333333.33, + 'manager_id' => $tenant1Admin->id, + ], + [ + 'tenant_id' => $tenant1->id, + 'name' => 'بازسازی بیمارستان شفا', + 'code' => 'SPD-1403-005', + 'description' => 'بازسازی و نوسازی بخش‌های داخلی بیمارستان شفا شامل اورژانس، ICU و بخش جراحی', + 'status' => 'on_hold', + 'start_date' => '2024-03-01', + 'end_date' => '2025-03-01', + 'progress' => 25, + 'budget' => 500000000000, // 500 billion IRR + 'default_currency_id' => $irr->id, + 'budget_usd' => 11904761.90, + 'manager_id' => $tenant1Manager->id, + ], + ]; + + foreach ($persianProjects as $projectData) { + Project::create($projectData); + } + + // ───────────────────────────────────────────── + // Tenant 2 — Global (English construction projects) + // ───────────────────────────────────────────── + + $englishProjects = [ + [ + 'tenant_id' => $tenant2->id, + 'name' => 'Riverside Tower', + 'code' => 'GLO-2024-001', + 'description' => 'Construction of a 20-story mixed-use tower with retail, office, and residential spaces along the waterfront', + 'status' => 'in_progress', + 'start_date' => '2024-02-01', + 'end_date' => '2026-06-30', + 'progress' => 35, + 'budget' => 45000000, // 45M USD + 'default_currency_id' => $usd->id, + 'budget_usd' => 45000000.00, + 'manager_id' => $tenant2Admin->id, + ], + [ + 'tenant_id' => $tenant2->id, + 'name' => 'Industrial Warehouse Complex', + 'code' => 'GLO-2024-002', + 'description' => 'Building a 50,000 sqm warehouse complex with loading docks and logistics center', + 'status' => 'planning', + 'start_date' => '2024-08-01', + 'end_date' => '2025-04-30', + 'progress' => 5, + 'budget' => 12000000, // 12M USD + 'default_currency_id' => $usd->id, + 'budget_usd' => 12000000.00, + 'manager_id' => $tenant2Admin->id, + ], + [ + 'tenant_id' => $tenant2->id, + 'name' => 'Greenfield School Campus', + 'code' => 'GLO-2024-003', + 'description' => 'Construction of a modern school campus with three buildings, sports facilities, and parking area', + 'status' => 'completed', + 'start_date' => '2023-05-01', + 'end_date' => '2024-07-15', + 'progress' => 100, + 'budget' => 8500000, // 8.5M USD + 'default_currency_id' => $usd->id, + 'budget_usd' => 8500000.00, + 'manager_id' => $tenant2Admin->id, + ], + ]; + + foreach ($englishProjects as $projectData) { + Project::create($projectData); + } + + $total = count($persianProjects) + count($englishProjects); + $this->command->info("Projects seeded: {$total} records created."); + } +} diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php new file mode 100644 index 00000000..de11a1e2 --- /dev/null +++ b/database/seeders/RolePermissionSeeder.php @@ -0,0 +1,216 @@ +getPermissions(); + + foreach ($permissions as $name) { + Permission::firstOrCreate( + ['name' => $name, 'guard_name' => 'web'] + ); + } + + $this->command->info('Permissions seeded: ' . count($permissions) . ' created.'); + + // 2. Create Roles + $roles = ['super-admin', 'admin', 'project-manager', 'accountant', 'employee']; + + // Super-admin role at the platform level (no tenant) + Role::firstOrCreate( + ['name' => 'super-admin', 'guard_name' => 'web', 'tenant_id' => null] + )->syncPermissions($permissions); + + // Tenant-scoped roles + $tenants = Tenant::all(); + + foreach ($tenants as $tenant) { + foreach ($roles as $roleName) { + if ($roleName === 'super-admin') { + continue; + } + + Role::firstOrCreate( + ['name' => $roleName, 'guard_name' => 'web', 'tenant_id' => $tenant->id] + ); + } + } + + // 3. Assign Permissions to Tenant-Scoped Roles + foreach ($tenants as $tenant) { + $this->assignRolePermissions($tenant); + } + + $this->command->info('Roles seeded for ' . $tenants->count() . ' tenants.'); + } + + protected function getPermissions(): array + { + return [ + // Projects + 'projects.view', + 'projects.create', + 'projects.update', + 'projects.delete', + + // Tasks + 'tasks.view', + 'tasks.create', + 'tasks.update', + 'tasks.delete', + 'tasks.update-status', + + // Employees + 'employees.view', + 'employees.create', + 'employees.update', + 'employees.delete', + 'employees.view-financials', + + // Expenses + 'expenses.view', + 'expenses.create', + 'expenses.update', + 'expenses.delete', + 'expenses.approve', + + // Inventory + 'inventory.view', + 'inventory.create', + 'inventory.update', + 'inventory.delete', + + // Letters (Correspondence) + 'letters.view', + 'letters.create', + 'letters.update', + 'letters.delete', + 'letters.manage', + + // Documents + 'documents.view', + 'documents.create', + 'documents.update', + 'documents.delete', + 'documents.manage', + + // Payroll + 'payroll.view', + 'payroll.create', + 'payroll.manage', + + // Reports + 'reports.view', + 'reports.export', + + // Petty Cash + 'petty-cash.view', + 'petty-cash.create', + 'petty-cash.update', + 'petty-cash.delete', + 'petty-cash.approve', + ]; + } + + protected function assignRolePermissions(Tenant $tenant): void + { + // Admin — full access within the tenant + $admin = Role::where('name', 'admin') + ->where('tenant_id', $tenant->id) + ->first(); + + if ($admin) { + $admin->syncPermissions($this->getPermissions()); + } + + // Project Manager + $pm = Role::where('name', 'project-manager') + ->where('tenant_id', $tenant->id) + ->first(); + + if ($pm) { + $pm->syncPermissions([ + 'projects.view', + 'projects.create', + 'projects.update', + 'tasks.view', + 'tasks.create', + 'tasks.update', + 'tasks.delete', + 'tasks.update-status', + 'employees.view', + 'expenses.view', + 'expenses.create', + 'expenses.update', + 'expenses.approve', + 'letters.view', + 'letters.create', + 'letters.update', + 'documents.view', + 'documents.create', + 'documents.update', + 'petty-cash.view', + 'petty-cash.create', + 'reports.view', + 'reports.export', + ]); + } + + // Accountant + $accountant = Role::where('name', 'accountant') + ->where('tenant_id', $tenant->id) + ->first(); + + if ($accountant) { + $accountant->syncPermissions([ + 'projects.view', + 'tasks.view', + 'employees.view', + 'employees.view-financials', + 'expenses.view', + 'expenses.create', + 'expenses.update', + 'expenses.approve', + 'petty-cash.view', + 'petty-cash.create', + 'petty-cash.approve', + 'payroll.view', + 'payroll.create', + 'payroll.manage', + 'letters.view', + 'documents.view', + 'reports.view', + 'reports.export', + ]); + } + + // Employee + $employee = Role::where('name', 'employee') + ->where('tenant_id', $tenant->id) + ->first(); + + if ($employee) { + $employee->syncPermissions([ + 'projects.view', + 'tasks.view', + 'tasks.update-status', + 'expenses.view', + 'expenses.create', + 'letters.view', + 'documents.view', + ]); + } + } +} diff --git a/database/seeders/TaskSeeder.php b/database/seeders/TaskSeeder.php new file mode 100644 index 00000000..897269ad --- /dev/null +++ b/database/seeders/TaskSeeder.php @@ -0,0 +1,247 @@ +firstOrFail(); + $tenant2 = Tenant::where('slug', 'global')->firstOrFail(); + + $tenant1Projects = $tenant1->projects()->orderBy('id')->get(); + $tenant2Projects = $tenant2->projects()->orderBy('id')->get(); + + $manager = User::where('email', 'manager@sepideh.ir')->firstOrFail(); + $admin1 = User::where('email', 'admin@sepideh.ir')->firstOrFail(); + $admin2 = User::where('email', 'admin@global.com')->firstOrFail(); + + // ───────────────────────────────────────────── + // Tenant 1 — Sepideh Tasks + // ───────────────────────────────────────────── + + $tenant1Tasks = [ + // Project: ساختمان مسکونی گلستان + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, + 'title' => 'تأمین مصالح مرحله اسکلت', + 'description' => 'خرید و تحویل میلگرد، سیمان و آجر برای اسکلت طبقات ۵ تا ۸', + 'status' => 'in_progress', + 'priority' => 'high', + 'assignee_id' => $manager->id, + 'due_date' => '2024-08-15', + 'progress' => 60, + 'estimated_hours' => 120.00, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[0]->id, + 'title' => 'نصب قالب‌بندی طبقه ۶', + 'description' => 'قالب‌بندی ستون‌ها و دال طبقه ششم با نظارت مهندس ناظر', + 'status' => 'todo', + 'priority' => 'medium', + 'assignee_id' => $admin1->id, + 'due_date' => '2024-09-01', + 'progress' => 0, + 'estimated_hours' => 80.00, + ], + // Project: پل عابر سپید + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[1]->id, + 'title' => 'مطالعات ژئوتکنیک', + 'description' => 'انجاد گمانه‌های ژئوتکنیک و تهیه گزارش خاک‌شناسی محوطه پروژه', + 'status' => 'in_progress', + 'priority' => 'urgent', + 'assignee_id' => $manager->id, + 'due_date' => '2024-07-30', + 'progress' => 80, + 'estimated_hours' => 200.00, + ], + // Project: مجتمع تجاری مروارید + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[2]->id, + 'title' => 'نصب سیستم تهویه مطبوع', + 'description' => 'نصب چیلر، داکت‌ها و ایرواشر طبقات تجاری', + 'status' => 'in_progress', + 'priority' => 'high', + 'assignee_id' => $manager->id, + 'due_date' => '2024-11-30', + 'progress' => 35, + 'estimated_hours' => 480.00, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[2]->id, + 'title' => 'تأسیسات برقی فاز ۲', + 'description' => 'سیم‌کشی تابلوهای برق اصلی و پنل‌های کنترلی طبقات ۴ تا ۸', + 'status' => 'review', + 'priority' => 'medium', + 'assignee_id' => $admin1->id, + 'due_date' => '2024-08-20', + 'progress' => 90, + 'estimated_hours' => 160.00, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[2]->id, + 'title' => 'نازک‌کاری دیوارهای داخلی', + 'description' => 'گچ‌کاری و نازک‌کاری دیوارهای داخلی واحد‌های تجاری طبقات ۱ تا ۳', + 'status' => 'todo', + 'priority' => 'low', + 'assignee_id' => null, + 'due_date' => '2025-01-15', + 'progress' => 0, + 'estimated_hours' => 320.00, + ], + // Project: استخر و سونا سلامت + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[3]->id, + 'title' => 'تست فشار لوله‌کشی', + 'description' => 'تست هیدرواستاتیک لوله‌های آب سرد و گرم و سیستم فاضلاب', + 'status' => 'done', + 'priority' => 'high', + 'assignee_id' => $admin1->id, + 'due_date' => '2024-04-15', + 'progress' => 100, + 'estimated_hours' => 40.00, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[3]->id, + 'title' => 'تحویل نهایی به کارفرما', + 'description' => 'آماده‌سازی اسناد تحویل، لیست عیوب و رفع آنها و تحویل رسمی', + 'status' => 'done', + 'priority' => 'medium', + 'assignee_id' => $manager->id, + 'due_date' => '2024-05-30', + 'progress' => 100, + 'estimated_hours' => 24.00, + ], + // Project: بازسازی بیمارستان شفا + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[4]->id, + 'title' => 'تخریب دیوارهای داخلی بخش اورژانس', + 'description' => 'تخریب و حمل مصالح دیوارهای غیرسازه‌ای بخش اورژانس فعلی', + 'status' => 'todo', + 'priority' => 'urgent', + 'assignee_id' => $manager->id, + 'due_date' => '2024-09-15', + 'progress' => 0, + 'estimated_hours' => 96.00, + ], + [ + 'tenant_id' => $tenant1->id, + 'project_id' => $tenant1Projects[4]->id, + 'title' => 'تهیه نقشه‌های اجرایی', + 'description' => 'تکمیل نقشه‌های اجرایی معماری، تأسیسات و برق برای بخش ICU', + 'status' => 'in_progress', + 'priority' => 'high', + 'assignee_id' => $admin1->id, + 'due_date' => '2024-08-10', + 'progress' => 50, + 'estimated_hours' => 160.00, + ], + ]; + + foreach ($tenant1Tasks as $taskData) { + Task::create($taskData); + } + + // ───────────────────────────────────────────── + // Tenant 2 — Global Tasks + // ───────────────────────────────────────────── + + $tenant2Tasks = [ + // Project: Riverside Tower + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[0]->id, + 'title' => 'Foundation Pouring — Phase 2', + 'description' => 'Complete concrete pouring for the tower foundation section B including reinforcement inspection', + 'status' => 'in_progress', + 'priority' => 'urgent', + 'assignee_id' => $admin2->id, + 'due_date' => '2024-08-30', + 'progress' => 70, + 'estimated_hours' => 240.00, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[0]->id, + 'title' => 'Structural Steel Erection — Floors 5-8', + 'description' => 'Erect and weld structural steel frames for floors 5 through 8 of the tower', + 'status' => 'todo', + 'priority' => 'high', + 'assignee_id' => $admin2->id, + 'due_date' => '2024-11-15', + 'progress' => 0, + 'estimated_hours' => 360.00, + ], + // Project: Industrial Warehouse Complex + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[1]->id, + 'title' => 'Site Survey & Soil Testing', + 'description' => 'Conduct topographic survey and soil bearing capacity tests across the 50,000 sqm site', + 'status' => 'in_progress', + 'priority' => 'high', + 'assignee_id' => $admin2->id, + 'due_date' => '2024-09-15', + 'progress' => 45, + 'estimated_hours' => 120.00, + ], + // Project: Greenfield School Campus + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[2]->id, + 'title' => 'Final Inspection & Handover', + 'description' => 'Complete final building inspection, fire safety certification, and handover to the school board', + 'status' => 'done', + 'priority' => 'medium', + 'assignee_id' => $admin2->id, + 'due_date' => '2024-07-10', + 'progress' => 100, + 'estimated_hours' => 40.00, + ], + [ + 'tenant_id' => $tenant2->id, + 'project_id' => $tenant2Projects[2]->id, + 'title' => 'Landscaping & Parking Lot Completion', + 'description' => 'Final grading, asphalt paving for parking area, and installation of outdoor lighting and signage', + 'status' => 'review', + 'priority' => 'low', + 'assignee_id' => null, + 'due_date' => '2024-07-15', + 'progress' => 95, + 'estimated_hours' => 80.00, + ], + ]; + + foreach ($tenant2Tasks as $taskData) { + Task::create($taskData); + } + + $total = count($tenant1Tasks) + count($tenant2Tasks); + $this->command->info("Tasks seeded: {$total} records created."); + } +} diff --git a/database/seeders/TenantSeeder.php b/database/seeders/TenantSeeder.php new file mode 100644 index 00000000..3378d238 --- /dev/null +++ b/database/seeders/TenantSeeder.php @@ -0,0 +1,64 @@ +firstOrFail(); + $usd = Currency::where('code', 'USD')->firstOrFail(); + + $tenants = [ + [ + 'name' => 'سازندگی سپید', + 'slug' => 'sepideh', + 'domain' => 'sepideh.Vernova.ir', + 'is_active' => true, + 'default_locale' => 'fa', + 'default_calendar' => 'jalali', + 'base_currency_id' => $irr->id, + 'allow_multi_currency' => true, + 'logo_path' => null, + 'settings' => [ + 'week_start' => 'saturday', + 'working_days' => ['saturday', 'sunday', 'monday', 'tuesday', 'wednesday'], + 'fiscal_year_start' => '1403-01-01', + ], + ], + [ + 'name' => 'Global Construction Co.', + 'slug' => 'global', + 'domain' => 'global.Vernova.ir', + 'is_active' => true, + 'default_locale' => 'en', + 'default_calendar' => 'gregorian', + 'base_currency_id' => $usd->id, + 'allow_multi_currency' => true, + 'logo_path' => null, + 'settings' => [ + 'week_start' => 'monday', + 'working_days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], + 'fiscal_year_start' => '2024-01-01', + ], + ], + ]; + + foreach ($tenants as $tenantData) { + Tenant::create($tenantData); + } + + $this->command->info('Tenants seeded: ' . count($tenants) . ' records created.'); + } +} diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php new file mode 100644 index 00000000..87f05b6a --- /dev/null +++ b/database/seeders/UserSeeder.php @@ -0,0 +1,103 @@ +firstOrFail(); + $tenant2 = Tenant::where('slug', 'global')->firstOrFail(); + + $users = [ + [ + 'tenant_id' => null, + 'name' => 'Super Admin', + 'email' => 'admin@projectra.ir', + 'password' => Hash::make('password'), + 'locale' => 'fa', + 'preferred_calendar' => 'jalali', + 'is_active' => true, + 'email_verified_at' => now(), + 'role' => 'super-admin', + ], + [ + 'tenant_id' => $tenant1->id, + 'name' => 'محمد احمدی', + 'email' => 'admin@sepideh.ir', + 'password' => Hash::make('password'), + 'locale' => 'fa', + 'preferred_calendar' => 'jalali', + 'is_active' => true, + 'email_verified_at' => now(), + 'role' => 'admin', + ], + [ + 'tenant_id' => $tenant1->id, + 'name' => 'علی رضایی', + 'email' => 'manager@sepideh.ir', + 'password' => Hash::make('password'), + 'locale' => 'fa', + 'preferred_calendar' => 'jalali', + 'is_active' => true, + 'email_verified_at' => now(), + 'role' => 'project-manager', + ], + [ + 'tenant_id' => $tenant2->id, + 'name' => 'John Smith', + 'email' => 'admin@global.com', + 'password' => Hash::make('password'), + 'locale' => 'en', + 'preferred_calendar' => 'gregorian', + 'is_active' => true, + 'email_verified_at' => now(), + 'role' => 'admin', + ], + ]; + + foreach ($users as $userData) { + $roleName = $userData['role']; + unset($userData['role']); + + $user = User::create($userData); + + // Assign the Spatie role. For tenant-scoped users, scope the role + // to their tenant; for super-admin, use the global (null tenant) role. + if ($user->tenant_id) { + $role = Role::where('name', $roleName) + ->where('tenant_id', $user->tenant_id) + ->first(); + } else { + $role = Role::where('name', $roleName) + ->whereNull('tenant_id') + ->first(); + } + + if ($role) { + $user->assignRole($role); + } else { + $this->command->warn("Role '{$roleName}' not found for user {$user->email}. Skipping role assignment."); + } + } + + $this->command->info('Users seeded: ' . count($users) . ' records created with roles.'); + } +} diff --git a/move-files.bat b/move-files.bat new file mode 100644 index 00000000..e69de29b diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..21608e70 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2546 @@ +{ + "name": "Vernova", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/forms": "^0.5.7", + "alpinejs": "^3.13", + "autoprefixer": "^10.4", + "axios": "^1.7", + "laravel-vite-plugin": "^1.0", + "postcss": "^8.4", + "tailwindcss": "^3.4", + "vite": "^5.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", + "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/alpinejs": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz", + "integrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz", + "integrity": "sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + } + } +} diff --git a/package.json b/package.json index 4e934caa..48de9a10 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,13 @@ "build": "vite build" }, "devDependencies": { - "axios": "^1.6.4", + "@tailwindcss/forms": "^0.5.7", + "alpinejs": "^3.13", + "autoprefixer": "^10.4", + "axios": "^1.7", "laravel-vite-plugin": "^1.0", + "postcss": "^8.4", + "tailwindcss": "^3.4", "vite": "^5.0" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..49c0612d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/css/jalali-datepicker.css b/public/css/jalali-datepicker.css new file mode 100644 index 00000000..c5dd9cc2 --- /dev/null +++ b/public/css/jalali-datepicker.css @@ -0,0 +1,135 @@ +/* Vernova - Jalali Date Picker Styles */ +.jalali-picker-dropdown { + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 12px; + box-shadow: 0 10px 25px rgba(0,0,0,0.12); + padding: 0; + z-index: 99999; + width: 280px; + font-family: 'Vazirmatn', sans-serif; + direction: rtl; + user-select: none; +} + +.jp-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + background: #14b8a6; + color: #fff; + border-radius: 12px 12px 0 0; +} + +.jp-title { + font-size: 14px; + font-weight: 600; +} + +.jp-nav { + background: rgba(255,255,255,0.2); + border: none; + color: #fff; + width: 28px; + height: 28px; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s; +} + +.jp-nav:hover { + background: rgba(255,255,255,0.35); +} + +.jp-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + padding: 8px 8px 2px; + gap: 2px; +} + +.jp-wd { + text-align: center; + font-size: 11px; + color: #9ca3af; + font-weight: 500; + padding: 4px 0; +} + +.jp-days { + display: grid; + grid-template-columns: repeat(7, 1fr); + padding: 2px 8px 8px; + gap: 2px; +} + +.jp-day { + width: 34px; + height: 34px; + border: none; + background: transparent; + border-radius: 8px; + cursor: pointer; + font-size: 13px; + font-family: 'Vazirmatn', sans-serif; + color: #374151; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + margin: 0 auto; +} + +.jp-day:hover { + background: #f0fdfa; + color: #0d9488; +} + +.jp-day.jp-today { + background: #fef3c7; + color: #92400e; + font-weight: 600; +} + +.jp-day.jp-selected { + background: #14b8a6; + color: #fff; + font-weight: 600; +} + +.jp-day.jp-selected:hover { + background: #0d9488; + color: #fff; +} + +.jp-empty { + width: 34px; + height: 34px; +} + +.jp-footer { + border-top: 1px solid #f3f4f6; + padding: 6px 8px; + text-align: center; +} + +.jp-today-btn { + background: #f0fdfa; + border: 1px solid #ccfbf1; + color: #0d9488; + padding: 4px 16px; + border-radius: 6px; + font-size: 12px; + font-family: 'Vazirmatn', sans-serif; + cursor: pointer; + transition: all 0.15s; +} + +.jp-today-btn:hover { + background: #ccfbf1; +} diff --git a/public/js/jalali-datepicker.js b/public/js/jalali-datepicker.js new file mode 100644 index 00000000..3fea1351 --- /dev/null +++ b/public/js/jalali-datepicker.js @@ -0,0 +1,326 @@ +/** + * Vernova - Lightweight Vanilla JS Jalali Date Picker + * No jQuery, no eval, no external dependencies. CSP-safe. + */ +(function () { + 'use strict'; + + // ─── Jalali ↔ Gregorian Conversion ───────────────────────── + var JalaliConverter = { + gregorianToJalali: function (gy, gm, gd) { + var g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; + var gy2 = (gm > 2) ? (gy + 1) : gy; + var days = 355666 + (365 * gy) + Math.floor((gy2 + 3) / 4) - Math.floor((gy2 + 99) / 100) + Math.floor((gy2 + 399) / 400) + gd + g_d_m[gm - 1]; + var jy = -1595 + (33 * Math.floor(days / 12053)); + days %= 12053; + jy += 4 * Math.floor(days / 1461); + days %= 1461; + if (days > 365) { + jy += Math.floor((days - 1) / 365); + days = (days - 1) % 365; + } + var jm, jd; + if (days < 186) { + jm = 1 + Math.floor(days / 31); + jd = 1 + (days % 31); + } else { + jm = 7 + Math.floor((days - 186) / 30); + jd = 1 + ((days - 186) % 30); + } + return [jy, jm, jd]; + }, + jalaliToGregorian: function (jy, jm, jd) { + var jy2 = jy - 979; + var jm2 = jm - 1; + var jd2 = jd - 1; + var days = 365 * jy2 + Math.floor(jy2 / 33) * 8 + Math.floor((jy2 % 33 + 3) / 4) + 78 + jd2 + ((jm2 < 7) ? (jm2 * 31) : ((jm2 * 30) + 6)); + var gy = 1600 + 400 * Math.floor(days / 146097); + days %= 146097; + if (days > 36524) { + gy += 100 * Math.floor(--days / 36524); + days %= 36524; + if (days >= 365) days++; + } + gy += 4 * Math.floor(days / 1461); + days %= 1461; + if (days > 365) { + gy += Math.floor((days - 1) / 365); + days = (days - 1) % 365; + } + var gd = days + 1; + var sal_a = [0, 31, ((gy % 4 === 0 && gy % 100 !== 0) || (gy % 400 === 0)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + var gm; + for (gm = 0; gm < 13 && gd > sal_a[gm]; gm++) gd -= sal_a[gm]; + return [gy, gm, gd]; + }, + isJalaliLeap: function (jy) { + var breaks = [1, 5, 9, 13, 17, 22, 26, 30]; + var r = jy % 33; + return breaks.indexOf(r) !== -1; + }, + jalaliMonthDays: function (jy, jm) { + if (jm <= 6) return 31; + if (jm <= 11) return 30; + return this.isJalaliLeap(jy) ? 30 : 29; + }, + gregorianMonthDays: function (gy, gm) { + var days = [0, 31, ((gy % 4 === 0 && gy % 100 !== 0) || (gy % 400 === 0)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + return days[gm]; + } + }; + + // ─── Jalali Month/Day Names ──────────────────────────────── + var jalaliMonths = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']; + var jalaliWeekDaysShort = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج']; + var jalaliWeekDaysLong = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه']; + + // ─── Pad helper ──────────────────────────────────────────── + function pad(n) { return n < 10 ? '0' + n : '' + n; } + + // ─── Date Picker Class ───────────────────────────────────── + function JalaliDatePicker(inputEl, hiddenEl) { + this.inputEl = inputEl; + this.hiddenEl = hiddenEl; + this.container = null; + this.isOpen = false; + this.currentYear = 1404; + this.currentMonth = 1; + this.selectedYear = null; + this.selectedMonth = null; + this.selectedDay = null; + + this._init(); + } + + JalaliDatePicker.prototype = { + _init: function () { + // Parse initial value + var val = this.inputEl.value; + if (val && /^\d{4}\/\d{1,2}\/\d{1,2}$/.test(val)) { + var parts = val.split('/'); + this.selectedYear = this.currentYear = parseInt(parts[0]); + this.selectedMonth = this.currentMonth = parseInt(parts[1]); + this.selectedDay = parseInt(parts[2]); + } else { + // Default to today + var now = new Date(); + var today = JalaliConverter.gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate()); + this.currentYear = today[0]; + this.currentMonth = today[1]; + } + + // Create container + this.container = document.createElement('div'); + this.container.className = 'jalali-picker-dropdown'; + this.container.style.display = 'none'; + document.body.appendChild(this.container); + + // Bind events + var self = this; + this.inputEl.addEventListener('focus', function (e) { self.open(); }); + this.inputEl.addEventListener('click', function (e) { e.stopPropagation(); }); + this.container.addEventListener('click', function (e) { e.stopPropagation(); }); + + // Close on outside click + document.addEventListener('click', function (e) { + if (self.isOpen && e.target !== self.inputEl && !self.container.contains(e.target)) { + self.close(); + } + }); + + // Handle manual input + this.inputEl.addEventListener('blur', function () { + self._syncFromInput(); + }); + + // Handle keyboard + this.inputEl.addEventListener('keydown', function (e) { + if (e.key === 'Escape') self.close(); + if (e.key === 'Enter') { self._syncFromInput(); self.close(); } + }); + }, + + _syncFromInput: function () { + var val = this.inputEl.value.trim(); + if (/^\d{4}\/\d{1,2}\/\d{1,2}$/.test(val)) { + var parts = val.split('/'); + var jy = parseInt(parts[0]), jm = parseInt(parts[1]), jd = parseInt(parts[2]); + var maxDay = JalaliConverter.jalaliMonthDays(jy, jm); + if (jd > maxDay) jd = maxDay; + this.selectedYear = jy; + this.selectedMonth = jm; + this.selectedDay = jd; + this.inputEl.value = pad(jy) + '/' + pad(jm) + '/' + pad(jd); + this._updateHidden(); + } else if (val === '') { + this.selectedYear = null; + this.selectedMonth = null; + this.selectedDay = null; + if (this.hiddenEl) this.hiddenEl.value = ''; + } + }, + + _updateHidden: function () { + if (this.selectedYear && this.selectedMonth && this.selectedDay) { + var g = JalaliConverter.jalaliToGregorian(this.selectedYear, this.selectedMonth, this.selectedDay); + var gregStr = pad(g[0]) + '-' + pad(g[1]) + '-' + pad(g[2]); + if (this.hiddenEl) this.hiddenEl.value = gregStr; + } + }, + + open: function () { + this._render(); + this.container.style.display = 'block'; + this.isOpen = true; + this._position(); + }, + + close: function () { + this.container.style.display = 'none'; + this.isOpen = false; + }, + + _position: function () { + var rect = this.inputEl.getBoundingClientRect(); + var scrollTop = window.pageYOffset || document.documentElement.scrollTop; + var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + var pickerHeight = this.container.offsetHeight; + var spaceBelow = window.innerHeight - rect.bottom; + var spaceAbove = rect.top; + + this.container.style.position = 'absolute'; + if (spaceBelow < pickerHeight && spaceAbove > spaceBelow) { + this.container.style.top = (rect.top + scrollTop - pickerHeight - 4) + 'px'; + } else { + this.container.style.top = (rect.bottom + scrollTop + 4) + 'px'; + } + // RTL: align to right edge + this.container.style.left = (rect.right + scrollLeft - this.container.offsetWidth) + 'px'; + }, + + _render: function () { + var self = this; + var y = this.currentYear; + var m = this.currentMonth; + + // First day of month in Gregorian + var firstGreg = JalaliConverter.jalaliToGregorian(y, m, 1); + var firstDate = new Date(firstGreg[0], firstGreg[1] - 1, firstGreg[2]); + // Day of week: 0=Sun, adjust to Saturday=0 for Jalali + var dow = firstDate.getDay(); // 0=Sun + var jalaliDow = (dow + 1) % 7; // 0=Sat + + var daysInMonth = JalaliConverter.jalaliMonthDays(y, m); + + // Today + var now = new Date(); + var todayJ = JalaliConverter.gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate()); + + var html = ''; + + // Week day headers + html += ''; + + // Days grid + html += ''; + + // Footer + html += ''; + + this.container.innerHTML = html; + + // Bind navigation + this.container.querySelector('.jp-prev').addEventListener('click', function () { self._navigate(-1); }); + this.container.querySelector('.jp-next').addEventListener('click', function () { self._navigate(1); }); + this.container.querySelector('.jp-today-btn').addEventListener('click', function () { self._goToday(); }); + + // Bind day clicks + var dayBtns = this.container.querySelectorAll('.jp-day:not(.jp-empty)'); + for (var i = 0; i < dayBtns.length; i++) { + dayBtns[i].addEventListener('click', function () { + var day = parseInt(this.getAttribute('data-day')); + self._selectDay(self.currentYear, self.currentMonth, day); + }); + } + }, + + _navigate: function (dir) { + this.currentMonth += dir; + if (this.currentMonth > 12) { + this.currentMonth = 1; + this.currentYear++; + } else if (this.currentMonth < 1) { + this.currentMonth = 12; + this.currentYear--; + } + this._render(); + this._position(); + }, + + _selectDay: function (y, m, d) { + this.selectedYear = y; + this.selectedMonth = m; + this.selectedDay = d; + this.inputEl.value = pad(y) + '/' + pad(m) + '/' + pad(d); + this._updateHidden(); + this.close(); + // Trigger change event + var event = new Event('change', { bubbles: true }); + this.inputEl.dispatchEvent(event); + if (this.hiddenEl) { + this.hiddenEl.dispatchEvent(new Event('change', { bubbles: true })); + } + }, + + _goToday: function () { + var now = new Date(); + var today = JalaliConverter.gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate()); + this._selectDay(today[0], today[1], today[2]); + } + }; + + // ─── Initialize all date pickers ─────────────────────────── + function initPickers() { + var inputs = document.querySelectorAll('.jalali-datepicker-input'); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if (input._jalaliPickerInitialized) continue; + + var hiddenId = input.getAttribute('data-gregorian-target'); + var hidden = hiddenId ? document.getElementById(hiddenId) : null; + + new JalaliDatePicker(input, hidden); + input._jalaliPickerInitialized = true; + } + } + + // Run on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPickers); + } else { + initPickers(); + } + +})(); diff --git a/resources/css/app.css b/resources/css/app.css index e69de29b..9d0a8678 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -0,0 +1,50 @@ +@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + [dir="rtl"] { + font-family: 'Vazirmatn', sans-serif; + } + [dir="ltr"] { + font-family: 'Inter', sans-serif; + } +} + +@layer components { + .sidebar-link { + @apply w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors; + } + .sidebar-link-active { + @apply bg-sidebar-primary text-sidebar-primary-foreground font-medium; + } + .sidebar-link-inactive { + @apply text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground; + } + .kpi-card { + @apply relative overflow-hidden rounded-xl border bg-card text-card-foreground; + } + .status-badge { + @apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium; + } +} + +@layer utilities { + .ps-custom { padding-inline-start: 1rem; } + .pe-custom { padding-inline-end: 1rem; } + .ms-custom { margin-inline-start: 0.5rem; } + .me-custom { margin-inline-end: 0.5rem; } + .border-s-custom { border-inline-start-width: 1px; } + .border-e-custom { border-inline-end-width: 1px; } +} + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #94a3b8; } + +@media print { + .no-print { display: none !important; } +} diff --git a/resources/js/app.js b/resources/js/app.js index e59d6a0a..4b7b9e91 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,62 @@ -import './bootstrap'; +import Alpine from 'alpinejs'; +import axios from 'axios'; + +window.Alpine = Alpine; + +Alpine.store('app', { + sidebarOpen: window.innerWidth >= 768, + darkMode: localStorage.getItem('darkMode') === 'true', + mobileMenuOpen: false, + + toggleSidebar() { + this.sidebarOpen = !this.sidebarOpen; + }, + toggleDarkMode() { + this.darkMode = !this.darkMode; + localStorage.setItem('darkMode', this.darkMode); + document.documentElement.classList.toggle('dark', this.darkMode); + }, + toggleMobileMenu() { + this.mobileMenuOpen = !this.mobileMenuOpen; + }, +}); + +Alpine.data('dropdown', () => ({ + open: false, + toggle() { this.open = !this.open; }, + close() { this.open = false; }, +})); + +Alpine.data('confirmDialog', () => ({ + showing: false, + message: '', + onConfirm: null, + show(message, onConfirm) { + this.message = message; + this.onConfirm = onConfirm; + this.showing = true; + }, + confirm() { + if (this.onConfirm) this.onConfirm(); + this.showing = false; + }, + cancel() { this.showing = false; }, +})); + +Alpine.data('toast', () => ({ + toasts: [], + add(message, type = 'success', duration = 3000) { + const id = Date.now(); + this.toasts.push({ id, message, type }); + setTimeout(() => { this.toasts = this.toasts.filter(t => t.id !== id); }, duration); + }, +})); + +axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; +axios.defaults.headers.common['Accept'] = 'application/json'; +axios.defaults.headers.common['X-Locale'] = document.documentElement.lang || 'fa'; + +const token = document.querySelector('meta[name="csrf-token"]'); +if (token) axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; + +Alpine.start(); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js deleted file mode 100644 index 5f1390b0..00000000 --- a/resources/js/bootstrap.js +++ /dev/null @@ -1,4 +0,0 @@ -import axios from 'axios'; -window.axios = axios; - -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/resources/lang/en.json b/resources/lang/en.json new file mode 100644 index 00000000..13986329 --- /dev/null +++ b/resources/lang/en.json @@ -0,0 +1,761 @@ +{ + "common.appName": "Projectra", + "common.projectManagement": "Project Management", + "common.save": "Save", + "common.save_changes": "Save Changes", + "common.cancel": "Cancel", + "common.delete": "Delete", + "common.edit": "Edit", + "common.create": "Create", + "common.search": "Search...", + "common.filter": "Filter", + "common.export": "Export", + "common.exportExcel": "Export Excel", + "common.exportPdf": "Export PDF", + "common.confirm": "Confirm", + "common.reject": "Reject", + "common.approve": "Approve", + "common.back": "Back", + "common.actions": "Actions", + "common.status": "Status", + "common.all": "All", + "common.noResults": "No results found", + "common.loading": "Loading...", + "common.yes": "Yes", + "common.no": "No", + "common.areYouSure": "Are you sure?", + "common.from": "From", + "common.to": "To", + "common.select": "Select", + "common.reset": "Reset", + "common.all_rights_reserved": "All rights reserved", + "common.comingSoon": "Coming Soon", + "common.success": "Operation completed successfully", + "common.error": "An error occurred", + "common.view": "View", + "common.view_all": "View All", + "common.details": "Details", + "common.no_data": "No data available", + "common.total": "Total", + "common.count": "Count", + "common.date": "Date", + "common.amount": "Amount", + "common.description": "Description", + "common.notes": "Notes", + "common.project": "Project", + "common.currency": "Currency", + "common.upload": "Upload File", + "common.choose_file": "Choose File", + "common.no_file": "No file selected", + "common.confirm_delete": "Are you sure you want to delete?", + "common.good_morning": "Good Morning", + "common.good_afternoon": "Good Afternoon", + "common.good_evening": "Good Evening", + "common.none": "None", + "common.no_records": "No records found", + "common.today": "Today", + + "nav.dashboard": "Dashboard", + "nav.projects": "Projects", + "nav.tasks": "Tasks", + "nav.expenses": "Expenses", + "nav.employees": "Employees", + "nav.warehouse": "Warehouse", + "nav.pettyCash": "Petty Cash", + "nav.letters": "Correspondence", + "nav.documents": "Documents", + "nav.reports": "Reports", + "nav.payroll": "Payroll", + "nav.settings": "Settings", + "nav.help": "Help", + + "auth.login": "Login", + "auth.logout": "Logout", + "auth.email": "Email", + "auth.password": "Password", + "auth.rememberMe": "Remember Me", + "auth.forgotPassword": "Forgot Password?", + "auth.invalidCredentials": "Invalid email or password", + + "dashboard.title": "Dashboard", + "dashboard.active_projects": "Active Projects", + "dashboard.total_budget": "Total Budget", + "dashboard.pending_tasks": "Pending Tasks", + "dashboard.active_employees": "Active Employees", + "dashboard.addProject": "Add Project", + "dashboard.addExpense": "Add Expense", + "dashboard.addEmployee": "Add Employee", + "dashboard.viewReports": "View Reports", + "dashboard.recent_activity": "Recent Activity", + "dashboard.vs_last_month": "vs Last Month", + "dashboard.budgetGrowth": "Budget Growth", + "dashboard.urgent": "Urgent", + "dashboard.newHires": "New Hires", + "dashboard.my_tasks": "My Tasks", + "dashboard.no_activity": "No activities recorded", + "dashboard.no_tasks": "No tasks", + "dashboard.no_projects": "No projects", + "dashboard.projects_progress": "Projects Progress", + "dashboard.calendar": "Calendar", + "dashboard.numbers": "Numbers", + "dashboard.currency": "Currency", + "dashboard.today": "Today", + "dashboard.short": "Short", + "dashboard.calendar_system": "Calendar System", + "dashboard.calendarType": "Calendar Type", + "dashboard.integer": "Integer", + "dashboard.decimal": "Decimal", + "dashboard.percentage": "Percentage", + "dashboard.number_format": "Number Format", + "dashboard.numeralSystem": "Numeral System", + "dashboard.conversionSample": "Conversion Sample", + "dashboard.currency_format": "Currency Format", + "dashboard.localization_info": "Localization Info", + "dashboard.i18nFeatures": "Internationalization Features", + "dashboard.i18nDesc": "Live preview of date, number and currency conversion based on locale settings", + + "project.create": "Create Project", + "project.new": "New Project", + "project.edit": "Edit Project", + "project.name": "Project Name", + "project.code": "Project Code", + "project.description": "Description", + "project.status": "Status", + "project.startDate": "Start Date", + "project.endDate": "End Date", + "project.start_date": "Start Date", + "project.end_date": "End Date", + "project.budget": "Budget", + "project.manager": "Project Manager", + "project.currency": "Currency", + "project.members": "Project Members", + "project.location": "Project Location", + "project.progress": "Progress", + "project.activeProjects": "Active Projects", + "project.no_projects": "No projects found", + "project.status_planning": "Planning", + "project.status_in_progress": "In Progress", + "project.status_on_hold": "On Hold", + "project.status_completed": "Completed", + "project.status_cancelled": "Cancelled", + "project.status_active": "Active", + "project.created_successfully": "Project created successfully", + "project.updated_successfully": "Project updated successfully", + "project.deleted_successfully": "Project deleted successfully", + "project.status_updated": "Project status updated", + + "task.create": "Create Task", + "task.new": "New Task", + "task.edit": "Edit Task", + "task.title": "Title", + "task.description": "Description", + "task.priority": "Priority", + "task.dueDate": "Due Date", + "task.assignee": "Assignee", + "task.estimatedHours": "Estimated Hours", + "task.status": "Status", + "task.start_date": "Start Date", + "task.completedAt": "Completed At", + "task.activityLog": "Activity Log", + "task.logCreated": "Task created", + "task.logStatusChanged": "Status changed", + "task.parent_task": "Parent Task", + "task.progress": "Progress", + "task.duration_days": "Duration (days)", + "task.sub_tasks": "Sub Tasks", + "task.status_updated": "Status updated", + "task.status_pending": "Pending", + "task.status_in_progress": "In Progress", + "task.status_review": "In Review", + "task.status_done": "Done", + "task.status_cancelled": "Cancelled", + "task.priority_low": "Low", + "task.priority_medium": "Medium", + "task.priority_high": "High", + "task.priority_urgent": "Urgent", + "task.kanban": "Kanban View", + "task.listView": "List View", + "task.gantt_view": "Gantt Chart", + "task.gantt": "Gantt", + "task.no_tasks": "No tasks", + "task.blocked": "Blocked", + "task.can_start": "Can Start", + "task.dependencies": "Dependencies", + "task.add_dependency": "Add Dependency", + "task.remove_dependency": "Remove Dependency", + "task.dependency_type": "Dependency Type", + "task.finish_to_start": "Finish to Start", + "task.start_to_start": "Start to Start", + "task.finish_to_finish": "Finish to Finish", + "task.start_to_finish": "Start to Finish", + "task.blocking_tasks": "Blocking Tasks", + "task.dependent_tasks": "Dependent Tasks", + "task.circular_dependency_error": "Circular dependency — cannot add", + "task.dependency_added": "Dependency added successfully", + "task.dependency_removed": "Dependency removed", + "task.created_successfully": "Task created successfully", + "task.updated_successfully": "Task updated successfully", + "task.deleted_successfully": "Task deleted successfully", + + "employee.create": "Add Employee", + "employee.new": "New Employee", + "employee.edit": "Edit Employee", + "employee.firstName": "First Name", + "employee.lastName": "Last Name", + "employee.nationalCode": "National Code", + "employee.phone": "Phone", + "employee.jobTitle": "Job Title", + "employee.contractType": "Contract Type", + "employee.hireDate": "Hire Date", + "employee.baseSalary": "Base Salary", + "employee.salary_currency": "Salary Currency", + "employee.bankAccount": "Bank Account", + "employee.status": "Status", + "employee.status_active": "Active", + "employee.status_inactive": "Inactive", + "employee.status_on_leave": "On Leave", + "employee.project": "Project", + "employee.workLogs": "Work Logs", + "employee.financials": "Financials", + "employee.totalHours": "Total Hours", + "employee.overtimeHours": "Overtime Hours", + "employee.hoursWorked": "Hours Worked", + "employee.date": "Date", + "employee.description": "Description", + "employee.totalPaid": "Total Paid", + "employee.totalUnpaid": "Total Unpaid", + "employee.salary": "Salary", + "employee.bonus": "Bonus", + "employee.deduction": "Deduction", + "employee.overtime": "Overtime", + "employee.advance": "Advance", + "employee.reimbursement": "Reimbursement", + "employee.isPaid": "Paid", + "employee.isUnpaid": "Unpaid", + "employee.type": "Type", + "employee.amount": "Amount", + "employee.effectiveDate": "Effective Date", + "employee.termination_date": "Termination Date", + "employee.noWorkLogs": "No work logs recorded", + "employee.noFinancials": "No financial records", + "employee.contract_full_time": "Full-time", + "employee.contract_part_time": "Part-time", + "employee.contract_contractor": "Contractor", + "employee.contract_intern": "Intern", + "employee.contract_consultant": "Consultant", + "employee.created_successfully": "Employee added successfully", + "employee.updated_successfully": "Employee updated successfully", + "employee.deleted_successfully": "Employee deleted successfully", + + "expense.create": "Create Expense", + "expense.create_expense": "Create Expense", + "expense.edit_expense": "Edit Expense", + "expense.new": "New Expense", + "expense.edit": "Edit Expense", + "expense.expenses": "Expenses", + "expense.title": "Title", + "expense.amount": "Amount", + "expense.original_amount": "Original Amount", + "expense.category": "Category", + "expense.date": "Expense Date", + "expense.description": "Description", + "expense.receipt": "Receipt/Attachment", + "expense.current_receipt": "Current Receipt", + "expense.base_currency": "Base Currency", + "expense.currency_conversion_note": "Amount is automatically converted to base currency", + "expense.invoice_number": "Invoice Number", + "expense.currency": "Currency", + "expense.project": "Project", + "expense.petty_cash": "Related Petty Cash", + "expense.requested_by": "Requested By", + "expense.approved_by": "Approved By", + "expense.approval_date": "Approval Date", + "expense.from_date": "From Date", + "expense.to_date": "To Date", + "expense.confirm_reject": "Are you sure you want to reject this expense?", + "expense.status_pending": "Pending", + "expense.status_approved": "Approved", + "expense.status_rejected": "Rejected", + "expense.status_paid": "Paid", + "expense.category_materials": "Materials", + "expense.category_labor": "Labor", + "expense.category_equipment": "Equipment", + "expense.category_transport": "Transport", + "expense.category_services": "Services", + "expense.category_food": "Food", + "expense.category_accommodation": "Accommodation", + "expense.category_other": "Other", + "expense.created_successfully": "Expense created successfully", + "expense.updated_successfully": "Expense updated successfully", + "expense.deleted_successfully": "Expense deleted successfully", + "expense.approved_successfully": "Expense approved", + "expense.rejected_successfully": "Expense rejected", + "expense.settled_successfully": "Petty cash settled", + "expense.no_expenses": "No expenses recorded", + + "inventory.inventory": "Inventory", + "inventory.dashboard": "Dashboard", + "inventory.items": "Inventory Items", + "inventory.item_name": "Item Name", + "inventory.item_code": "Item Code", + "inventory.unit": "Unit", + "inventory.category": "Category", + "inventory.category_placeholder": "e.g., Materials, Tools, Hardware...", + "inventory.warehouses": "Warehouses", + "inventory.warehouse_name": "Warehouse Name", + "inventory.location": "Location", + "inventory.active": "Active", + "inventory.inactive": "Inactive", + "inventory.quantity": "Quantity", + "inventory.unit_price": "Unit Price", + "inventory.total_price": "Total Price", + "inventory.reference": "Reference Number", + "inventory.notes": "Notes", + "inventory.description": "Description", + "inventory.current_stock": "Current Stock", + "inventory.min_stock": "Min Stock", + "inventory.min_stock_help": "Alert when stock falls below this amount", + "inventory.low_stock": "Low Stock", + "inventory.low_stock_items": "Low Stock Items", + "inventory.low_stock_alert": "Stock Alert", + "inventory.active_items": "Active Items", + "inventory.all_stock_ok": "All items have sufficient stock", + "inventory.recent_transactions": "Recent Transactions", + "inventory.items_count": "Items Count", + "inventory.transactions": "Transactions", + "inventory.transaction_type": "Transaction Type", + "inventory.type_in": "Stock In", + "inventory.type_out": "Stock Out", + "inventory.type_transfer": "Transfer", + "inventory.type_return": "Return", + "inventory.type_adjustment": "Adjustment", + "inventory.stock_in": "Stock In", + "inventory.stock_out": "Stock Out", + "inventory.transfer": "Transfer Between Warehouses", + "inventory.from_warehouse": "From Warehouse", + "inventory.to_warehouse": "To Warehouse", + "inventory.current_stock_source": "Source Stock", + "inventory.from_date": "From Date", + "inventory.to_date": "To Date", + "inventory.warehouse": "Warehouse", + "inventory.item": "Item", + "inventory.project": "Project", + "inventory.create_warehouse": "Add New Warehouse", + "inventory.create_item": "Add New Item", + "inventory.create_transaction": "New Transaction", + "inventory.new_transaction": "New Transaction", + "inventory.edit_item": "Edit Item", + "inventory.edit_warehouse": "Edit Warehouse", + "inventory.stock_in_success": "Stock in recorded successfully", + "inventory.stock_out_success": "Stock out recorded successfully", + "inventory.transfer_success": "Transfer completed successfully", + "inventory.insufficient_stock": "Insufficient stock", + "inventory.insufficient_stock_source": "Source warehouse has insufficient stock", + "inventory.no_items": "No items recorded", + "inventory.no_warehouses": "No warehouses recorded", + "inventory.no_transactions": "No transactions recorded", + "inventory.warehouse_created": "Warehouse created successfully", + "inventory.item_created": "Item created successfully", + "inventory.transaction_created": "Transaction recorded successfully", + "inventory.item_updated": "Item updated successfully", + "inventory.item_deleted": "Item deleted successfully", + "inventory.item_has_transactions": "This item has transactions and cannot be deleted", + "inventory.warehouse_updated": "Warehouse updated successfully", + "inventory.warehouse_deleted": "Warehouse deleted successfully", + "inventory.warehouse_has_transactions": "This warehouse has transactions and cannot be deleted", + + "pettyCash.pettyCash": "Petty Cash", + "pettyCash.title": "Title", + "pettyCash.amount": "Amount", + "pettyCash.original_amount": "Original Amount", + "pettyCash.remaining": "Remaining", + "pettyCash.description": "Description", + "pettyCash.project": "Project", + "pettyCash.currency": "Currency", + "pettyCash.base_currency": "Base Currency", + "pettyCash.currency_conversion_note": "Amount is automatically converted to base currency", + "pettyCash.request_date": "Request Date", + "pettyCash.approval_date": "Approval Date", + "pettyCash.requested_by": "Requested By", + "pettyCash.approved_by": "Approved By", + "pettyCash.status": "Status", + "pettyCash.pending": "Pending", + "pettyCash.approved": "Approved", + "pettyCash.rejected": "Rejected", + "pettyCash.settled": "Settled", + "pettyCash.create": "Create Petty Cash", + "pettyCash.edit": "Edit Petty Cash", + "pettyCash.settle": "Settle", + "pettyCash.receipt": "Receipt/Attachment", + "pettyCash.current_receipt": "Current Receipt", + "pettyCash.receipt_help": "Image or PDF file (max 10MB)", + "pettyCash.expenses": "Related Expenses", + "pettyCash.no_petty_cashes": "No petty cash records", + "pettyCash.created_successfully": "Petty cash created successfully", + "pettyCash.updated_successfully": "Petty cash updated successfully", + "pettyCash.deleted_successfully": "Petty cash deleted successfully", + "pettyCash.approved_successfully": "Petty cash approved", + "pettyCash.rejected_successfully": "Petty cash rejected", + "pettyCash.settled_successfully": "Petty cash settled", + + "letters.letters": "Correspondence", + "letters.title": "Correspondence", + "letters.create_title": "New Letter", + "letters.edit_title": "Edit Letter", + "letters.show_title": "View Letter", + "letters.list_title": "Letters List", + "letters.type": "Letter Type", + "letters.type_incoming": "Incoming", + "letters.type_outgoing": "Outgoing", + "letters.incoming": "Incoming Letter", + "letters.outgoing": "Outgoing Letter", + "letters.new_incoming": "New Incoming", + "letters.new_outgoing": "New Outgoing", + "letters.status": "Status", + "letters.status_draft": "Draft", + "letters.status_sent": "Sent", + "letters.status_received": "Received", + "letters.status_archived": "Archived", + "letters.draft": "Draft", + "letters.sent": "Sent", + "letters.received": "Received", + "letters.archived": "Archived", + "letters.subject": "Subject", + "letters.letter_number": "Letter Number", + "letters.letter_date": "Letter Date", + "letters.sender": "Sender", + "letters.recipient": "Recipient", + "letters.content": "Content", + "letters.description": "Description", + "letters.reference_number": "Reference Number", + "letters.priority": "Priority", + "letters.category": "Category", + "letters.project": "Project", + "letters.parent_letter": "Related Letter", + "letters.attachments": "Attachments", + "letters.has_attachment": "Has Attachment", + "letters.normal": "Normal", + "letters.urgent": "Urgent", + "letters.very_urgent": "Very Urgent", + "letters.from_date": "From Date", + "letters.to_date": "To Date", + "letters.create": "New Letter", + "letters.edit": "Edit", + "letters.delete": "Delete", + "letters.archive": "Archive", + "letters.download": "Download", + "letters.upload_attachment": "Upload Attachment", + "letters.delete_attachment": "Delete Attachment", + "letters.save": "Save", + "letters.cancel": "Cancel", + "letters.back": "Back", + "letters.search": "Search", + "letters.filter": "Filter", + "letters.print": "Print", + "letters.send": "Send", + "letters.view_letter": "View Letter", + "letters.no_letters": "No letters found", + "letters.created_successfully": "Letter created successfully", + "letters.updated_successfully": "Letter updated successfully", + "letters.deleted_successfully": "Letter deleted successfully", + "letters.archived_successfully": "Letter archived successfully", + + "letter.letters": "Correspondence", + "letter.create_letter": "New Letter", + "letter.edit_letter": "Edit Letter", + "letter.incoming": "Incoming", + "letter.outgoing": "Outgoing", + "letter.type_incoming": "Incoming", + "letter.type_outgoing": "Outgoing", + "letter.subject": "Subject", + "letter.content": "Content", + "letter.sender": "Sender", + "letter.recipient": "Recipient", + "letter.letter_date": "Letter Date", + "letter.reference_number": "Reference Number", + "letter.type": "Letter Type", + "letter.project": "Related Project", + "letter.parent_letter": "Original Letter", + "letter.parent_letter_help": "The letter this letter is a reply to", + "letter.status": "Status", + "letter.status_draft": "Draft", + "letter.status_sent": "Sent", + "letter.status_received": "Received", + "letter.status_archived": "Archived", + "letter.attachments": "Attachments", + "letter.attachments_help": "Max 5 files (image, PDF — max 10MB)", + "letter.current_attachments": "Current Attachments", + "letter.replies": "Replies", + "letter.people": "Correspondence Info", + "letter.no_letters": "No letters found", + "letter.no_replies": "No replies yet", + "letter.archive": "Archive", + "letter.download": "Download", + "letter.search_placeholder": "Search subject, reference number, sender...", + "letter.confirm_archive": "Are you sure you want to archive this letter?", + "letter.file_name": "File Name", + "letter.file_size": "File Size", + "letter.remove_attachment": "Remove Attachment", + "letter.created_successfully": "Letter created successfully", + "letter.updated_successfully": "Letter updated successfully", + "letter.deleted_successfully": "Letter deleted successfully", + "letter.archived_successfully": "Letter archived", + + "document.documents": "Documents", + "document.upload_document": "Upload Document", + "document.edit_document": "Edit Document", + "document.title": "Title", + "document.description": "Description", + "document.project": "Project", + "document.file": "File", + "document.file_type": "File Type", + "document.uploaded_by": "Uploaded By", + "document.upload_date": "Upload Date", + "document.current_file": "Current File", + "document.max_file_size_help": "Max 50MB", + "document.no_documents": "No documents uploaded", + "document.download": "Download", + "document.type_pdf": "PDF", + "document.type_image": "Image", + "document.type_word": "Word", + "document.type_excel": "Excel", + "document.type_text": "Text", + "document.type_other": "Other", + "document.file_not_found": "File not found", + "document.created_successfully": "Document uploaded successfully", + "document.updated_successfully": "Document updated successfully", + "document.deleted_successfully": "Document deleted successfully", + + "report.reports": "Reports", + "report.financial_report": "Financial Report", + "report.employee_worklog_report": "Worklog Report", + "report.inventory_report": "Inventory Report", + "report.payroll_report": "Payroll Report", + "report.financial_description": "Financial report of projects including budget, expenses and balance", + "report.worklog_description": "Employee work hours report by project", + "report.inventory_description": "Inventory stock report and shortage alerts", + "report.payroll_description": "Salary, bonus and deductions report", + "report.project_code": "Project Code", + "report.project_name": "Project Name", + "report.budget": "Budget", + "report.total_expenses": "Total Expenses", + "report.total_petty_cash": "Total Petty Cash", + "report.remaining": "Remaining", + "report.utilization_percent": "Utilization %", + "report.manager": "Project Manager", + "report.employee_name": "Employee Name", + "report.job_title": "Job Title", + "report.project": "Project", + "report.date": "Date", + "report.hours_worked": "Hours Worked", + "report.overtime_hours": "Overtime Hours", + "report.task": "Task", + "report.item_code": "Item Code", + "report.item_name": "Item Name", + "report.warehouse": "Warehouse", + "report.total_in": "Total In", + "report.total_out": "Total Out", + "report.current_stock": "Current Stock", + "report.min_quantity": "Min Quantity", + "report.unit_price": "Unit Price", + "report.stock_value": "Stock Value", + "report.status": "Status", + "report.low_stock": "Low Stock", + "report.ok": "OK", + "report.contract_type": "Contract Type", + "report.salary": "Salary", + "report.bonus": "Bonus", + "report.overtime": "Overtime", + "report.deduction": "Deduction", + "report.other": "Other", + "report.net_pay": "Net Pay", + "report.total_paid": "Total Paid", + "report.total_unpaid": "Total Unpaid", + "report.employee_count": "Employee Count", + "report.date_from": "From Date", + "report.date_to": "To Date", + "report.no_data": "No data available to display", + "report.low_stock_alerts": "Low Stock Alerts", + "report.total_inventory_value": "Total Inventory Value", + "report.total_items": "Total Items", + "report.grand_totals": "Grand Totals", + + "reports.reports": "Reports", + "reports.financial": "Financial Report", + "reports.financial_description": "Financial report of projects including budget, expenses and balance", + "reports.employee_worklog": "Worklog Report", + "reports.employee_worklog_description": "Employee work hours report by project", + "reports.inventory": "Inventory Report", + "reports.inventory_description": "Inventory stock report and shortage alerts", + "reports.payroll": "Payroll Report", + "reports.payroll_description": "Salary, bonus and deductions report", + "reports.total_expenses": "Total Expenses", + "reports.approved_amount": "Approved Amount", + "reports.pending_amount": "Pending Amount", + "reports.export_coming_soon": "Export coming soon", + "reports.detailed_view_coming_soon": "Detailed view coming soon", + + "payroll.payroll": "Payroll", + "payroll.calculate_payroll": "Calculate Payroll", + "payroll.final_settlement": "Final Settlement", + "payroll.total_salaries": "Total Salaries", + "payroll.total_paid": "Total Paid", + "payroll.total_unpaid": "Total Unpaid", + "payroll.total_deductions": "Total Deductions", + "payroll.month": "Month", + "payroll.year": "Year", + "payroll.employee": "Employee", + "payroll.select_employee": "Select Employee", + "payroll.type": "Type", + "payroll.amount": "Amount", + "payroll.currency": "Currency", + "payroll.date": "Date", + "payroll.effective_date": "Effective Date", + "payroll.is_paid": "Paid", + "payroll.pay": "Pay", + "payroll.pay_bulk": "Bulk Pay", + "payroll.settle_all": "Settle All", + "payroll.settlement": "Settlement", + "payroll.new_entry": "New Entry", + "payroll.load": "Load", + "payroll.show": "Show", + "payroll.base_salary": "Base Salary", + "payroll.existing_records": "Existing Records", + "payroll.all_settled": "All Settled", + "payroll.unpaid_count": "Unpaid Count", + "payroll.confirm_settlement": "Are you sure you want to settle all outstanding payments?", + "payroll.type_salary": "Salary", + "payroll.type_bonus": "Bonus", + "payroll.type_deduction": "Deduction", + "payroll.type_advance": "Advance", + "payroll.type_overtime": "Overtime", + "payroll.type_allowance": "Allowance", + "payroll.no_records": "No records found", + "payroll.settlement_coming_soon": "Full settlement coming soon", + "payroll.detailed_view_coming_soon": "Detailed view coming soon", + "payroll.record_created_successfully": "Financial record created successfully", + "payroll.created_successfully": "Financial record created successfully", + "payroll.marked_as_paid": "Payment recorded successfully", + "payroll.already_paid": "This record is already paid", + "payroll.bulk_paid_successfully": "Bulk payments recorded successfully", + "payroll.settlement_processed": "Settlement processed successfully", + "payroll.settlement_processed_successfully": "Settlement processed successfully", + "payroll.total_gross": "Total Gross", + "payroll.total_net": "Net Pay", + "payroll.unpaid_employees": "Unpaid Employees", + "payroll.payment_status": "Payment Status", + "payroll.paid_percent": "paid", + "payroll.financial_records": "Financial Records", + "payroll.confirm_bulk_pay": "Are you sure you want to bulk pay selected items?", + "payroll.confirm_pay": "Are you sure you want to mark this as paid?", + "payroll.paid": "Paid", + "payroll.unpaid": "Unpaid", + "payroll.manual_entry": "Manual Entry", + "payroll.auto_calculate": "Auto Calculate", + "payroll.auto_calculate_btn": "Calculate & Create", + "payroll.auto_calculate_help_title": "Auto Calculate Payroll", + "payroll.auto_calculate_help": "Based on each employee's base salary, a salary record will be created for the selected month. If salary for the current month already exists, that employee will be skipped.", + "payroll.auto_salary_description": "Salary for month :month year :year", + "payroll.auto_overtime_description": "Overtime :hours hours", + "payroll.auto_calculated": ":created records created, :skipped skipped (already exist)", + "payroll.fill_base_salary": "Base Salary", + "payroll.base_salary_short": "Base", + "payroll.include_worklogs": "Calculate overtime from work logs", + "payroll.include_worklogs_help": "Overtime hours from recorded work logs will be calculated and converted to overtime pay", + "payroll.select_employees": "Select Employees", + "payroll.notes": "Notes", + "payroll.payroll_year": "Payroll Year :year", + "payroll.no_records_year": "No records found for this year", + "payroll.settlement_type": "Settlement Type", + "payroll.settlement_resignation": "Resignation", + "payroll.settlement_termination": "Termination", + "payroll.settlement_end_of_contract": "End of Contract", + "payroll.settlement_date": "Settlement Date", + "payroll.last_working_date": "Last Working Date", + "payroll.settlement_preview": "Settlement Preview", + "payroll.preview": "Preview", + "payroll.process_settlement": "Process Settlement", + "payroll.settlement_warning_title": "Attention!", + "payroll.settlement_warning": "By processing the settlement, all unpaid records will be marked as paid and the employee status will change to 'Terminated'. This action cannot be undone.", + "payroll.total_payable": "Total Payable", + "payroll.net_payable": "Net Payable", + "payroll.months_worked": "Months Worked", + "payroll.no_unpaid_records": "No unpaid records", + "payroll.cannot_delete_paid": "Paid records cannot be deleted", + "payroll.deleted_successfully": "Record deleted successfully", + + "employee.contract_daily": "Daily Wage", + "employee.status_terminated": "Terminated", + + "calendar.jalali": "Jalali", + "calendar.gregorian": "Gregorian", + "calendar.hijri": "Hijri", + + "currency.IRR": "Rial", + "currency.USD": "Dollar", + "currency.EUR": "Euro", + "currency.AED": "Dirham", + "currency.code_irr": "Rial", + "currency.code_usd": "Dollar", + "currency.code_eur": "Euro", + "currency.code_aed": "Dirham", + + "attachment.attachments": "Attachments", + "attachment.add_file": "Add File", + "attachment.additional_files": "Additional Attachment Files", + "attachment.max_files_help": "Max 5 files (image, PDF, Word — max 10MB each)", + "attachment.no_attachments": "No attachments", + "attachment.download": "Download", + "attachment.uploaded_successfully": "File uploaded successfully", + "attachment.deleted_successfully": "Attachment deleted successfully", + "attachment.upload_failed": "File upload failed", + "attachment.delete_failed": "Failed to delete attachment", + "attachment.invalid_model_type": "Invalid model type", + "attachment.model_not_found": "Model not found", + + "settings.settings": "Settings", + "settings.organization": "Organization", + "settings.users": "Users", + "settings.currencies": "Currencies", + "settings.org_name": "Organization Name", + "settings.domain": "Domain", + "settings.base_currency": "Base Currency", + "settings.default_locale": "Default Language", + "settings.default_calendar": "Default Calendar", + "settings.allow_multi_currency": "Allow Multi-Currency", + "settings.logo": "Logo", + "settings.logo_help": "Allowed formats: JPEG, PNG, SVG — max 512KB", + "settings.organization_updated": "Organization settings updated successfully", + "settings.add_user": "Add User", + "settings.edit_user": "Edit User", + "settings.user_name": "Name", + "settings.user_email": "Email", + "settings.user_role": "Role", + "settings.role_admin": "System Admin", + "settings.role_manager": "Project Manager", + "settings.role_accountant": "Accountant", + "settings.role_employee": "Employee", + "settings.active": "Active", + "settings.inactive": "Inactive", + "settings.activate": "Activate", + "settings.deactivate": "Deactivate", + "settings.confirm": "Confirm", + "settings.password_help": "Leave blank if you don't want to change it", + "settings.user_created": "User created successfully", + "settings.user_updated": "User updated successfully", + "settings.user_status_updated": "User status updated", + "settings.no_users": "No users found", + "settings.add_currency": "Add Currency", + "settings.edit_currency": "Edit Currency", + "settings.currency_code": "Currency Code", + "settings.currency_name": "Currency Name", + "settings.currency_symbol": "Symbol", + "settings.exchange_rate": "Exchange Rate to USD", + "settings.exchange_rate_help": "Conversion rate of one unit of this currency to US Dollar", + "settings.decimal_places": "Decimal Places", + "settings.currency_created": "Currency created successfully", + "settings.currency_updated": "Currency updated successfully", + "settings.currency_status_updated": "Currency status updated", + "settings.no_currencies": "No currencies found", + "settings.full_settings_coming_soon": "Full settings page coming soon" +} diff --git a/resources/lang/fa.json b/resources/lang/fa.json new file mode 100644 index 00000000..fc4b8cee --- /dev/null +++ b/resources/lang/fa.json @@ -0,0 +1,826 @@ +{ + "common.appName": "ورنوا", + "common.projectManagement": "مدیریت پروژه", + "common.save": "ذخیره", + "common.save_changes": "ذخیره تغییرات", + "common.cancel": "انصراف", + "common.delete": "حذف", + "common.edit": "ویرایش", + "common.create": "ایجاد", + "common.search": "جستجو...", + "common.filter": "فیلتر", + "common.export": "خروجی", + "common.exportExcel": "خروجی اکسل", + "common.exportPdf": "خروجی PDF", + "common.confirm": "تأیید", + "common.reject": "رد", + "common.approve": "تأیید", + "common.back": "بازگشت", + "common.actions": "عملیات", + "common.status": "وضعیت", + "common.all": "همه", + "common.noResults": "نتیجه‌ای یافت نشد", + "common.loading": "در حال بارگذاری...", + "common.yes": "بله", + "common.no": "خیر", + "common.areYouSure": "آیا مطمئن هستید؟", + "common.from": "از", + "common.to": "تا", + "common.select": "انتخاب کنید", + "common.reset": "بازنشانی", + "common.all_rights_reserved": "تمامی حقوق محفوظ است", + "common.comingSoon": "به‌زودی", + "common.success": "عملیات با موفقیت انجام شد", + "common.error": "خطایی رخ داد", + "common.view": "مشاهده", + "common.view_all": "مشاهده همه", + "common.details": "جزئیات", + "common.no_data": "داده‌ای موجود نیست", + "common.total": "مجموع", + "common.count": "تعداد", + "common.date": "تاریخ", + "common.amount": "مبلغ", + "common.description": "شرح", + "common.notes": "توضیحات", + "common.project": "پروژه", + "common.currency": "ارز", + "common.upload": "آپلود فایل", + "common.choose_file": "انتخاب فایل", + "common.no_file": "فایلی انتخاب نشده", + "common.confirm_delete": "آیا از حذف مطمئن هستید؟", + "common.good_morning": "صبح بخیر", + "common.good_afternoon": "ظهر بخیر", + "common.good_evening": "عصر بخیر", + "common.none": "هیچ‌کدام", + "common.no_records": "رکوردی یافت نشد", + "common.today": "امروز", + + "nav.dashboard": "داشبورد", + "nav.projects": "پروژه‌ها", + "nav.tasks": "وظایف", + "nav.expenses": "هزینه‌ها", + "nav.employees": "نیروها", + "nav.warehouse": "انبار", + "nav.pettyCash": "تنخواه", + "nav.letters": "نامه‌نگاری", + "nav.documents": "مستندات", + "nav.reports": "گزارش‌ها", + "nav.payroll": "حقوق و دستمزد", + "nav.settings": "تنظیمات", + "nav.help": "راهنما", + + "auth.login": "ورود", + "auth.logout": "خروج", + "auth.email": "ایمیل", + "auth.password": "رمز عبور", + "auth.rememberMe": "مرا به خاطر بسپار", + "auth.forgotPassword": "رمز عبور را فراموش کرده‌اید؟", + "auth.invalidCredentials": "ایمیل یا رمز عبور اشتباه است", + + "dashboard.title": "داشبورد", + "dashboard.active_projects": "پروژه‌های فعال", + "dashboard.total_budget": "بودجه کل", + "dashboard.pending_tasks": "وظایف در انتظار", + "dashboard.active_employees": "نیروهای فعال", + "dashboard.addProject": "افزودن پروژه", + "dashboard.addExpense": "ثبت هزینه", + "dashboard.addEmployee": "افزودن نیرو", + "dashboard.viewReports": "مشاهده گزارش‌ها", + "dashboard.recent_activity": "فعالیت‌های اخیر", + "dashboard.vs_last_month": "نسبت به ماه قبل", + "dashboard.budgetGrowth": "رشد بودجه", + "dashboard.urgent": "فوری", + "dashboard.newHires": "نیروی جدید", + "dashboard.my_tasks": "وظایف من", + "dashboard.no_activity": "فعالیتی ثبت نشده است", + "dashboard.no_tasks": "وظیفه‌ای نیست", + "dashboard.no_projects": "پروژه‌ای نیست", + "dashboard.projects_progress": "پیشرفت پروژه‌ها", + "dashboard.calendar": "تقویم", + "dashboard.numbers": "اعداد", + "dashboard.currency": "ارز", + "dashboard.today": "امروز", + "dashboard.short": "کوتاه", + "dashboard.calendar_system": "سیستم تقویم", + "dashboard.calendarType": "نوع تقویم", + "dashboard.integer": "عدد صحیح", + "dashboard.decimal": "عدد اعشاری", + "dashboard.percentage": "درصد", + "dashboard.number_format": "فرمت اعداد", + "dashboard.numeralSystem": "سیستم رقم", + "dashboard.conversionSample": "تبدیل نمونه", + "dashboard.currency_format": "فرمت ارز", + "dashboard.localization_info": "اطلاعات بومی‌سازی", + "dashboard.i18nFeatures": "قابلیت‌های بین‌المللی‌سازی", + "dashboard.i18nDesc": "نمایش زنده تبدیل تاریخ، عدد و ارز بر اساس تنظیمات منطقه‌ای", + + "project.create": "ایجاد پروژه", + "project.new": "پروژه جدید", + "project.edit": "ویرایش پروژه", + "project.name": "نام پروژه", + "project.code": "کد پروژه", + "project.description": "توضیحات", + "project.status": "وضعیت", + "project.startDate": "تاریخ شروع", + "project.endDate": "تاریخ پایان", + "project.start_date": "تاریخ شروع", + "project.end_date": "تاریخ پایان", + "project.budget": "بودجه", + "project.manager": "مدیر پروژه", + "project.currency": "ارز", + "project.members": "اعضای پروژه", + "project.location": "محل پروژه", + "project.progress": "پیشرفت", + "project.activeProjects": "پروژه‌های فعال", + "project.no_projects": "پروژه‌ای ثبت نشده است", + "project.status_planning": "برنامه‌ریزی", + "project.status_in_progress": "در حال اجرا", + "project.status_on_hold": "متوقف", + "project.status_completed": "تکمیل‌شده", + "project.status_cancelled": "لغو‌شده", + "project.status_active": "فعال", + "project.created_successfully": "پروژه با موفقیت ایجاد شد", + "project.updated_successfully": "پروژه با موفقیت بروزرسانی شد", + "project.deleted_successfully": "پروژه با موفقیت حذف شد", + "project.status_updated": "وضعیت پروژه بروزرسانی شد", + + "task.create": "ایجاد وظیفه", + "task.new": "وظیفه جدید", + "task.edit": "ویرایش وظیفه", + "task.title": "عنوان", + "task.description": "توضیحات", + "task.priority": "اولویت", + "task.dueDate": "مهلت", + "task.assignee": "مسئول", + "task.estimatedHours": "ساعت تخمینی", + "task.status": "وضعیت", + "task.start_date": "تاریخ شروع", + "task.completedAt": "تاریخ تکمیل", + "task.activityLog": "تاریخچه تغییرات", + "task.logCreated": "وظیفه ایجاد شد", + "task.logStatusChanged": "تغییر وضعیت", + "task.parent_task": "وظیفه والد", + "task.progress": "پیشرفت", + "task.duration_days": "مدت (روز)", + "task.sub_tasks": "زیروظایف", + "task.status_updated": "وضعیت بروزرسانی شد", + "task.status_pending": "در انتظار", + "task.status_in_progress": "در حال انجام", + "task.status_review": "در بررسی", + "task.status_done": "انجام‌شده", + "task.status_cancelled": "لغو‌شده", + "task.priority_low": "کم", + "task.priority_medium": "متوسط", + "task.priority_high": "زیاد", + "task.priority_urgent": "فوری", + "task.kanban": "نمایش کانبان", + "task.listView": "نمایش لیست", + "task.gantt_view": "نمودار گانت", + "task.gantt": "گانت", + "task.no_tasks": "وظیفه‌ای نیست", + "task.blocked": "مسدود", + "task.can_start": "قابل شروع", + "task.dependencies": "وابستگی‌ها", + "task.add_dependency": "افزودن وابستگی", + "task.remove_dependency": "حذف وابستگی", + "task.dependency_type": "نوع وابستگی", + "task.finish_to_start": "پایان به شروع", + "task.start_to_start": "شروع به شروع", + "task.finish_to_finish": "پایان به پایان", + "task.start_to_finish": "شروع به پایان", + "task.blocking_tasks": "وظایف مسدودکننده", + "task.dependent_tasks": "وظایف وابسته", + "task.circular_dependency_error": "وابستگی چرخشی — امکان ثبت نیست", + "task.dependency_added": "وابستگی با موفقیت ثبت شد", + "task.dependency_removed": "وابستگی حذف شد", + "task.created_successfully": "وظیفه با موفقیت ایجاد شد", + "task.updated_successfully": "وظیفه با موفقیت بروزرسانی شد", + "task.deleted_successfully": "وظیفه با موفقیت حذف شد", + + "employee.create": "افزودن نیرو", + "employee.new": "نیروی جدید", + "employee.edit": "ویرایش نیرو", + "employee.firstName": "نام", + "employee.lastName": "نام خانوادگی", + "employee.nationalCode": "کد ملی", + "employee.phone": "تلفن", + "employee.jobTitle": "عنوان شغلی", + "employee.contractType": "نوع قرارداد", + "employee.hireDate": "تاریخ استخدام", + "employee.baseSalary": "حقوق پایه", + "employee.salary_currency": "ارز حقوق", + "employee.bankAccount": "شماره حساب", + "employee.status": "وضعیت", + "employee.status_active": "فعال", + "employee.status_inactive": "غیرفعال", + "employee.status_on_leave": "مرخصی", + "employee.project": "پروژه", + "employee.workLogs": "کارکرد", + "employee.financials": "مالی", + "employee.totalHours": "جمع ساعات", + "employee.overtimeHours": "ساعت اضافه‌کار", + "employee.hoursWorked": "ساعت کار", + "employee.date": "تاریخ", + "employee.description": "شرح", + "employee.totalPaid": "جمع پرداختی", + "employee.totalUnpaid": "جمع پرداخت‌نشده", + "employee.salary": "حقوق", + "employee.bonus": "پاداش", + "employee.deduction": "کسورات", + "employee.overtime": "اضافه‌کار", + "employee.advance": "مساعده", + "employee.reimbursement": "بازپرداخت", + "employee.isPaid": "پرداخت‌شده", + "employee.isUnpaid": "پرداخت‌نشده", + "employee.type": "نوع", + "employee.amount": "مبلغ", + "employee.effectiveDate": "تاریخ اجرا", + "employee.termination_date": "تاریخ تسویه", + "employee.noWorkLogs": "کارکردی ثبت نشده است", + "employee.noFinancials": "رکورد مالی ثبت نشده است", + "employee.contract_full_time": "تمام‌وقت", + "employee.contract_part_time": "پاره‌وقت", + "employee.contract_contractor": "پیمانکار", + "employee.contract_intern": "کارآموز", + "employee.contract_consultant": "مشاور", + "employee.created_successfully": "نیرو با موفقیت اضافه شد", + "employee.updated_successfully": "نیرو با موفقیت بروزرسانی شد", + "employee.deleted_successfully": "نیرو با موفقیت حذف شد", + + "expense.create": "ثبت هزینه", + "expense.create_expense": "ثبت هزینه", + "expense.edit_expense": "ویرایش هزینه", + "expense.new": "هزینه جدید", + "expense.edit": "ویرایش هزینه", + "expense.expenses": "هزینه‌ها", + "expense.title": "عنوان", + "expense.amount": "مبلغ", + "expense.original_amount": "مبلغ اصلی", + "expense.category": "دسته‌بندی", + "expense.date": "تاریخ هزینه", + "expense.description": "شرح هزینه", + "expense.receipt": "رسید/پیوست", + "expense.current_receipt": "رسید فعلی", + "expense.base_currency": "ارز پایه", + "expense.currency_conversion_note": "مبلغ به صورت خودکار به ارز پایه تبدیل می‌شود", + "expense.invoice_number": "شماره فاکتور", + "expense.currency": "ارز", + "expense.project": "پروژه", + "expense.petty_cash": "تنخواه مربوطه", + "expense.requested_by": "درخواست‌کننده", + "expense.approved_by": "تأییدکننده", + "expense.approval_date": "تاریخ تأیید", + "expense.from_date": "از تاریخ", + "expense.to_date": "تا تاریخ", + "expense.confirm_reject": "آیا از رد این هزینه مطمئن هستید؟", + "expense.status_pending": "در انتظار", + "expense.status_approved": "تأییدشده", + "expense.status_rejected": "ردشده", + "expense.status_paid": "پرداخت‌شده", + "expense.category_materials": "مصالح", + "expense.category_labor": "نیروی کار", + "expense.category_equipment": "تجهیزات", + "expense.category_transport": "حمل‌ونقل", + "expense.category_services": "خدمات", + "expense.category_food": "خوراک", + "expense.category_accommodation": "اسکان", + "expense.category_other": "سایر", + "expense.created_successfully": "هزینه با موفقیت ثبت شد", + "expense.updated_successfully": "هزینه با موفقیت بروزرسانی شد", + "expense.deleted_successfully": "هزینه با موفقیت حذف شد", + "expense.approved_successfully": "هزینه تأیید شد", + "expense.rejected_successfully": "هزینه رد شد", + "expense.settled_successfully": "تنخواه تسویه شد", + "expense.no_expenses": "هزینه‌ای ثبت نشده است", + + "inventory.inventory": "انبار", + "inventory.dashboard": "داشبورد", + "inventory.items": "اقلام انبار", + "inventory.item_name": "نام قلم", + "inventory.item_code": "کد قلم", + "inventory.unit": "واحد", + "inventory.category": "دسته‌بندی", + "inventory.category_placeholder": "مثال: مصالح، ابزار، یراق‌آلات،...", + "inventory.warehouses": "انبارها", + "inventory.warehouse_name": "نام انبار", + "inventory.location": "محل", + "inventory.active": "فعال", + "inventory.inactive": "غیرفعال", + "inventory.quantity": "تعداد", + "inventory.unit_price": "قیمت واحد", + "inventory.total_price": "مبلغ کل", + "inventory.reference": "شماره مرجع", + "inventory.notes": "توضیحات", + "inventory.description": "توضیحات", + "inventory.current_stock": "موجودی فعلی", + "inventory.min_stock": "حداقل موجودی", + "inventory.min_stock_help": "هشدار وقتی موجودی کمتر از این مقدار شود", + "inventory.low_stock": "موجودی کم", + "inventory.low_stock_items": "اقلام کم‌موجودی", + "inventory.low_stock_alert": "هشدار موجودی", + "inventory.active_items": "اقلام فعال", + "inventory.all_stock_ok": "همه اقلام موجودی کافی دارند", + "inventory.recent_transactions": "آخرین تراکنش‌ها", + "inventory.items_count": "تعداد اقلام", + "inventory.transactions": "رسید و حواله", + "inventory.transaction_type": "نوع حواله", + "inventory.type_in": "رسید", + "inventory.type_out": "حواله", + "inventory.type_transfer": "انتقال", + "inventory.type_return": "برگشت", + "inventory.type_adjustment": "تعدیل", + "inventory.stock_in": "ثبت رسید", + "inventory.stock_out": "ثبت حواله", + "inventory.transfer": "انتقال بین انبارها", + "inventory.from_warehouse": "انبار مبدأ", + "inventory.to_warehouse": "انبار مقصد", + "inventory.current_stock_source": "موجودی مبدأ", + "inventory.from_date": "از تاریخ", + "inventory.to_date": "تا تاریخ", + "inventory.warehouse": "انبار", + "inventory.item": "قلم", + "inventory.project": "پروژه", + "inventory.create_warehouse": "افزودن انبار جدید", + "inventory.create_item": "افزودن قلم جدید", + "inventory.create_transaction": "حواله جدید", + "inventory.new_transaction": "ثبت رسید/حواله", + "inventory.edit_item": "ویرایش قلم", + "inventory.edit_warehouse": "ویرایش انبار", + "inventory.stock_in_success": "رسید با موفقیت ثبت شد", + "inventory.stock_out_success": "حواله با موفقیت ثبت شد", + "inventory.transfer_success": "انتقال با موفقیت انجام شد", + "inventory.insufficient_stock": "موجودی کافی نیست", + "inventory.insufficient_stock_source": "موجودی انبار مبدأ کافی نیست", + "inventory.no_items": "قلمی ثبت نشده است", + "inventory.no_warehouses": "انباری ثبت نشده است", + "inventory.no_transactions": "تراکنشی ثبت نشده است", + "inventory.warehouse_created": "انبار با موفقیت ایجاد شد", + "inventory.item_created": "قلم با موفقیت ایجاد شد", + "inventory.transaction_created": "حواله با موفقیت ثبت شد", + "inventory.item_updated": "قلم با موفقیت بروزرسانی شد", + "inventory.item_deleted": "قلم با موفقیت حذف شد", + "inventory.item_has_transactions": "این قلم دارای تراکنش است و قابل حذف نیست", + "inventory.warehouse_updated": "انبار با موفقیت بروزرسانی شد", + "inventory.warehouse_deleted": "انبار با موفقیت حذف شد", + "inventory.warehouse_has_transactions": "این انبار دارای تراکنش است و قابل حذف نیست", + + "pettyCash.pettyCash": "تنخواه", + "pettyCash.title": "عنوان", + "pettyCash.amount": "مبلغ", + "pettyCash.original_amount": "مبلغ اصلی", + "pettyCash.remaining": "مانده", + "pettyCash.description": "شرح", + "pettyCash.project": "پروژه", + "pettyCash.currency": "ارز", + "pettyCash.base_currency": "ارز پایه", + "pettyCash.currency_conversion_note": "مبلغ به صورت خودکار به ارز پایه تبدیل می‌شود", + "pettyCash.request_date": "تاریخ درخواست", + "pettyCash.approval_date": "تاریخ تأیید", + "pettyCash.requested_by": "درخواست‌کننده", + "pettyCash.approved_by": "تأییدکننده", + "pettyCash.status": "وضعیت", + "pettyCash.pending": "در انتظار", + "pettyCash.approved": "تأییدشده", + "pettyCash.rejected": "ردشده", + "pettyCash.settled": "تسویه‌شده", + "pettyCash.create": "ثبت تنخواه", + "pettyCash.edit": "ویرایش تنخواه", + "pettyCash.settle": "تسویه", + "pettyCash.receipt": "رسید/پیوست", + "pettyCash.current_receipt": "رسید فعلی", + "pettyCash.receipt_help": "فایل تصویر یا PDF (حداکثر ۱۰ مگابایت)", + "pettyCash.expenses": "هزینه‌های مرتبط", + "pettyCash.no_petty_cashes": "تنخواهی ثبت نشده است", + "pettyCash.created_successfully": "تنخواه با موفقیت ثبت شد", + "pettyCash.updated_successfully": "تنخواه با موفقیت بروزرسانی شد", + "pettyCash.deleted_successfully": "تنخواه با موفقیت حذف شد", + "pettyCash.approved_successfully": "تنخواه تأیید شد", + "pettyCash.rejected_successfully": "تنخواه رد شد", + "pettyCash.settled_successfully": "تنخواه تسویه شد", + + "letters.letters": "نامه نگاری", + "letters.title": "نامه نگاری", + "letters.create_title": "ثبت نامه جدید", + "letters.edit_title": "ویرایش نامه", + "letters.show_title": "مشاهده نامه", + "letters.list_title": "لیست نامه‌ها", + "letters.type": "نوع نامه", + "letters.type_incoming": "دریافتی", + "letters.type_outgoing": "ارسالی", + "letters.incoming": "نامه دریافتی", + "letters.outgoing": "نامه ارسالی", + "letters.new_incoming": "نامه دریافتی جدید", + "letters.new_outgoing": "نامه ارسالی جدید", + "letters.status": "وضعیت", + "letters.status_draft": "پیش‌نویس", + "letters.status_sent": "ارسال شده", + "letters.status_received": "دریافت شده", + "letters.status_archived": "بایگانی شده", + "letters.draft": "پیش‌نویس", + "letters.sent": "ارسال شده", + "letters.received": "دریافت شده", + "letters.archived": "بایگانی شده", + "letters.subject": "موضوع", + "letters.letter_number": "شماره نامه", + "letters.letter_date": "تاریخ نامه", + "letters.sender": "فرستنده", + "letters.recipient": "گیرنده", + "letters.content": "متن نامه", + "letters.description": "توضیحات", + "letters.reference_number": "شماره مرجع", + "letters.priority": "اولویت", + "letters.category": "دسته‌بندی", + "letters.project": "پروژه", + "letters.parent_letter": "نامه مرتبط", + "letters.attachments": "پیوست‌ها", + "letters.has_attachment": "دارای پیوست", + "letters.normal": "عادی", + "letters.urgent": "فوری", + "letters.very_urgent": "خیلی فوری", + "letters.from_date": "از تاریخ", + "letters.to_date": "تا تاریخ", + "letters.create": "ثبت نامه جدید", + "letters.edit": "ویرایش", + "letters.delete": "حذف", + "letters.archive": "بایگانی", + "letters.download": "دانلود", + "letters.upload_attachment": "آپلود پیوست", + "letters.delete_attachment": "حذف پیوست", + "letters.save": "ذخیره", + "letters.cancel": "انصراف", + "letters.back": "بازگشت", + "letters.search": "جستجو", + "letters.filter": "فیلتر", + "letters.print": "چاپ", + "letters.send": "ارسال", + "letters.view_letter": "مشاهده نامه", + "letters.no_letters": "هیچ نامه‌ای یافت نشد", + "letters.created_successfully": "نامه با موفقیت ثبت شد", + "letters.updated_successfully": "نامه با موفقیت بروزرسانی شد", + "letters.deleted_successfully": "نامه با موفقیت حذف شد", + "letters.archived_successfully": "نامه با موفقیت بایگانی شد", + + "letter.letters": "نامه‌نگاری", + "letter.create_letter": "ثبت نامه جدید", + "letter.edit_letter": "ویرایش نامه", + "letter.incoming": "وارده", + "letter.outgoing": "صادره", + "letter.type_incoming": "وارده", + "letter.type_outgoing": "صادره", + "letter.subject": "موضوع", + "letter.content": "متن نامه", + "letter.sender": "فرستنده", + "letter.recipient": "گیرنده", + "letter.letter_date": "تاریخ نامه", + "letter.reference_number": "شماره مرجع", + "letter.type": "نوع نامه", + "letter.project": "پروژه مرتبط", + "letter.parent_letter": "نامه اصلی", + "letter.parent_letter_help": "نامه‌ای که این نامه پاسخ آن است", + "letter.status": "وضعیت", + "letter.status_draft": "پیش‌نویس", + "letter.status_sent": "ارسال‌شده", + "letter.status_received": "دریافت‌شده", + "letter.status_archived": "بایگانی‌شده", + "letter.attachments": "پیوست‌ها", + "letter.attachments_help": "حداکثر ۵ فایل (تصویر، PDF - حداکثر ۱۰ مگابایت)", + "letter.current_attachments": "پیوست‌های فعلی", + "letter.replies": "پاسخ‌ها", + "letter.people": "اطلاعات مراسله", + "letter.no_letters": "نامه‌ای ثبت نشده است", + "letter.no_replies": "پاسخی ثبت نشده است", + "letter.archive": "بایگانی", + "letter.download": "دانلود", + "letter.search_placeholder": "جستجو در موضوع، شماره مرجع، فرستنده...", + "letter.confirm_archive": "آیا از بایگانی این نامه مطمئن هستید؟", + "letter.file_name": "نام فایل", + "letter.file_size": "اندازه فایل", + "letter.remove_attachment": "حذف پیوست", + "letter.created_successfully": "نامه با موفقیت ثبت شد", + "letter.updated_successfully": "نامه با موفقیت بروزرسانی شد", + "letter.deleted_successfully": "نامه با موفقیت حذف شد", + "letter.archived_successfully": "نامه بایگانی شد", + + "document.documents": "مستندات", + "document.upload_document": "آپلود مستند", + "document.edit_document": "ویرایش مستند", + "document.title": "عنوان", + "document.description": "شرح", + "document.project": "پروژه", + "document.file": "فایل", + "document.file_type": "نوع فایل", + "document.uploaded_by": "آپلودکننده", + "document.upload_date": "تاریخ آپلود", + "document.current_file": "فایل فعلی", + "document.max_file_size_help": "حداکثر ۵۰ مگابایت", + "document.no_documents": "مستندی آپلود نشده است", + "document.download": "دانلود", + "document.type_pdf": "PDF", + "document.type_image": "تصویر", + "document.type_word": "Word", + "document.type_excel": "Excel", + "document.type_text": "متنی", + "document.type_other": "سایر", + "document.file_not_found": "فایل یافت نشد", + "document.created_successfully": "مستند با موفقیت آپلود شد", + "document.updated_successfully": "مستند با موفقیت بروزرسانی شد", + "document.deleted_successfully": "مستند با موفقیت حذف شد", + + "report.reports": "گزارش‌ها", + "report.financial_report": "گزارش مالی", + "report.employee_worklog_report": "گزارش کارکرد", + "report.inventory_report": "گزارش انبار", + "report.payroll_report": "گزارش حقوق و دستمزد", + "report.financial_description": "گزارش مالی پروژه‌ها شامل بودجه، هزینه‌ها و مانده", + "report.worklog_description": "گزارش ساعات کار نیروها به تفکیک پروژه", + "report.inventory_description": "گزارش موجودی انبار و هشدار کمبود", + "report.payroll_description": "گزارش حقوق، پاداش و کسورات نیروها", + "report.project_code": "کد پروژه", + "report.project_name": "نام پروژه", + "report.budget": "بودجه", + "report.total_expenses": "جمع هزینه‌ها", + "report.total_petty_cash": "جمع تنخواه", + "report.remaining": "مانده", + "report.utilization_percent": "درصد مصرف", + "report.manager": "مدیر پروژه", + "report.employee_name": "نام نیرو", + "report.job_title": "عنوان شغلی", + "report.project": "پروژه", + "report.date": "تاریخ", + "report.hours_worked": "ساعات کار", + "report.overtime_hours": "ساعت اضافه‌کار", + "report.task": "وظیفه", + "report.item_code": "کد قلم", + "report.item_name": "نام قلم", + "report.warehouse": "انبار", + "report.total_in": "جمع ورودی", + "report.total_out": "جمع خروجی", + "report.current_stock": "موجودی فعلی", + "report.min_quantity": "حداقل موجودی", + "report.unit_price": "قیمت واحد", + "report.stock_value": "ارزش موجودی", + "report.status": "وضعیت", + "report.low_stock": "کمبود موجودی", + "report.ok": "مطلوب", + "report.contract_type": "نوع قرارداد", + "report.salary": "حقوق", + "report.bonus": "پاداش", + "report.overtime": "اضافه‌کار", + "report.deduction": "کسورات", + "report.other": "سایر", + "report.net_pay": "خالص پرداختی", + "report.total_paid": "جمع پرداختی", + "report.total_unpaid": "جمع پرداخت‌نشده", + "report.employee_count": "تعداد نیروها", + "report.date_from": "از تاریخ", + "report.date_to": "تا تاریخ", + "report.no_data": "داده‌ای برای نمایش وجود ندارد", + "report.low_stock_alerts": "هشدار کمبود موجودی", + "report.total_inventory_value": "ارزش کل موجودی", + "report.total_items": "تعداد اقلام", + "report.grand_totals": "جمع کل", + + "reports.reports": "گزارش‌ها", + "reports.financial": "گزارش مالی", + "reports.financial_description": "گزارش مالی پروژه‌ها شامل بودجه، هزینه‌ها و مانده", + "reports.employee_worklog": "گزارش کارکرد", + "reports.employee_worklog_description": "گزارش ساعات کار نیروها به تفکیک پروژه", + "reports.inventory": "گزارش انبار", + "reports.inventory_description": "گزارش موجودی انبار و هشدار کمبود", + "reports.payroll": "گزارش حقوق", + "reports.payroll_description": "گزارش حقوق، پاداش و کسورات نیروها", + "reports.total_expenses": "جمع هزینه‌ها", + "reports.approved_amount": "مبلغ تأییدشده", + "reports.pending_amount": "مبلغ در انتظار", + "reports.export_coming_soon": "خروجی به‌زودی اضافه می‌شود", + "reports.detailed_view_coming_soon": "نمایش تفصیلی به‌زودی اضافه می‌شود", + + "payroll.payroll": "حقوق و دستمزد", + "payroll.calculate_payroll": "محاسبه حقوق", + "payroll.final_settlement": "تسویه حساب", + "payroll.total_salaries": "جمع حقوق", + "payroll.total_paid": "جمع پرداختی", + "payroll.total_unpaid": "جمع پرداخت‌نشده", + "payroll.total_deductions": "جمع کسورات", + "payroll.month": "ماه", + "payroll.year": "سال", + "payroll.employee": "نیرو", + "payroll.select_employee": "انتخاب نیرو", + "payroll.type": "نوع", + "payroll.amount": "مبلغ", + "payroll.currency": "ارز", + "payroll.date": "تاریخ", + "payroll.effective_date": "تاریخ اجرا", + "payroll.is_paid": "پرداخت‌شده", + "payroll.pay": "پرداخت", + "payroll.pay_bulk": "پرداخت دسته‌ای", + "payroll.settle_all": "تسویه همه", + "payroll.settlement": "تسویه حساب", + "payroll.new_entry": "ثبت رکورد جدید", + "payroll.load": "بارگذاری", + "payroll.show": "نمایش", + "payroll.base_salary": "حقوق پایه", + "payroll.existing_records": "رکوردهای موجود", + "payroll.all_settled": "همه تسویه شده", + "payroll.unpaid_count": "تعداد پرداخت‌نشده", + "payroll.confirm_settlement": "آیا از تسویه همه پرداخت‌های معوق مطمئن هستید؟", + "payroll.type_salary": "حقوق", + "payroll.type_bonus": "پاداش", + "payroll.type_deduction": "کسورات", + "payroll.type_advance": "مساعده", + "payroll.type_overtime": "اضافه‌کار", + "payroll.type_allowance": "مزایا", + "payroll.no_records": "رکوردی ثبت نشده است", + "payroll.settlement_coming_soon": "تسویه حساب کامل به‌زودی اضافه می‌شود", + "payroll.detailed_view_coming_soon": "نمایش تفصیلی به‌زودی اضافه می‌شود", + "payroll.record_created_successfully": "رکورد مالی با موفقیت ثبت شد", + "payroll.created_successfully": "رکورد مالی با موفقیت ثبت شد", + "payroll.marked_as_paid": "پرداخت با موفقیت ثبت شد", + "payroll.already_paid": "این رکورد قبلاً پرداخت شده است", + "payroll.bulk_paid_successfully": "پرداخت‌های دسته‌ای با موفقیت ثبت شد", + "payroll.settlement_processed": "تسویه حساب با موفقیت انجام شد", + "payroll.settlement_processed_successfully": "تسویه حساب با موفقیت انجام شد", + "payroll.total_gross": "جمع ناخالص", + "payroll.total_net": "خالص پرداختی", + "payroll.unpaid_employees": "نیروهای پرداخت‌نشده", + "payroll.payment_status": "وضعیت پرداخت", + "payroll.paid_percent": "پرداخت شده", + "payroll.financial_records": "رکوردهای مالی", + "payroll.confirm_bulk_pay": "آیا از پرداخت دسته‌ای موارد انتخاب‌شده مطمئن هستید؟", + "payroll.confirm_pay": "آیا از ثبت پرداخت مطمئن هستید؟", + "payroll.paid": "پرداخت‌شده", + "payroll.unpaid": "پرداخت‌نشده", + "payroll.manual_entry": "ثبت دستی", + "payroll.auto_calculate": "محاسبه خودکار", + "payroll.auto_calculate_btn": "محاسبه و ایجاد", + "payroll.auto_calculate_help_title": "محاسبه خودکار حقوق", + "payroll.auto_calculate_help": "بر اساس حقوق پایه هر نیرو، رکورد حقوقی ماه انتخاب‌شده ایجاد می‌شود. اگر قبلاً حقوق ماه جاری ثبت شده باشد، آن نیرو رد می‌شود.", + "payroll.auto_salary_description": "حقوق ماه :month سال :year", + "payroll.auto_overtime_description": "اضافه‌کار :hours ساعت", + "payroll.auto_calculated": ":created رکورد ایجاد شد، :skipped رکورد رد شد (قبلاً ثبت شده)", + "payroll.fill_base_salary": "حقوق پایه", + "payroll.base_salary_short": "پایه", + "payroll.include_worklogs": "محاسبه اضافه‌کار از کارکرد", + "payroll.include_worklogs_help": "ساعات اضافه‌کار از کارکرد ثبت‌شده محاسبه و به مبلغ اضافه‌کار تبدیل می‌شود", + "payroll.select_employees": "انتخاب نیروها", + "payroll.notes": "یادداشت", + "payroll.payroll_year": "حقوق سال :year", + "payroll.no_records_year": "رکوردی برای این سال ثبت نشده است", + "payroll.settlement_type": "نوع تسویه", + "payroll.settlement_resignation": "استعفا", + "payroll.settlement_termination": "اخراج", + "payroll.settlement_end_of_contract": "پایان قرارداد", + "payroll.settlement_date": "تاریخ تسویه", + "payroll.last_working_date": "آخرین روز کاری", + "payroll.settlement_preview": "پیش‌نمایش تسویه", + "payroll.preview": "پیش‌نمایش", + "payroll.process_settlement": "انجام تسویه", + "payroll.settlement_warning_title": "توجه!", + "payroll.settlement_warning": "با انجام تسویه حساب، تمام رکوردهای پرداخت‌نشده نیرو به عنوان پرداخت‌شده علامت‌گذاری شده و وضعیت نیرو به «تسویه‌شده» تغییر می‌کند. این عمل قابل بازگشت نیست.", + "payroll.total_payable": "جمع قابل پرداخت", + "payroll.net_payable": "خالص قابل پرداخت", + "payroll.months_worked": "ماه کارکرد", + "payroll.no_unpaid_records": "رکورد پرداخت‌نشده‌ای وجود ندارد", + "payroll.cannot_delete_paid": "رکورد پرداخت‌شده قابل حذف نیست", + "payroll.deleted_successfully": "رکورد با موفقیت حذف شد", + + "employee.contract_daily": "روزمزد", + "employee.status_terminated": "تسویه‌شده", + + "calendar.jalali": "شمسی", + "calendar.gregorian": "میلادی", + "calendar.hijri": "قمری", + + "currency.IRR": "ریال", + "currency.USD": "دلار", + "currency.EUR": "یورو", + "currency.AED": "درهم", + "currency.code_irr": "ریال", + "currency.code_usd": "دلار", + "currency.code_eur": "یورو", + "currency.code_aed": "درهم", + + "attachment.attachments": "پیوست‌ها", + "attachment.add_file": "افزودن فایل", + "attachment.additional_files": "فایل‌های پیوست اضافی", + "attachment.max_files_help": "حداکثر ۵ فایل (تصویر، PDF، Word - حداکثر ۱۰ مگابایت هر کدام)", + "attachment.no_attachments": "پیوستی وجود ندارد", + "attachment.download": "دانلود", + "attachment.uploaded_successfully": "فایل با موفقیت آپلود شد", + "attachment.deleted_successfully": "پیوست با موفقیت حذف شد", + "attachment.upload_failed": "خطا در آپلود فایل", + "attachment.delete_failed": "خطا در حذف پیوست", + "attachment.invalid_model_type": "نوع مدل نامعتبر است", + "attachment.model_not_found": "مدل یافت نشد", + + "settings.settings": "تنظیمات", + "settings.organization": "سازمان", + "settings.users": "کاربران", + "settings.currencies": "ارزها", + "settings.org_name": "نام سازمان", + "settings.domain": "دامنه", + "settings.base_currency": "ارز پایه", + "settings.default_locale": "زبان پیش‌فرض", + "settings.default_calendar": "تقویم پیش‌فرض", + "settings.allow_multi_currency": "اجازه استفاده از چند ارز", + "settings.logo": "لوگو", + "settings.logo_help": "فرمت‌های مجاز: JPEG, PNG, SVG — حداکثر ۵۱۲ کیلوبایت", + "settings.organization_updated": "تنظیمات سازمان با موفقیت بروزرسانی شد", + "settings.add_user": "افزودن کاربر", + "settings.edit_user": "ویرایش کاربر", + "settings.user_name": "نام", + "settings.user_email": "ایمیل", + "settings.user_role": "نقش", + "settings.role_admin": "مدیر سیستم", + "settings.role_manager": "مدیر پروژه", + "settings.role_accountant": "حسابدار", + "settings.role_employee": "کارمند", + "settings.active": "فعال", + "settings.inactive": "غیرفعال", + "settings.activate": "فعال‌سازی", + "settings.deactivate": "غیرفعال‌سازی", + "settings.confirm": "تأیید", + "settings.password_help": "خالی بگذارید اگر نمی‌خواهید تغییر کنید", + "settings.user_created": "کاربر با موفقیت ایجاد شد", + "settings.user_updated": "کاربر با موفقیت بروزرسانی شد", + "settings.user_status_updated": "وضعیت کاربر تغییر کرد", + "settings.no_users": "کاربری ثبت نشده است", + "settings.add_currency": "افزودن ارز", + "settings.edit_currency": "ویرایش ارز", + "settings.currency_code": "کد ارز", + "settings.currency_name": "نام ارز", + "settings.currency_symbol": "نماد", + "settings.exchange_rate": "نرخ تبدیل به دلار", + "settings.exchange_rate_help": "نرخ تبدیل هر واحد این ارز به دلار آمریکا", + "settings.decimal_places": "تعداد اعشار", + "settings.currency_created": "ارز با موفقیت ایجاد شد", + "settings.currency_updated": "ارز با موفقیت بروزرسانی شد", + "settings.currency_status_updated": "وضعیت ارز تغییر کرد", + "settings.no_currencies": "ارزی ثبت نشده است", + "settings.full_settings_coming_soon": "صفحه کامل تنظیمات به‌زودی اضافه می‌شود", + "employee.notes": "یادداشت ها", + "project.projects": "پروژه ها", + "task.gantt_no_tasks": "هیچ وظیفه ای برای نمایش وجود ندارد", + "task.gantt_day": "روز", + "task.gantt_week": "هفته", + "task.gantt_month": "ماه", + "task.gantt_year": "سال", + "task.gantt_all": "همه", + "task.gantt_loading": "در حال بارگذاری...", + "task.gantt_no_data": "هیچ داده ای برای نمایش وجود ندارد", + "task.gantt_today": "امروز", + "auth.login_subtitle": "به سامانه مدریرت پروژه ورنوا خوش آمدید", + "auth.logout_success": "خروج با موفقیت انجام شد", + "auth.register": "ثبت نام", + "auth.password_reset": "بازیابی رمز عبور", + "auth.password_reset_subtitle": "رمز عبور خود را وارد کنید", + "auth.password_reset_success": "رمز عبور شما با موفقیت تغییر کرد", + "auth.login_success": "به حساب کاربری خودتون خوش اومدید", + "auth.email_placeholder": "ایمیل", + "auth.password_placeholder": "رمز عبور", + "auth.register_success": "ثبت نام با موفقیت انجام شد", + "auth.register_subtitle": "حساب کاربری خود را بسازید", + "auth.register_email_exists": "این ایمیل قبلا ثبت شده است", + "auth.remember_me": "مرا به خاطر بسپار", + "auth.sign_in": "ورود", + "project.create_project": "ایجاد پروژه جدید", + "task.project": "پروژه", + "task.kanban_view": "نمایش کانبان", + "task.create_task": "ایجاد وظیفه جدید", + "task.due_date": "تاریخ پایان", + "task.estimated_hours": "ساعت تخمینی", + "task.task_name": "نام وظیفه", + "gantt_duration": "مدت زمان", + + "expense.status": "وضعیت", + "expense.pending": "در انتظار", + "expense.approv": "تایید شده", + "expense.reject": "رد شده", + "expense.approved": "تأیید", + + "employee.employees": "کارمندان", + "employee.search_placeholder": "جستجوی کارمند", + "employee.contract_type": "نوع قرارداد", + "employee.inactive": "وضعیت غیرفعال", + "employee.create_employee": "ایجاد کارمند جدید", + "employee.first_name": "نام", + "employee.last_name": "نام خانوادگی", + "employee.email": "ایمیل", + "employee.address": "آدرس", + "employee.active": "فعال", + "employee.hire_date": "تاریخ استخدام", + "employee.position": "سمت و عنوان شغلی", + "employee.department": "واحد فعالیت", + "employee.base_salary": "حقوق پایه", + "employee.national_code": "کد ملی", + + "pettyCash.total_amount": "مجموع مبلغ تنخواه", + "pettyCash.total_expenses": "مجموع هزینه های تنخواه", + "pettyCash.total_remaining": "مجموع باقیمانده تنخواه", + + + "letters.create_letter": "نامه جدید", + "letters.attachments_help": "فایل های مربوط به نامه با فرمت مناسب pdf یا تصویر رو بارگزاری کنید." + + +} diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 00000000..7e9b8602 --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,82 @@ + + + + + + + {{ __('auth.login') }} - Vernova + + + + @vite(['resources/css/app.css']) + + + +
+ +
+
+ + + +
+

Vernova

+

{{ __('auth.login_subtitle') }}

+
+ + +
+ @if(session('success')) +
+ {{ session('success') }} +
+ @endif + +
+ @csrf + + +
+ + + @error('email') +

{{ $message }}

+ @enderror +
+ + +
+ + + @error('password') +

{{ $message }}

+ @enderror +
+ + +
+ +
+ + + +
+
+ + +

© {{ date('Y') }} Vernova. {{ __('common.all_rights_reserved') }}

+
+ + diff --git a/resources/views/components/currency-display.blade.php b/resources/views/components/currency-display.blade.php new file mode 100644 index 00000000..43193034 --- /dev/null +++ b/resources/views/components/currency-display.blade.php @@ -0,0 +1,7 @@ +@props(['amount' => 0, 'currency' => null]) + +@php + $formatted = \App\Helpers\CurrencyHelper::formatWithSymbol((float) $amount, $currency); +@endphp + +merge(['class' => 'font-medium']) }}>{{ $formatted }} diff --git a/resources/views/components/date-input.blade.php b/resources/views/components/date-input.blade.php new file mode 100644 index 00000000..a79ac59b --- /dev/null +++ b/resources/views/components/date-input.blade.php @@ -0,0 +1,55 @@ +@props(['name', 'value' => null, 'id' => null, 'class' => '', 'required' => false]) + +@php +$calendar = session('calendar', 'jalali'); +$inputId = $id ?? $name; +$gregorianValue = null; +$displayValue = ''; + +// Normalize the value to a Gregorian Y-m-d string +if ($value) { + if ($value instanceof \DateTimeInterface) { + $gregorianValue = $value->format('Y-m-d'); + } elseif (is_string($value) && strlen($value) >= 10) { + $gregorianValue = substr($value, 0, 10); + } + + if ($calendar === 'jalali' && $gregorianValue) { + try { + $displayValue = \Morilog\Jalali\Jalalian::fromDateTime($gregorianValue)->format('Y/m/d'); + } catch (\Exception $e) { + $displayValue = $gregorianValue; + } + } else { + $displayValue = $gregorianValue ?? ''; + } +} + +$defaultClass = 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 focus:border-teal-500'; +$cssClass = $class ?: $defaultClass; +@endphp + +@if($calendar === 'jalali') +
+ + +
+@else + +@endif diff --git a/resources/views/components/localized-date.blade.php b/resources/views/components/localized-date.blade.php new file mode 100644 index 00000000..f9f7ec3e --- /dev/null +++ b/resources/views/components/localized-date.blade.php @@ -0,0 +1,13 @@ +@props(['date' => null, 'format' => null, 'calendar' => null]) + +@php + $displayDate = ''; + if ($date) { + $displayDate = \App\Helpers\CalendarHelper::formatDate($date, $format, $calendar); + if (app()->getLocale() === 'fa') { + $displayDate = persian_num($displayDate); + } + } +@endphp + +merge(['class' => '']) }}>{{ $displayDate }} diff --git a/resources/views/components/priority-badge.blade.php b/resources/views/components/priority-badge.blade.php new file mode 100644 index 00000000..b47db288 --- /dev/null +++ b/resources/views/components/priority-badge.blade.php @@ -0,0 +1,25 @@ +@props(['priority' => 'medium']) + +@php + $colors = [ + 'low' => 'bg-gray-100 text-gray-600', + 'medium' => 'bg-amber-50 text-amber-600', + 'high' => 'bg-orange-50 text-orange-600', + 'urgent' => 'bg-rose-50 text-rose-600', + ]; + + $dots = [ + 'low' => 'bg-gray-400', + 'medium' => 'bg-amber-400', + 'high' => 'bg-orange-500', + 'urgent' => 'bg-rose-500', + ]; + + $colorClass = $colors[$priority] ?? $colors['medium']; + $dotClass = $dots[$priority] ?? $dots['medium']; +@endphp + + + + {{ __('task.priority_' . $priority) }} + diff --git a/resources/views/components/status-badge.blade.php b/resources/views/components/status-badge.blade.php new file mode 100644 index 00000000..14424e38 --- /dev/null +++ b/resources/views/components/status-badge.blade.php @@ -0,0 +1,39 @@ +@props(['type' => 'project', 'status' => '']) + +@php + $colors = [ + 'project' => [ + 'planning' => 'bg-blue-50 text-blue-700 border-blue-200', + 'active' => 'bg-emerald-50 text-emerald-700 border-emerald-200', + 'on_hold' => 'bg-amber-50 text-amber-700 border-amber-200', + 'completed' => 'bg-gray-50 text-gray-700 border-gray-200', + 'cancelled' => 'bg-red-50 text-red-700 border-red-200', + ], + 'task' => [ + 'pending' => 'bg-gray-50 text-gray-700 border-gray-200', + 'in_progress' => 'bg-blue-50 text-blue-700 border-blue-200', + 'review' => 'bg-amber-50 text-amber-700 border-amber-200', + 'done' => 'bg-emerald-50 text-emerald-700 border-emerald-200', + 'cancelled' => 'bg-red-50 text-red-700 border-red-200', + ], + 'expense' => [ + 'pending' => 'bg-amber-50 text-amber-700 border-amber-200', + 'approved' => 'bg-emerald-50 text-emerald-700 border-emerald-200', + 'rejected' => 'bg-red-50 text-red-700 border-red-200', + 'paid' => 'bg-blue-50 text-blue-700 border-blue-200', + ], + 'petty_cash' => [ + 'pending' => 'bg-amber-50 text-amber-700 border-amber-200', + 'approved' => 'bg-emerald-50 text-emerald-700 border-emerald-200', + 'rejected' => 'bg-red-50 text-red-700 border-red-200', + 'settled' => 'bg-blue-50 text-blue-700 border-blue-200', + ], + ]; + + $colorClass = $colors[$type][$status] ?? 'bg-gray-50 text-gray-700 border-gray-200'; + $label = __($type . '.status_' . $status); +@endphp + +merge(['class' => "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border {$colorClass}"]) }}> + {{ $label }} + diff --git a/resources/views/dashboard/index.blade.php b/resources/views/dashboard/index.blade.php new file mode 100644 index 00000000..53d801ff --- /dev/null +++ b/resources/views/dashboard/index.blade.php @@ -0,0 +1,242 @@ +@extends('layouts.app') + +@section('content') +
+ +
+
+

{{ __('dashboard.title') }}

+

{{ $todayDisplay }}

+
+ +
+ + +
+ +
+
+
+
+

{{ __('dashboard.active_projects') }}

+

+ {{ app()->getLocale() === 'fa' ? persian_num($activeProjectsCount) : $activeProjectsCount }} +

+
+
+ + + +
+
+ @if($projectTrend != 0) +

+ {{ $projectTrend > 0 ? '↑' : '↓' }} {{ abs($projectTrend) }}% {{ __('dashboard.vs_last_month') }} +

+ @endif +
+ + +
+
+
+
+

{{ __('dashboard.total_budget') }}

+

{{ $totalBudgetFormatted }}

+
+
+ + + +
+
+
+ + +
+
+
+
+

{{ __('dashboard.pending_tasks') }}

+

+ {{ app()->getLocale() === 'fa' ? persian_num($pendingTasksCount) : $pendingTasksCount }} +

+
+
+ + + +
+
+ @if($taskTrend != 0) +

+ {{ $taskTrend > 0 ? '↑' : '↓' }} {{ abs($taskTrend) }}% {{ __('dashboard.vs_last_month') }} +

+ @endif +
+ + +
+
+
+
+

{{ __('dashboard.active_employees') }}

+

+ {{ app()->getLocale() === 'fa' ? persian_num($activeEmployeesCount) : $activeEmployeesCount }} +

+
+
+ + + +
+
+ @if($employeeTrend != 0) +

+ {{ $employeeTrend > 0 ? '↑' : '↓' }} {{ abs($employeeTrend) }}% {{ __('dashboard.vs_last_month') }} +

+ @endif +
+
+ + +
+ +
+
+

{{ __('dashboard.projects_progress') }}

+ {{ __('common.view_all') }} +
+
+ @forelse($projectsInProgress as $project) +
+
+
+ + {{ $project->name }} + + + {{ app()->getLocale() === 'fa' ? persian_num($project->progress) : $project->progress }}% + +
+
+
+
+
+ {{ $project->code }} + @if($project->manager) + {{ $project->manager->name }} + @endif +
+
+
+ @empty +

{{ __('dashboard.no_projects') }}

+ @endforelse +
+
+ + +
+ + + + +
+
+

{{ __('dashboard.recent_activity') }}

+
+
+ @forelse($recentActivities as $activity) +
+
+ {{ mb_strtoupper(mb_substr($activity->user?->name ?? '?', 0, 1)) }} +
+
+

{{ $activity->user?->name }} — {{ $activity->description }}

+

{{ calendar_date($activity->created_at) }}

+
+
+ @empty +

{{ __('dashboard.no_activity') }}

+ @endforelse +
+
+
+
+ + +
+

{{ __('dashboard.localization_info') }}

+
+
+

{{ __('dashboard.calendar_system') }}

+

{{ $currentCalendar === 'jalali' ? __('calendar.jalali') : __('calendar.gregorian') }}

+

{{ __('dashboard.today') }}: {{ $todayDisplay }}

+
+
+

{{ __('dashboard.number_format') }}

+

+ {{ app()->getLocale() === 'fa' ? persian_num(1234567.89) : number_format(1234567.89, 2) }} +

+
+
+

{{ __('dashboard.currency_format') }}

+

{{ format_currency(150000000, $currencyCode) }}

+
+
+
+ + + +
+@endsection diff --git a/resources/views/documents/form.blade.php b/resources/views/documents/form.blade.php new file mode 100644 index 00000000..f6b1ef8b --- /dev/null +++ b/resources/views/documents/form.blade.php @@ -0,0 +1,55 @@ +@extends('layouts.app') + +@section('content') +
+
+ + + +

{{ isset($document) ? __('document.edit_document') : __('document.upload_document') }}

+
+ +
+ @csrf + @if(isset($document)) @method('PUT') @endif + +
+
+ + + @error('title')

{{ $message }}

@enderror +
+ +
+ + +
+ +
+ + +
+ +
+ + + @error('file')

{{ $message }}

@enderror +
+
+ +
+ {{ __('common.cancel') }} + +
+
+
+@endsection diff --git a/resources/views/documents/index.blade.php b/resources/views/documents/index.blade.php new file mode 100644 index 00000000..bd8a8491 --- /dev/null +++ b/resources/views/documents/index.blade.php @@ -0,0 +1,91 @@ +@extends('layouts.app') + +@section('content') +
+
+

{{ __('document.documents') }}

+ + + {{ __('document.upload_document') }} + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + @forelse($documents as $document) + + + + + + + + @empty + + + + @endforelse + +
{{ __('document.title') }}{{ __('document.file_type') }}{{ __('document.project') }}{{ __('document.uploaded_by') }}{{ __('common.actions') }}
+ {{ $document->title }} + + {{ __('document.type_' . $document->file_type) }} + {{ $document->project?->name ?? '-' }}{{ $document->uploader?->name ?? '-' }} +
+ {{ __('document.download') }} + {{ __('common.edit') }} +
+ @csrf @method('DELETE') + +
+
+
{{ __('document.no_documents') }}
+
+
+ +
+ {{ $documents->withQueryString()->links() }} +
+
+@endsection diff --git a/resources/views/documents/show.blade.php b/resources/views/documents/show.blade.php new file mode 100644 index 00000000..490fec5a --- /dev/null +++ b/resources/views/documents/show.blade.php @@ -0,0 +1,45 @@ +@extends('layouts.app') + +@section('content') +
+
+
+ + + +

{{ $document->title }}

+
+ +
+ +
+
+
+

{{ __('document.title') }}

+

{{ $document->title }}

+
+
+

{{ __('document.file_type') }}

+ {{ __('document.type_' . $document->file_type) }} +
+
+

{{ __('document.project') }}

+

{{ $document->project?->name ?? '-' }}

+
+
+

{{ __('document.uploaded_by') }}

+

{{ $document->uploader?->name ?? '-' }}

+
+ @if($document->description) +
+

{{ __('document.description') }}

+

{{ $document->description }}

+
+ @endif +
+
+
+@endsection diff --git a/resources/views/employees/create.blade.php b/resources/views/employees/create.blade.php new file mode 100644 index 00000000..868f54e0 --- /dev/null +++ b/resources/views/employees/create.blade.php @@ -0,0 +1,18 @@ +@extends('layouts.app') + +@section('title', __('employee.create')) + +@section('content') +
+
+

{{ __('employee.create') }}

+
+ +
+
+ @csrf + @include('employees.form') +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/employees/edit.blade.php b/resources/views/employees/edit.blade.php new file mode 100644 index 00000000..0503f784 --- /dev/null +++ b/resources/views/employees/edit.blade.php @@ -0,0 +1,19 @@ +@extends('layouts.app') + +@section('title', __('employee.edit')) + +@section('content') +
+
+

{{ __('employee.edit') }}

+
+ +
+
+ @csrf + @method('PUT') + @include('employees.form') +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/employees/financials.blade.php b/resources/views/employees/financials.blade.php new file mode 100644 index 00000000..9b15a409 --- /dev/null +++ b/resources/views/employees/financials.blade.php @@ -0,0 +1,113 @@ +@extends('layouts.app') + +@section('content') +
+ +
+
+ + + +
+

{{ __('employee.financials') }}

+

{{ $employee->first_name }} {{ $employee->last_name }}

+
+
+
+ + +
+
+

{{ __('employee.totalPaid') }}

+

{{ format_currency((float)$totalPaid, $employee->currency?->code) }}

+
+
+

{{ __('employee.totalUnpaid') }}

+

{{ format_currency((float)$totalUnpaid, $employee->currency?->code) }}

+
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + @forelse($financials as $fin) + + + + + + + + @empty + + + + @endforelse + +
{{ __('employee.effectiveDate') }}{{ __('employee.type') }}{{ __('employee.amount') }}{{ __('common.status') }}{{ __('employee.description') }}
{{ $fin->effective_date ? calendar_date($fin->effective_date) : '-' }} + + {{ __('employee.' . $fin->type) }} + + + {{ in_array($fin->type, ['deduction', 'advance']) ? '-' : '+' }}{{ format_currency((float)$fin->amount, $fin->currency?->code) }} + + @if(isset($fin->is_paid)) + + {{ $fin->is_paid ? __('employee.isPaid') : __('employee.isUnpaid') }} + + @else + - + @endif + {{ $fin->description ?? '-' }}
{{ __('employee.noFinancials') }}
+
+
+ + +
+ {{ $financials->withQueryString()->links() }} +
+
+@endsection diff --git a/resources/views/employees/form.blade.php b/resources/views/employees/form.blade.php new file mode 100644 index 00000000..1aee6ea4 --- /dev/null +++ b/resources/views/employees/form.blade.php @@ -0,0 +1,138 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($employee) ? __('employee.edit_employee') : __('employee.create_employee') }} +

+
+ + +
+ @csrf + @if(isset($employee)) + @method('PUT') + @endif + +
+ +
+ + + @error('first_name')

{{ $message }}

@enderror +
+ + +
+ + + @error('last_name')

{{ $message }}

@enderror +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/employees/index.blade.php b/resources/views/employees/index.blade.php new file mode 100644 index 00000000..6b4ab5a2 --- /dev/null +++ b/resources/views/employees/index.blade.php @@ -0,0 +1,78 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('employee.employees') }}

+ + + {{ __('employee.new') }} + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + + + + +
+ {{ $employees->withQueryString()->links() }} +
+
+@endsection diff --git a/resources/views/employees/show.blade.php b/resources/views/employees/show.blade.php new file mode 100644 index 00000000..d3009fd6 --- /dev/null +++ b/resources/views/employees/show.blade.php @@ -0,0 +1,125 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +
+

{{ $employee->full_name }}

+

{{ $employee->job_title ?? '-' }}

+
+ +
+ + +
+
+
+

{{ __('employee.firstName') }}

+

{{ $employee->first_name }}

+
+
+

{{ __('employee.lastName') }}

+

{{ $employee->last_name }}

+
+
+

{{ __('employee.jobTitle') }}

+

{{ $employee->job_title ?? '-' }}

+
+
+

{{ __('employee.contractType') }}

+

{{ __('employee.' . $employee->contract_type) ?? $employee->contract_type ?? '-' }}

+
+
+

{{ __('employee.phone') }}

+

{{ $employee->phone ?? '-' }}

+
+
+

{{ __('employee.nationalCode') }}

+

{{ $employee->national_code ?? '-' }}

+
+
+

{{ __('employee.hireDate') }}

+

{{ $employee->hire_date ? calendar_date($employee->hire_date) : '-' }}

+
+
+

{{ __('employee.project') }}

+

{{ $employee->project?->name ?? '-' }}

+
+
+

{{ __('employee.baseSalary') }}

+

{{ $employee->base_salary ? format_currency((float)$employee->base_salary, $employee->currency?->code) : '-' }}

+
+
+

{{ __('employee.bankAccount') }}

+

{{ $employee->bank_account ?? '-' }}

+
+
+

{{ __('employee.status') }}

+ + {{ __('employee.' . $employee->status) ?? $employee->status }} + +
+
+
+ + + @if($employee->workLogs->count() > 0) +
+
+

{{ __('employee.workLogs') }}

+ {{ __('common.all') }} ← +
+
+ @foreach($employee->workLogs->take(5) as $log) +
+
+

{{ calendar_date($log->date) }}

+

{{ $log->project?->name ?? '-' }}

+
+ {{ persian_num(number_format($log->hours_worked, 1)) }} {{ __('employee.hoursWorked') }} +
+ @endforeach +
+
+ @endif + + + @if($employee->financials->count() > 0) +
+
+

{{ __('employee.financials') }}

+ {{ __('common.all') }} ← +
+
+ @foreach($employee->financials->take(5) as $fin) +
+
+

{{ __('employee.' . $fin->type) }}

+

{{ $fin->effective_date ? calendar_date($fin->effective_date) : '-' }}

+
+ + {{ format_currency((float)$fin->amount, $fin->currency?->code) }} + +
+ @endforeach +
+
+ @endif +
+@endsection diff --git a/resources/views/employees/work-logs.blade.php b/resources/views/employees/work-logs.blade.php new file mode 100644 index 00000000..17637c89 --- /dev/null +++ b/resources/views/employees/work-logs.blade.php @@ -0,0 +1,97 @@ +@extends('layouts.app') + +@section('content') +
+ +
+
+ + + +
+

{{ __('employee.workLogs') }}

+

{{ $employee->first_name }} {{ $employee->last_name }}

+
+
+
+ + +
+
+

{{ __('employee.totalHours') }}

+

{{ persian_num(number_format($totalHours, 1)) }}

+
+
+

{{ __('employee.overtimeHours') }}

+

{{ persian_num(number_format($overtimeHours, 1)) }}

+
+
+ + +
+
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + @forelse($workLogs as $log) + + + + + + + + @empty + + + + @endforelse + +
{{ __('employee.date') }}{{ __('employee.project') }}{{ __('employee.hoursWorked') }}{{ __('employee.overtimeHours') }}{{ __('employee.description') }}
{{ calendar_date($log->date) }}{{ $log->project?->name ?? '-' }}{{ persian_num(number_format($log->hours_worked, 1)) }} + {{ $log->overtime_hours > 0 ? persian_num(number_format($log->overtime_hours, 1)) : '-' }} + {{ $log->description ?? '-' }}
{{ __('employee.noWorkLogs') }}
+
+
+ + +
+ {{ $workLogs->withQueryString()->links() }} +
+
+@endsection diff --git a/resources/views/expenses/expense-form.blade.php b/resources/views/expenses/expense-form.blade.php new file mode 100644 index 00000000..30afb260 --- /dev/null +++ b/resources/views/expenses/expense-form.blade.php @@ -0,0 +1,126 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($expense) ? __('expense.edit_expense') : __('expense.create_expense') }} +

+
+ + +
+ @csrf + @if(isset($expense)) + @method('PUT') + @endif + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

{{ __('expense.currency_conversion_note') }}

+
+ + +
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500" required> +
+ + +
+ + +
+ + +
+ + @if(isset($expense) && $expense?->receipt_path) + + @endif + +
+ + +
+ + +
+ + +
+ + +

{{ __('attachment.max_files_help') }}

+
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/expenses/expense-index.blade.php b/resources/views/expenses/expense-index.blade.php new file mode 100644 index 00000000..e273c81d --- /dev/null +++ b/resources/views/expenses/expense-index.blade.php @@ -0,0 +1,167 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('expense.expenses') }}

+ + + {{ __('expense.new') }} + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + @forelse($expenses as $expense) + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('expense.title') }}{{ __('expense.project') }}{{ __('expense.amount') }}{{ __('expense.category') }}{{ __('expense.date') }}{{ __('expense.status') }}{{ __('common.actions') }}
+
+ + {{ $expense->title ?? '-' }} + + {{-- Attachment indicator --}} + @if($expense->attachments_count > 0 || $expense->receipt_path) + + + @if($expense->attachments_count > 0) + {{ $expense->attachments_count }} + @endif + + @endif +
+
{{ $expense->project?->name }} + {{ format_currency((float)$expense->amount, $expense->currency?->code) }} + @if($expense->original_amount) + ({{ format_currency((float)$expense->original_amount, $expense->currency?->code) }}) + @endif + {{ __('expense.category_' . $expense->category) }}{{ $expense->expense_date ? calendar_date($expense->expense_date) : '-' }} + + {{ $expense->status_label }} + + +
+ {{-- View button --}} + + + + {{-- Edit button --}} + + + + @if($expense->status === 'pending') + + + @endif +
+
{{ __('expense.no_expenses') }}
+
+
+ + +
+ {{ $expenses->withQueryString()->links() }} +
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/expenses/expense-show.blade.php b/resources/views/expenses/expense-show.blade.php new file mode 100644 index 00000000..91ab6ccc --- /dev/null +++ b/resources/views/expenses/expense-show.blade.php @@ -0,0 +1,292 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

{{ $expense->title }}

+ + {{ $expense->status_label }} + +
+ + + {{ __('common.edit') }} + + @if($expense->status === 'pending') + + + @endif +
+
+ + +
+
+ +
+
+

{{ __('expense.project') }}

+

{{ $expense->project?->name ?? '-' }}

+
+
+

{{ __('expense.category') }}

+

{{ __('expense.category_' . $expense->category) }}

+
+
+

{{ __('expense.amount') }}

+

{{ format_currency((float)$expense->amount, $expense->currency?->code) }}

+ @if($expense->original_amount && $expense->original_amount != $expense->amount) +

{{ __('expense.original_amount') ?? 'مبلغ اصلی' }}: {{ format_currency((float)$expense->original_amount, $expense->currency?->code) }}

+ @endif +
+
+

{{ __('expense.date') }}

+

{{ $expense->expense_date ? calendar_date($expense->expense_date) : '-' }}

+
+
+ + +
+ @if($expense->invoice_number) +
+

{{ __('expense.invoice_number') }}

+

{{ $expense->invoice_number }}

+
+ @endif +
+

{{ __('expense.requested_by') ?? 'درخواست‌کننده' }}

+

{{ $expense->requestedBy?->name ?? '-' }}

+
+ @if($expense->approvedBy) +
+

{{ __('expense.approved_by') ?? 'تأییدکننده' }}

+

{{ $expense->approvedBy?->name }}

+
+ @endif + @if($expense->approval_date) +
+

{{ __('expense.approval_date') ?? 'تاریخ تأیید' }}

+

{{ calendar_date($expense->approval_date) }}

+
+ @endif + @if($expense->pettyCash) +
+

{{ __('expense.petty_cash') ?? 'تنخواه' }}

+ {{ $expense->pettyCash->title ?? '#' . $expense->petty_cash_id }} +
+ @endif +
+
+ + + @if($expense->description) +
+

{{ __('expense.description') }}

+

{{ $expense->description }}

+
+ @endif +
+ + + @if($expense->receipt_path) +
+

+ + {{ __('expense.receipt') }} +

+
+ @if(str_ends_with(strtolower($expense->receipt_path), '.pdf')) +
+ +
+ @else +
+ Receipt +
+ @endif +
+

{{ basename($expense->receipt_path) }}

+
+ + + {{ __('letter.download') }} + +
+
+ @endif + + +
+
+

+ + {{ __('attachment.attachments') }} + @if($expense->attachments->count() > 0) + {{ $expense->attachments->count() }} + @endif +

+ +
+ + +
+ @if($expense->attachments->count() > 0) +
+ @foreach($expense->attachments as $attachment) +
+ @if($attachment->is_image) +
+ {{ $attachment->file_name }} +
+ @elseif($attachment->is_pdf) +
+ +
+ @else +
+ +
+ @endif + +
+ {{ $attachment->file_name }} +

{{ $attachment->formatted_size }} • {{ $attachment->created_at?->format('Y/m/d H:i') }}

+
+ +
+ + + + @can('delete', $attachment) + + @endcan +
+
+ @endforeach +
+ @else +
+ +

{{ __('attachment.no_attachments') }}

+
+ @endif +
+
+ + + +
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/expenses/form.blade.php b/resources/views/expenses/form.blade.php new file mode 100644 index 00000000..3d177180 --- /dev/null +++ b/resources/views/expenses/form.blade.php @@ -0,0 +1,125 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($expense) ? __('expense.edit_expense') : __('expense.create_expense') }} +

+
+ + +
+ @csrf + @if(isset($expense)) + @method('PUT') + @endif + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

{{ __('expense.currency_conversion_note') }}

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + @if(isset($expense) && $expense?->receipt_path) + + @endif + +
+ + +
+ + +
+ + +
+ + +

{{ __('attachment.max_files_help') }}

+
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/expenses/index.blade.php b/resources/views/expenses/index.blade.php new file mode 100644 index 00000000..f16cebb4 --- /dev/null +++ b/resources/views/expenses/index.blade.php @@ -0,0 +1,140 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('expense.expenses') }}

+ + + {{ __('expense.new') }} + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + @forelse($expenses as $expense) + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('expense.title') }}{{ __('expense.project') }}{{ __('expense.amount') }}{{ __('expense.category') }}{{ __('expense.date') }}{{ __('expense.status') }}{{ __('common.actions') }}
+ {{ $expense->title }} + {{ $expense->project?->name }} + {{ format_currency((float)$expense->amount, $expense->currency?->code) }} + @if($expense->original_amount) + ({{ format_currency((float)$expense->original_amount, $expense->currency?->code) }}) + @endif + {{ __('expense.category_' . $expense->category) }}{{ calendar_date($expense->expense_date) }} + + {{ $expense->status_label }} + + +
+ @if($expense->status === 'pending') + + + @endif +
+
{{ __('expense.no_expenses') }}
+
+
+ + +
+ {{ $expenses->withQueryString()->links() }} +
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/expenses/show.blade.php b/resources/views/expenses/show.blade.php new file mode 100644 index 00000000..02239d72 --- /dev/null +++ b/resources/views/expenses/show.blade.php @@ -0,0 +1,262 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

{{ $expense->title }}

+ + {{ $expense->status_label }} + +
+ + {{ __('common.edit') }} + + @if($expense->status === 'pending') + + + @endif +
+
+ + +
+
+ +
+
+

{{ __('expense.project') }}

+

{{ $expense->project?->name ?? '-' }}

+
+
+

{{ __('expense.category') }}

+

{{ __('expense.category_' . $expense->category) }}

+
+
+

{{ __('expense.amount') }}

+

{{ format_currency((float)$expense->amount, $expense->currency?->code) }}

+ @if($expense->original_amount && $expense->original_amount != $expense->amount) +

({{ format_currency((float)$expense->original_amount, $expense->currency?->code) }})

+ @endif +
+
+

{{ __('expense.date') }}

+

{{ $expense->expense_date ? calendar_date($expense->expense_date) : '-' }}

+
+
+ + +
+ @if($expense->invoice_number) +
+

{{ __('expense.invoice_number') }}

+

{{ $expense->invoice_number }}

+
+ @endif +
+

{{ __('expense.requested_by') ?? __('common.project') }}

+

{{ $expense->requestedBy?->name ?? '-' }}

+
+ @if($expense->approvedBy) +
+

{{ __('expense.approved_by') ?? 'تأییدکننده' }}

+

{{ $expense->approvedBy?->name }}

+
+ @endif + @if($expense->approval_date) +
+

{{ __('expense.approval_date') ?? 'تاریخ تأیید' }}

+

{{ calendar_date($expense->approval_date) }}

+
+ @endif +
+
+ + + @if($expense->description) +
+

{{ __('expense.description') }}

+

{{ $expense->description }}

+
+ @endif +
+ + + @if($expense->receipt_path) +
+

{{ __('expense.receipt') }}

+
+ @if(str_ends_with(strtolower($expense->receipt_path), '.pdf')) +
+ +
+ @else +
+ Receipt +
+ @endif +
+

{{ basename($expense->receipt_path) }}

+
+ + {{ __('letter.download') }} + +
+
+ @endif + + +
+
+

{{ __('attachment.attachments') }}

+ +
+ + + + + +
+ @forelse($expense->attachments as $attachment) +
+ @if($attachment->is_image) +
+ {{ $attachment->file_name }} +
+ @elseif($attachment->is_pdf) +
+ +
+ @else +
+ +
+ @endif + +
+

{{ $attachment->file_name }}

+

{{ $attachment->formatted_size }} • {{ $attachment->created_at?->format('Y/m/d H:i') }}

+
+ +
+ + + + +
+
+ @empty +
+ +

{{ __('attachment.no_attachments') }}

+
+ @endforelse +
+
+ + + +
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/help/index.blade.php b/resources/views/help/index.blade.php new file mode 100644 index 00000000..ca5351b1 --- /dev/null +++ b/resources/views/help/index.blade.php @@ -0,0 +1,43 @@ +@extends('layouts.app') + +@section('title', __('nav.help')) + +@section('content') +
+

+ {{ __('nav.help') }} +

+ +
+
+

Vernova - راهنمای سیستم

+

سیستم مدیریت پروژه چندمستاجری با پشتیبانی از چندزبانی، چندتقویمی و چندارزی.

+
+ +
+

🟢 پروژه‌ها

+

مدیریت پروژه‌ها، وضعیت، پیشرفت و بودجه

+
+ +
+

📋 وظایف

+

مدیریت وظایف با نمای لیست و کانبان، اولویت‌بندی و تخصیص

+
+ +
+

💰 هزینه‌ها

+

ثبت و تایید هزینه‌ها با پشتیبانی از چند ارز

+
+ +
+

👥 کارکنان

+

مدیریت پرسنل، قراردادها و سوابق کاری

+
+ +
+

📦 انبار

+

مدیریت اقلام، انبارها و تراکنش‌های انبارداری

+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory-transactions/form.blade.php b/resources/views/inventory-transactions/form.blade.php new file mode 100644 index 00000000..b3062a08 --- /dev/null +++ b/resources/views/inventory-transactions/form.blade.php @@ -0,0 +1,199 @@ +@extends('layouts.app') + +@section('title', __('inventory.new_transaction')) + +@section('content') +
+
+
+ + + +

{{ __('inventory.new_transaction') }}

+
+
+ +
+ + @csrf + + +
+
+ + + @error('type') +

{{ $message }}

+ @enderror +
+ +
+ + + value="{{ old('date', date('Y-m-d')) }}" + class="w-full rounded-md border-gray-300 shadow-sm focus:border-teal-500 focus:ring-teal-500" + required> + @error('date') +

{{ $message }}

+ @enderror +
+
+ + +
+
+ + + @error('item_id') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('warehouse_id') +

{{ $message }}

+ @enderror +
+
+ + +
+
+ + + @error('quantity') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('project_id') +

{{ $message }}

+ @enderror +
+
+ + +
+
+ + + @error('unit_price') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('currency_id') +

{{ $message }}

+ @enderror +
+
+ + +
+ + + @error('reference') +

{{ $message }}

+ @enderror +
+ + +
+ + + @error('description') +

{{ $message }}

+ @enderror +
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory-transactions/index.blade.php b/resources/views/inventory-transactions/index.blade.php new file mode 100644 index 00000000..f1d5c898 --- /dev/null +++ b/resources/views/inventory-transactions/index.blade.php @@ -0,0 +1,106 @@ +@extends('layouts.app') + +@section('title', __('inventory.transactions')) + +@section('content') +
+ +
+

{{ __('inventory.transactions') }}

+ + + {{ __('inventory.new_transaction') }} + +
+ + +
+
+ + + + +
+
+ + +
+
+ + + + + + + + + + + + + @forelse($transactions as $tr) + + + + + + + + + @empty + + + + @endforelse + +
{{ __('inventory.transaction_type') }}{{ __('inventory.name') }}{{ __('warehouse.warehouses') }}{{ __('inventory.quantity') }}{{ __('inventory.date') }}{{ __('common.actions') }}
+ @php + $ttk = 'inventory.type_' . $tr->type; + $ttl = __($ttk); + if ($ttl === $ttk) $ttl = $tr->type; + @endphp + + {{ $ttl }} + + {{ $tr->item?->name ?? '-' }}{{ $tr->warehouse?->name ?? '-' }}{{ $tr->quantity }}{{ $tr->date ? calendar_date($tr->date) : '-' }} +
+ {{ __('common.view') }} +
+ @csrf @method('DELETE') + +
+
+
{{ __('inventory.no_transactions') }}
+
+
+ + +
+ {{ $transactions->withQueryString()->links() }} +
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory-transactions/show.blade.php b/resources/views/inventory-transactions/show.blade.php new file mode 100644 index 00000000..1b134d85 --- /dev/null +++ b/resources/views/inventory-transactions/show.blade.php @@ -0,0 +1,103 @@ +@extends('layouts.app') + +@section('title', __('inventory.transactions') . ' #' . $inventoryTransaction->id) + +@section('content') +
+ +
+
+ + + +
+

{{ __('inventory.transactions') }} #{{ $inventoryTransaction->id }}

+
+
+
+ @csrf @method('DELETE') + +
+
+ +
+ +
+
+
+
+

{{ __('inventory.transaction_type') }}

+ @php + $ttk = 'inventory.type_' . $inventoryTransaction->type; + $ttl = __($ttk); + if ($ttl === $ttk) $ttl = $inventoryTransaction->type; + @endphp + + {{ $ttl }} + +
+
+

{{ __('inventory.name') }}

+

{{ $inventoryTransaction->item?->name ?? '-' }}

+
+
+

{{ __('warehouse.warehouses') }}

+

{{ $inventoryTransaction->warehouse?->name ?? '-' }}

+
+
+

{{ __('inventory.quantity') }}

+

{{ $inventoryTransaction->quantity }}

+
+
+

{{ __('inventory.unit_price') }}

+

{{ $inventoryTransaction->unit_price ? number_format($inventoryTransaction->unit_price) : '-' }}

+
+
+

{{ __('inventory.date') }}

+

{{ $inventoryTransaction->date ? calendar_date($inventoryTransaction->date) : '-' }}

+
+
+ + @if($inventoryTransaction->description) +
+

{{ __('common.description') }}

+

{{ $inventoryTransaction->description }}

+
+ @endif +
+
+ + +
+ @if($inventoryTransaction->project) +
+

{{ __('expense.project') }}

+ {{ $inventoryTransaction->project->name }} +
+ @endif + + @if($inventoryTransaction->reference) +
+

{{ __('inventory.reference') }}

+

{{ $inventoryTransaction->reference }}

+
+ @endif + + @if($inventoryTransaction->total_price) +
+

{{ __('inventory.total_price') }}

+

{{ number_format($inventoryTransaction->total_price) }}

+ @if($inventoryTransaction->currency) +

{{ $inventoryTransaction->currency->code }}

+ @endif +
+ @endif +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory/create-item.blade.php b/resources/views/inventory/create-item.blade.php new file mode 100644 index 00000000..e20e0df5 --- /dev/null +++ b/resources/views/inventory/create-item.blade.php @@ -0,0 +1,71 @@ +@extends('layouts.app') + +@section('title', 'افزودن قلم جدید') + +@section('content') +
+
+ + + بازگشت به لیست + +

افزودن قلم جدید

+
+ +
+
+ @csrf +
+
+ + + @error('name')

{{ $message }}

@enderror +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ +
+ + انصراف +
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory/create-transaction.blade.php b/resources/views/inventory/create-transaction.blade.php new file mode 100644 index 00000000..9e34063a --- /dev/null +++ b/resources/views/inventory/create-transaction.blade.php @@ -0,0 +1,77 @@ +@extends('layouts.app') + +@section('title', 'حواله جدید') + +@section('content') +
+
+ + + بازگشت به لیست + +

حواله جدید

+
+ +
+
+ @csrf +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ +
+ + انصراف +
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory/create-warehouse.blade.php b/resources/views/inventory/create-warehouse.blade.php new file mode 100644 index 00000000..4aaa5fde --- /dev/null +++ b/resources/views/inventory/create-warehouse.blade.php @@ -0,0 +1,48 @@ +@extends('layouts.app') + +@section('title', 'افزودن انبار جدید') + +@section('content') +
+
+ + + بازگشت به لیست + +

افزودن انبار جدید

+
+ +
+
+ @csrf +
+
+ + + @error('name')

{{ $message }}

@enderror +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + انصراف +
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory/form.blade.php b/resources/views/inventory/form.blade.php new file mode 100644 index 00000000..da0a6a67 --- /dev/null +++ b/resources/views/inventory/form.blade.php @@ -0,0 +1,132 @@ +@extends('layouts.app') + +@section('title', isset($item) ? __('inventory.edit_item') : __('inventory.add_item')) + +@section('content') +
+
+
+ + + +

+ {{ isset($item) ? __('inventory.edit_item') : __('inventory.add_item') }} +

+
+
+ +
+ + @csrf + @if(isset($item)) + @method('PUT') + @endif + + +
+
+ + + @error('name') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('code') +

{{ $message }}

+ @enderror +
+
+ + +
+
+ + + @error('category') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('unit') +

{{ $message }}

+ @enderror +
+
+ + +
+ + + @error('description') +

{{ $message }}

+ @enderror +
+ + +
+ is_active : true) ? 'checked' : '' }} + class="rounded border-gray-300 text-teal-600 focus:ring-teal-500"> + +
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory/index.blade.php b/resources/views/inventory/index.blade.php new file mode 100644 index 00000000..19f6965b --- /dev/null +++ b/resources/views/inventory/index.blade.php @@ -0,0 +1,157 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('inventory.inventory') ?? 'انبار' }}

+
+ + +
+ +
+
+
+ +
+
+

{{ __('inventory.items') }}

+

{{ persian_num($totalItems) }}

+
+
+
+ + +
+
+
+ +
+
+

{{ __('inventory.active_items') ?? 'اقلام فعال' }}

+

{{ persian_num($activeItems) }}

+
+
+
+ + +
+
+
+ +
+
+

{{ __('inventory.warehouses') }}

+

{{ persian_num($totalWarehouses) }}

+
+
+
+ + +
+
+
+ +
+
+

{{ __('inventory.low_stock_items') ?? 'موجودی کم' }}

+

{{ persian_num($lowStockItems->count()) }}

+
+
+
+
+ + + + +
+ +
+
+

{{ __('inventory.recent_transactions') ?? 'آخرین تراکنش‌ها' }}

+ {{ __('common.view_all') ?? 'مشاهده همه' }} +
+ @if($recentTransactions->count() > 0) +
+ @foreach($recentTransactions as $txn) +
+ + {{ __('inventory.type_' . $txn->type) }} + +
+

{{ $txn->item?->name ?? '-' }}

+

{{ $txn->warehouse?->name ?? '-' }}

+
+
+

+ {{ $txn->type === 'out' ? '-' : '+' }}{{ persian_num(number_format($txn->quantity, 2)) }} +

+

{{ $txn->created_at ? calendar_date($txn->created_at) : '' }}

+
+
+ @endforeach +
+ @else +
+ {{ __('inventory.no_transactions') ?? 'تراکنشی ثبت نشده است' }} +
+ @endif +
+ + +
+
+

{{ __('inventory.low_stock_alert') ?? 'هشدار موجودی' }}

+ {{ __('common.view_all') ?? 'مشاهده همه' }} +
+ @if($lowStockItems->count() > 0) +
+ @foreach($lowStockItems as $item) +
+
+ +
+
+

{{ $item->name }}

+

{{ $item->code ?? '-' }} · {{ $item->unit ?? '-' }}

+
+
+

+ {{ persian_num(number_format($item->current_stock, 2)) }} +

+

{{ __('inventory.min_stock') ?? 'حداقل' }}: {{ persian_num(number_format($item->min_stock ?? 0, 2)) }}

+
+
+ @endforeach +
+ @else +
+ +

{{ __('inventory.all_stock_ok') ?? 'همه اقلام موجودی کافی دارند' }}

+
+ @endif +
+
+
+@endsection diff --git a/resources/views/inventory/inventory-index.blade.php b/resources/views/inventory/inventory-index.blade.php new file mode 100644 index 00000000..b07aebeb --- /dev/null +++ b/resources/views/inventory/inventory-index.blade.php @@ -0,0 +1,138 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('inventory.inventory') ?? 'انبار' }}

+
+ + +
+ +
+
+
+ +
+
+

{{ __('inventory.items') }}

+

{{ persian_num($totalItems) }}

+
+
+
+ + +
+
+
+ +
+
+

{{ __('inventory.active_items') ?? 'اقلام فعال' }}

+

{{ persian_num($activeItems) }}

+
+
+
+ + +
+
+
+ +
+
+

{{ __('inventory.warehouses') }}

+

{{ persian_num($totalWarehouses) }}

+
+
+
+ + +
+
+
+ +
+
+

{{ __('inventory.low_stock_items') ?? 'موجودی کم' }}

+

{{ persian_num($lowStockItems->count()) }}

+
+
+
+
+ +
+ +
+
+

{{ __('inventory.recent_transactions') ?? 'آخرین تراکنش‌ها' }}

+ {{ __('common.view_all') ?? 'مشاهده همه' }} +
+ @if($recentTransactions->count() > 0) +
+ @foreach($recentTransactions as $txn) +
+ + {{ __('inventory.type_' . $txn->type) }} + +
+

{{ $txn->item?->name ?? '-' }}

+

{{ $txn->warehouse?->name ?? '-' }}

+
+
+

+ {{ $txn->type === 'out' ? '-' : '+' }}{{ persian_num(number_format($txn->quantity, 2)) }} +

+

{{ $txn->created_at ? calendar_date($txn->created_at) : '' }}

+
+
+ @endforeach +
+ @else +
+ {{ __('inventory.no_transactions') }} +
+ @endif +
+ + +
+
+

{{ __('inventory.low_stock_alert') ?? 'هشدار موجودی' }}

+ {{ __('common.view_all') ?? 'مشاهده همه' }} +
+ @if($lowStockItems->count() > 0) +
+ @foreach($lowStockItems as $item) +
+
+ +
+
+

{{ $item->name }}

+

{{ $item->code ?? '-' }} · {{ $item->unit ?? '-' }}

+
+
+

+ {{ persian_num(number_format($item->current_stock, 2)) }} +

+

{{ __('inventory.min_stock') ?? 'حداقل' }}: {{ persian_num(number_format($item->min_stock ?? 0, 2)) }}

+
+
+ @endforeach +
+ @else +
+ +

{{ __('inventory.all_stock_ok') ?? 'همه اقلام موجودی کافی دارند' }}

+
+ @endif +
+
+
+@endsection diff --git a/resources/views/inventory/item-form.blade.php b/resources/views/inventory/item-form.blade.php new file mode 100644 index 00000000..2308c0e4 --- /dev/null +++ b/resources/views/inventory/item-form.blade.php @@ -0,0 +1,116 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($item) && $item ? __('inventory.edit_item') : __('inventory.create_item') }} +

+
+ + +
+ @csrf + @if(isset($item) && $item) + @method('PUT') + @endif + +
+ +
+ + + @error('name') +

{{ $message }}

+ @enderror +
+ + +
+ + + @error('code') +

{{ $message }}

+ @enderror +
+ + +
+ + + @error('unit') +

{{ $message }}

+ @enderror +
+ + +
+ + + + @foreach($categories as $cat) + +
+ + +
+ + +

{{ __('inventory.min_stock_help') ?? 'هشدار کاهش موجودی' }}

+
+ + +
+ +
+ + +
+ + +
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/inventory/items.blade.php b/resources/views/inventory/items.blade.php new file mode 100644 index 00000000..5607aa24 --- /dev/null +++ b/resources/views/inventory/items.blade.php @@ -0,0 +1,120 @@ +@extends('layouts.app') + +@section('content') +
+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + @forelse($items as $item) + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('inventory.item_code') }}{{ __('inventory.item_name') }}{{ __('inventory.category') }}{{ __('inventory.unit') }}{{ __('inventory.current_stock') }}{{ __('common.status') }}
{{ $item->code ?? '-' }}{{ $item->name }}{{ $item->category ?? '-' }}{{ $item->unit ?? '-' }} + @php + $stock = $item->current_stock ?? 0; + $minStock = $item->min_stock ?? 0; + @endphp + + {{ persian_num(number_format($stock, 2)) }} + + @if($stock <= $minStock && $stock > 0) + + @elseif($stock <= 0) + + @endif + + + {{ $item->is_active ? __('inventory.active') : __('inventory.inactive') }} + + +
+ + + + @if($item->inventoryTransactions()->count() === 0) +
+ @csrf @method('DELETE') + +
+ @endif +
+
{{ __('inventory.no_items') }}
+
+
+ + +
+ {{ $items->withQueryString()->links() }} +
+
+@endsection diff --git a/resources/views/inventory/show.blade.php b/resources/views/inventory/show.blade.php new file mode 100644 index 00000000..bcc47501 --- /dev/null +++ b/resources/views/inventory/show.blade.php @@ -0,0 +1,77 @@ +@extends('layouts.app') + +@section('title', $item->name) + +@section('content') +
+ +
+
+ + + +
+

{{ $item->name }}

+

{{ $item->code }}

+
+
+
+ + {{ __('common.edit') }} + +
+ @csrf @method('DELETE') + +
+
+
+ +
+ +
+
+
+
+

{{ __('inventory.code') }}

+

{{ $item->code ?? '-' }}

+
+
+

{{ __('inventory.category') }}

+ @if($item->category) + @php + $ck = 'inventory.category_' . $item->category; + $cl = __($ck); + if ($cl === $ck) $cl = $item->category; + @endphp +

{{ $cl }}

+ @else +

-

+ @endif +
+
+

{{ __('inventory.unit') }}

+

{{ $item->unit ?? '-' }}

+
+
+

{{ __('common.status') }}

+ @if($item->is_active) + {{ __('common.active') }} + @else + {{ __('common.inactive') }} + @endif +
+
+ + @if($item->description) +
+

{{ __('common.description') }}

+

{{ $item->description }}

+
+ @endif +
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/inventory/transactions.blade.php b/resources/views/inventory/transactions.blade.php new file mode 100644 index 00000000..6471969e --- /dev/null +++ b/resources/views/inventory/transactions.blade.php @@ -0,0 +1,467 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('inventory.transactions') }}

+
+ + + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + @forelse($transactions as $txn) + + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('inventory.transaction_type') }}{{ __('inventory.item') }}{{ __('inventory.warehouse') }}{{ __('inventory.quantity') }}{{ __('inventory.unit_price') }}{{ __('inventory.total_price') }}{{ __('inventory.reference') }}{{ __('common.date') ?? 'تاریخ' }}
+ + {{ __('inventory.type_' . $txn->type) }} + + {{ $txn->item?->name ?? '-' }}{{ $txn->warehouse?->name ?? '-' }} + {{ $txn->type === 'out' ? '-' : '+' }}{{ persian_num(number_format($txn->quantity, 2)) }} + @if($txn->item?->unit) + {{ $txn->item->unit }} + @endif + + @if($txn->unit_price) + {{ format_currency((float)$txn->unit_price, $txn->currency?->code) }} + @else + - + @endif + + @if($txn->total_price) + {{ format_currency((float)$txn->total_price, $txn->currency?->code) }} + @else + - + @endif + {{ $txn->reference_number ?? $txn->reference ?? '-' }}{{ $txn->created_at ? calendar_date($txn->created_at) : '-' }}
{{ __('inventory.no_transactions') }}
+
+
+ + +
+ {{ $transactions->withQueryString()->links() }} +
+
+ + + + + + + + + + +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/inventory/warehouse-form.blade.php b/resources/views/inventory/warehouse-form.blade.php new file mode 100644 index 00000000..8bb8bbc2 --- /dev/null +++ b/resources/views/inventory/warehouse-form.blade.php @@ -0,0 +1,76 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($warehouse) && $warehouse ? __('inventory.edit_warehouse') : __('inventory.create_warehouse') }} +

+
+ + +
+ @csrf + @if(isset($warehouse) && $warehouse) + @method('PUT') + @endif + +
+ +
+ + + @error('name') +

{{ $message }}

+ @enderror +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/inventory/warehouses.blade.php b/resources/views/inventory/warehouses.blade.php new file mode 100644 index 00000000..20988847 --- /dev/null +++ b/resources/views/inventory/warehouses.blade.php @@ -0,0 +1,99 @@ +@extends('layouts.app') + +@section('content') +
+ + + + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + @forelse($warehouses as $warehouse) + + + + + + + + + @empty + + + + @endforelse + +
{{ __('inventory.warehouse_name') }}{{ __('inventory.location') }}{{ __('inventory.project') }}{{ __('inventory.items_count') ?? 'تعداد اقلام' }}{{ __('common.status') }}
{{ $warehouse->name }}{{ $warehouse->location ?? '-' }}{{ $warehouse->project?->name ?? '-' }}{{ persian_num($warehouse->inventoryTransactions()->distinct('item_id')->count('item_id')) }} + + {{ $warehouse->is_active ? __('inventory.active') : __('inventory.inactive') }} + + +
+ + + + @if($warehouse->inventoryTransactions()->count() === 0) +
+ @csrf @method('DELETE') + +
+ @endif +
+
{{ __('inventory.no_warehouses') }}
+
+
+ + +
+ {{ $warehouses->withQueryString()->links() }} +
+
+@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 00000000..93477c4b --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,96 @@ + + + + + + + + {{ isset($pageTitle) ? $pageTitle . ' - ' : '' }}{{ config('app.name') }} + + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + @stack('styles') + + + @if(session('calendar', 'jalali') === 'jalali') + + @endif + + + + +
+ + + @include('layouts.sidebar') + + +
+ + + @include('layouts.header') + + +
+ @if(session('success')) +
+ {{ session('success') }} + +
+ @endif + + @if(session('error')) +
+ {{ session('error') }} + +
+ @endif + + @yield('content') +
+ + +
+
+ © {{ persian_num(date('Y')) }} Vernova — {{ __('common.all_rights_reserved') }} +
+ + + {{ session('calendar', config('Vernova.defaults.calendar', 'jalali')) === 'jalali' ? __('calendar.jalali') : __('calendar.gregorian') }} + + + + {{ strtoupper(session('currency', config('Vernova.defaults.currency', 'IRR'))) }} + + {{ app()->getLocale() === 'fa' ? 'فارسی' : 'English' }} +
+
+
+
+
+ + @stack('scripts') + + + @if(session('calendar', 'jalali') === 'jalali') + + @endif + + diff --git a/resources/views/layouts/header.blade.php b/resources/views/layouts/header.blade.php new file mode 100644 index 00000000..5fbce96b --- /dev/null +++ b/resources/views/layouts/header.blade.php @@ -0,0 +1,113 @@ + +
+
+ + + + + + + +
+ + +
+ +
+
+ @csrf + +
+
+ @csrf + +
+
+
+ + +
+ +
+
+ @csrf + +
+
+ @csrf + +
+
+
+ + +
+ +
+ @foreach(['IRR', 'USD', 'EUR', 'AED'] as $curr) +
+ @csrf + +
+ @endforeach +
+
+ + + +
+
+
diff --git a/resources/views/layouts/sidebar.blade.php b/resources/views/layouts/sidebar.blade.php new file mode 100644 index 00000000..909b7e94 --- /dev/null +++ b/resources/views/layouts/sidebar.blade.php @@ -0,0 +1,241 @@ + + + + +
diff --git a/resources/views/letters/form.blade.php b/resources/views/letters/form.blade.php new file mode 100644 index 00000000..ea4c895e --- /dev/null +++ b/resources/views/letters/form.blade.php @@ -0,0 +1,141 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($letter) && $letter ? __('letters.edit_letter') : __('letters.create_letter') }} +

+
+ + + @if(isset($parentLetter) && $parentLetter) +
+
+ + {{ __('letters.reply_to') }}: + {{ $parentLetter->subject }} +
+
+ @endif + + +
+ @csrf + @if(isset($letter) && $letter) + @method('PUT') + @endif + +
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + @if(!isset($letter) || !$letter) +
+ + +

{{ __('letters.attachments_help') }}

+
+ @endif + + + @if(isset($parentLetter) && $parentLetter) + + @endif +
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/letters/index.blade.php b/resources/views/letters/index.blade.php new file mode 100644 index 00000000..938c72f5 --- /dev/null +++ b/resources/views/letters/index.blade.php @@ -0,0 +1,139 @@ +@extends('layouts.app') + +@section('content') +
+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + @forelse($letters as $letter) + + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('letters.type') }}{{ __('letters.reference_number') }}{{ __('letters.subject') }}{{ __('letters.sender') }}{{ __('letters.recipient') }}{{ __('common.status') }}{{ __('letters.letter_date') }}
+ + {{ __('letters.type_' . $letter->type) }} + + {{ $letter->reference_number ?? '-' }}{{ $letter->subject }}{{ $letter->sender }}{{ $letter->recipient }} + + {{ __('letters.status_' . $letter->status) }} + + {{ $letter->letter_date ? calendar_date($letter->letter_date) : '-' }} +
+ @if($letter->attachments->count() > 0) + + + + @endif + + + +
+
{{ __('letters.no_letters') }}
+
+
+ + +
+ {{ $letters->withQueryString()->links() }} +
+
+@endsection diff --git a/resources/views/letters/show.blade.php b/resources/views/letters/show.blade.php new file mode 100644 index 00000000..b88eba7b --- /dev/null +++ b/resources/views/letters/show.blade.php @@ -0,0 +1,224 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

{{ __('letters.letter_details') }}

+ + {{ __('letters.type_' . $letter->type) }} + +
+ +
+ +
+ +
+
+

{{ $letter->subject }}

+ + {{ __('letters.status_' . $letter->status) }} + +
+ + @if($letter->content) +
+ {{ $letter->content }} +
+ @endif + + +
+ @can('update', $letter) + + + {{ __('common.edit') }} + + @endcan + @can('archive', $letter) + @if($letter->status !== 'archived') +
+ @csrf @method('PUT') + +
+ @endif + @endcan + + + {{ __('letters.reply') }} + +
+
+ + +
+
+

{{ __('letters.attachments') }}

+ @can('update', $letter) + + @endcan +
+ + @if($letter->attachments->count() > 0) +
+ @foreach($letter->attachments as $attachment) +
+
+ @if(str_starts_with($attachment->mime_type, 'image/')) + + @elseif(str_starts_with($attachment->mime_type, 'application/pdf')) + + @else + + @endif +
+
+

{{ $attachment->file_name }}

+

{{ number_format($attachment->file_size / 1024, 1) }} KB

+
+
+ + + + @can('update', $letter) +
+ @csrf @method('DELETE') + +
+ @endcan +
+
+ @endforeach +
+ @else +

{{ __('letters.no_attachments') }}

+ @endif +
+ + + @if($letter->replies->count() > 0) +
+

{{ __('letters.replies') }}

+
+ @foreach($letter->replies as $reply) +
+
+
+ + {{ __('letters.type_' . $reply->type) }} + + {{ $reply->subject }} +
+ {{ __('letters.view_letter') }} +
+

{{ $reply->letter_date ? calendar_date($reply->letter_date) : '' }} · {{ $reply->reference_number ?? '-' }}

+
+ @endforeach +
+
+ @endif +
+ + +
+
+

{{ __('letters.letter_info') }}

+ +
+

{{ __('letters.reference_number') }}

+

{{ $letter->reference_number ?? '-' }}

+
+ +
+

{{ __('letters.letter_date') }}

+

{{ $letter->letter_date ? calendar_date($letter->letter_date) : '-' }}

+
+ +
+

{{ __('letters.sender') }}

+

{{ $letter->sender }}

+
+ +
+

{{ __('letters.recipient') }}

+

{{ $letter->recipient }}

+
+ + @if($letter->project) +
+

{{ __('letters.project') }}

+ {{ $letter->project->name }} +
+ @endif + + @if($letter->parentLetter) +
+

{{ __('letters.parent_letter') }}

+ {{ $letter->parentLetter->subject }} +
+ @endif + +
+

{{ __('letters.registered_by') }}

+

{{ $letter->createdBy?->name ?? '-' }}

+
+ +
+

{{ __('letters.registered_at') }}

+

{{ $letter->created_at ? calendar_date($letter->created_at) : '-' }}

+
+
+
+
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/payroll/calculate.blade.php b/resources/views/payroll/calculate.blade.php new file mode 100644 index 00000000..ba0e53c4 --- /dev/null +++ b/resources/views/payroll/calculate.blade.php @@ -0,0 +1,170 @@ +@extends('layouts.app') + +@section('content') +
+
+ + + +

{{ __('payroll.new_entry') }}

+
+ + +
+ +
+ + +
+
+ @csrf +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500" required> +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ {{ __('common.cancel') }} + +
+
+
+ + + +
+ + +@endsection diff --git a/resources/views/payroll/index.blade.php b/resources/views/payroll/index.blade.php new file mode 100644 index 00000000..4c8c2d19 --- /dev/null +++ b/resources/views/payroll/index.blade.php @@ -0,0 +1,179 @@ +@extends('layouts.app') + +@section('content') +
+ + + + +
+
+
{{ __('payroll.total_gross') }}
+
{{ format_currency($totalGross) }}
+
+
+
{{ __('payroll.total_deductions') }}
+
{{ format_currency($totalDeductions + $totalAdvances) }}
+
+
+
{{ __('payroll.total_net') }}
+
{{ format_currency($totalNet) }}
+
+
+
{{ __('payroll.unpaid_employees') }}
+
{{ $unpaidEmployeeCount }} / {{ $activeEmployeeCount }}
+
+
+ + +
+
+ {{ __('payroll.payment_status') }} + {{ __('payroll.total_paid') }}: {{ format_currency($totalPaid) }} | {{ __('payroll.total_unpaid') }}: {{ format_currency($totalUnpaid) }} +
+ @php $paidPercent = $totalGross > 0 ? round(($totalPaid / $totalGross) * 100) : 0; @endphp +
+
+
+
{{ $paidPercent }}% {{ __('payroll.paid_percent') }}
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+ @csrf + + +
+ @if($financials->count() > 0) +
+ {{ __('payroll.financial_records') }} — {{ $persianMonths[$month] ?? $month }} {{ $year }} + +
+ @endif +
+ + + + + + + + + + + + + + + @forelse($financials as $financial) + + + + + + + + + + + @empty + + + + @endforelse + +
+ + {{ __('payroll.employee') }}{{ __('payroll.type') }}{{ __('payroll.amount') }}{{ __('payroll.effective_date') }}{{ __('common.status') }}{{ __('common.description') }}
+ @if(!$financial->is_paid) + + @endif + + + {{ $financial->employee->first_name ?? '' }} {{ $financial->employee->last_name ?? '' }} + + + + {{ __('payroll.type_' . $financial->type) }} + + + {{ in_array($financial->type, ['deduction', 'advance']) ? '-' : '' }}{{ number_format($financial->amount) }} + {{ $financial->effective_date ? calendar_date($financial->effective_date) : '-' }} + @if($financial->is_paid) + + + {{ __('payroll.paid') }} + + @else + + {{ __('payroll.unpaid') }} + + @endif + {{ $financial->description ?? '-' }} +
+ @if(!$financial->is_paid) + + + + @endif +
+
{{ __('payroll.no_records') }}
+
+
+
+ + +
+ {{ $financials->withQueryString()->links() }} +
+
+ + +@endsection diff --git a/resources/views/payroll/settlement.blade.php b/resources/views/payroll/settlement.blade.php new file mode 100644 index 00000000..d53932d4 --- /dev/null +++ b/resources/views/payroll/settlement.blade.php @@ -0,0 +1,148 @@ +@extends('layouts.app') + +@section('content') +
+
+ + + +

{{ __('payroll.final_settlement') }}

+
+ +
+ {{ __('payroll.settlement_warning_title') }}
+ {{ __('payroll.settlement_warning') }} +
+ + +
+
+ @csrf +
+
+ + +
+
+ + +
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500" required> +
+
+ + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+
+ + +
+
+ + + + +
+ {{ __('common.cancel') }} + + +
+
+
+
+ + +@endsection diff --git a/resources/views/payroll/show.blade.php b/resources/views/payroll/show.blade.php new file mode 100644 index 00000000..9c56b2f2 --- /dev/null +++ b/resources/views/payroll/show.blade.php @@ -0,0 +1,151 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +
+

{{ $employee->full_name ?? ($employee->first_name . ' ' . $employee->last_name) }}

+

{{ $employee->job_title ?? '' }} — {{ __('payroll.payroll_year', ['year' => $year]) }}

+
+
+ + +
+
+
+
{{ __('employee.contractType') }}
+
{{ __('employee.contract_' . ($employee->contract_type ?? 'full_time')) }}
+
+
+
{{ __('employee.baseSalary') }}
+
{{ number_format($employee->base_salary ?? 0) }}
+
+
+
{{ __('employee.hireDate') }}
+
{{ $employee->hire_date ? calendar_date($employee->hire_date) : '-' }}
+
+
+
{{ __('employee.status') }}
+
+ + {{ __('employee.status_' . $employee->status) }} + +
+
+
+
+ + +
+
+
{{ __('payroll.total_gross') }}
+
{{ number_format($yearlyGross) }}
+
+
+
{{ __('payroll.total_deductions') }}
+
{{ number_format($yearlyDeductions + $yearlyAdvances) }}
+
+
+
{{ __('payroll.total_net') }}
+
{{ number_format($yearlyNet) }}
+
+
+
{{ __('payroll.total_paid') }}
+
{{ number_format($yearlyPaid) }}
+
+
+
{{ __('payroll.total_unpaid') }}
+
{{ number_format($yearlyUnpaid) }}
+
+
+ + +
+
+
+ + +
+ +
+
+ + +
+ @foreach(range(1, 12) as $m) + @isset($financials[$m]) +
+
+

{{ $persianMonths[$m] ?? $m }}

+ @php + $monthFinancials = $financials[$m]; + $monthGross = $monthFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); + $monthDeductions = $monthFinancials->where('type', 'deduction')->sum('amount'); + $monthAdvances = $monthFinancials->where('type', 'advance')->sum('amount'); + $monthNet = $monthGross - $monthDeductions - $monthAdvances; + $monthPaid = $monthFinancials->where('is_paid', true)->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); + @endphp +
+ {{ __('payroll.total_gross') }}: {{ number_format($monthGross) }} + {{ __('payroll.total_deductions') }}: {{ number_format($monthDeductions + $monthAdvances) }} + {{ __('payroll.total_net') }}: {{ number_format($monthNet) }} + @if($monthPaid < $monthGross) + {{ __('payroll.unpaid') }}: {{ number_format($monthGross - $monthPaid) }} + @endif +
+
+ + + @foreach($monthFinancials as $f) + + + + + + + + @endforeach + +
+ + {{ __('payroll.type_' . $f->type) }} + + + {{ in_array($f->type, ['deduction', 'advance']) ? '-' : '' }}{{ number_format($f->amount) }} + {{ $f->description ?? '-' }}{{ $f->effective_date ? calendar_date($f->effective_date) : '-' }} + @if($f->is_paid) + + + {{ __('payroll.paid') }} + + @else + {{ __('payroll.pay') }} + @endif +
+
+ @endisset + @endforeach + + @if($financials->isEmpty()) +
+ {{ __('payroll.no_records_year') }} +
+ @endif +
+
+@endsection diff --git a/resources/views/petty-cashes/PettyCashController.php b/resources/views/petty-cashes/PettyCashController.php new file mode 100644 index 00000000..6cf5347a --- /dev/null +++ b/resources/views/petty-cashes/PettyCashController.php @@ -0,0 +1,255 @@ +authorize('viewAny', PettyCash::class); + + $query = PettyCash::with(['project', 'currency', 'requestedBy', 'approvedBy'])->withCount('attachments'); + + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + $query->orderBy('request_date', 'desc'); + + $pettyCashes = $query->paginate(20)->appends($request->query()); + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $statuses = ['pending', 'approved', 'rejected', 'settled']; + + return view('petty-cashes.index', compact('pettyCashes', 'projects', 'statuses')); + } + + /** + * Show the form for creating a new petty cash request. + */ + public function create() + { + $this->authorize('create', PettyCash::class); + + $pettyCash = null; + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('petty-cashes.form', compact('pettyCash', 'projects', 'currencies', 'baseCurrency')); + } + + /** + * Store a newly created petty cash request. + */ + public function store(Request $request) + { + $this->authorize('create', PettyCash::class); + + $validated = $request->validate([ + 'project_id' => 'required|exists:projects,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'required|exists:currencies,id', + 'request_date' => 'required|date', + 'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + 'attachments' => 'nullable|array|max:5', + 'attachments.*' => 'file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + ]); + + $validated['tenant_id'] = $this->getTenantId(); + $validated['requested_by'] = Auth::id(); + $validated['status'] = 'pending'; + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($validated['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $validated['original_amount'] = $validated['amount']; + $validated['amount'] = CurrencyHelper::convert( + (float) $validated['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload (legacy) + if ($request->hasFile('receipt')) { + $validated['receipt_path'] = $request->file('receipt')->store('receipts/petty-cash', 'public'); + } + + $pettyCash = PettyCash::create($validated); + + // Handle multiple file attachments + if ($request->hasFile('attachments')) { + $pettyCash->attachFiles($request->file('attachments'), 'receipt'); + } + + return redirect() + ->route('petty-cashes.show', $pettyCash) + ->with('success', __('pettyCash.created_successfully')); + } + + /** + * Display the specified petty cash request. + */ + public function show(PettyCash $pettyCash) + { + $this->authorize('view', $pettyCash); + + $pettyCash->load(['project', 'currency', 'requestedBy', 'approvedBy', 'expenses', 'attachments']); + + return view('petty-cashes.show', compact('pettyCash')); + } + + /** + * Show the form for editing the specified petty cash request. + */ + public function edit(PettyCash $pettyCash) + { + $this->authorize('update', $pettyCash); + + $projects = Project::whereIn('status', ['planning', 'active'])->get(); + $currencies = Currency::where('is_active', true)->get(); + $baseCurrency = CurrencyHelper::getBaseCurrency(); + + return view('petty-cashes.form', compact('pettyCash', 'projects', 'currencies', 'baseCurrency')); + } + + /** + * Update the specified petty cash request. + */ + public function update(Request $request, PettyCash $pettyCash) + { + $this->authorize('update', $pettyCash); + + $validated = $request->validate([ + 'project_id' => 'required|exists:projects,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'amount' => 'required|numeric|min:0', + 'currency_id' => 'required|exists:currencies,id', + 'request_date' => 'required|date', + 'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + 'attachments' => 'nullable|array|max:5', + 'attachments.*' => 'file|mimes:jpg,jpeg,png,pdf,doc,docx|max:10240', + ]); + + // Handle currency conversion + $baseCurrency = CurrencyHelper::getBaseCurrency(); + $selectedCurrency = Currency::find($validated['currency_id']); + + if ($selectedCurrency && $baseCurrency && $selectedCurrency->id !== $baseCurrency->id) { + $validated['original_amount'] = $validated['amount']; + $validated['amount'] = CurrencyHelper::convert( + (float) $validated['amount'], + $selectedCurrency->code, + $baseCurrency->code + ); + } + + // Handle receipt upload + if ($request->hasFile('receipt')) { + // Delete old receipt if exists + if ($pettyCash->receipt_path) { + \Illuminate\Support\Facades\Storage::disk('public')->delete($pettyCash->receipt_path); + } + $validated['receipt_path'] = $request->file('receipt')->store('receipts/petty-cash', 'public'); + } + + $pettyCash->update($validated); + + // Handle multiple file attachments + if ($request->hasFile('attachments')) { + $pettyCash->attachFiles($request->file('attachments'), 'receipt'); + } + + return redirect() + ->route('petty-cashes.show', $pettyCash) + ->with('success', __('pettyCash.updated_successfully')); + } + + /** + * Remove the specified petty cash request. + */ + public function destroy(PettyCash $pettyCash) + { + $this->authorize('delete', $pettyCash); + + $pettyCash->delete(); + + return redirect() + ->route('petty-cashes.index') + ->with('success', __('pettyCash.deleted_successfully')); + } + + /** + * Approve a petty cash request. + */ + public function approve(PettyCash $pettyCash) + { + $this->authorize('approve', $pettyCash); + + $pettyCash->update([ + 'status' => 'approved', + 'approved_by' => Auth::id(), + 'approval_date' => now(), + ]); + + return response()->json([ + 'success' => true, + 'message' => __('pettyCash.approved_successfully'), + ]); + } + + /** + * Reject a petty cash request. + */ + public function reject(PettyCash $pettyCash) + { + $this->authorize('approve', $pettyCash); + + $pettyCash->update([ + 'status' => 'rejected', + 'approved_by' => Auth::id(), + 'approval_date' => now(), + ]); + + return response()->json([ + 'success' => true, + 'message' => __('pettyCash.rejected_successfully'), + ]); + } + + /** + * Settle a petty cash request (mark as settled after usage). + */ + public function settle(PettyCash $pettyCash) + { + $this->authorize('approve', $pettyCash); + + $pettyCash->update([ + 'status' => 'settled', + ]); + + return response()->json([ + 'success' => true, + 'message' => __('pettyCash.settled_successfully'), + ]); + } +} diff --git a/resources/views/petty-cashes/create.blade.php b/resources/views/petty-cashes/create.blade.php new file mode 100644 index 00000000..c44f5924 --- /dev/null +++ b/resources/views/petty-cashes/create.blade.php @@ -0,0 +1,18 @@ +@extends('layouts.app') + +@section('title', __('petty_cash.create')) + +@section('content') +
+
+

{{ __('petty_cash.create') }}

+
+ +
+
+ @csrf + @include('petty-cashes.form') +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/petty-cashes/expense-form.blade.php b/resources/views/petty-cashes/expense-form.blade.php new file mode 100644 index 00000000..5ee2126f --- /dev/null +++ b/resources/views/petty-cashes/expense-form.blade.php @@ -0,0 +1,112 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

{{ __('pettyCash.add_expense') }}

+
+ + +
+
+
+

{{ $pettyCash->title }}

+

{{ $pettyCash->project?->name }}

+
+
+

{{ __('pettyCash.remaining') }}

+

{{ format_currency((float)$pettyCash->remaining, $pettyCash->currency?->code) }}

+
+
+
+ + +
+ @csrf + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

{{ __('pettyCash.currency_conversion_note') }}

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

{{ __('pettyCash.receipt_help') }}

+
+ + +
+ + +

{{ __('attachment.max_files_help') }}

+
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/petty-cashes/form.blade.php b/resources/views/petty-cashes/form.blade.php new file mode 100644 index 00000000..164a0f40 --- /dev/null +++ b/resources/views/petty-cashes/form.blade.php @@ -0,0 +1,108 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($pettyCash) ? __('pettyCash.edit') : __('pettyCash.create') }} +

+
+ + +
+ @csrf + @if(isset($pettyCash)) + @method('PUT') + @endif + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

{{ __('pettyCash.currency_conversion_note') }}

+
+ + +
+ +
+ + +
+ + +
+ + +
+ + @if(isset($pettyCash) && $pettyCash?->receipt_path) + + @endif + +

{{ __('pettyCash.receipt_help') }}

+
+ + +
+ + +

{{ __('attachment.max_files_help') }}

+
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/petty-cashes/index.blade.php b/resources/views/petty-cashes/index.blade.php new file mode 100644 index 00000000..d38ad020 --- /dev/null +++ b/resources/views/petty-cashes/index.blade.php @@ -0,0 +1,164 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('pettyCash.pettyCash') }}

+ + + {{ __('pettyCash.create') }} + +
+ + +
+
+

{{ __('pettyCash.total_amount') }}

+

{{ format_currency((float)$totalAmount) }}

+
+
+

{{ __('pettyCash.total_expenses') }}

+

{{ format_currency((float)$totalExpenses) }}

+
+
+

{{ __('pettyCash.total_remaining') }}

+

{{ format_currency((float)$totalRemaining) }}

+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {{ __('common.reset') }} +
+
+ + +
+
+ + + + + + + + + + + + + + + @forelse($pettyCashes as $pc) + + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('pettyCash.title') }}{{ __('pettyCash.project') }}{{ __('pettyCash.amount') }}{{ __('pettyCash.total_expenses') }}{{ __('pettyCash.remaining') }}{{ __('pettyCash.request_date') }}{{ __('pettyCash.status') }}{{ __('common.actions') }}
+ {{ $pc->title ?? '-' }} + {{ $pc->project?->name ?? '-' }} + {{ format_currency((float)$pc->amount, $pc->currency?->code) }} + + {{ format_currency((float)$pc->total_expenses, $pc->currency?->code) }} + + {{ $pc->remaining !== null ? format_currency((float)$pc->remaining, $pc->currency?->code) : '-' }} + {{ $pc->request_date ? calendar_date($pc->request_date) : '-' }} + + {{ $pc->status_label }} + + +
+ @if($pc->status === 'pending') + + + @elseif($pc->status === 'approved') + {{ __('pettyCash.add_expense') }} + + @endif +
+
{{ __('pettyCash.no_petty_cashes') }}
+
+
+ + +
+ {{ $pettyCashes->withQueryString()->links() }} +
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/petty-cashes/petty-cash-index.blade.php b/resources/views/petty-cashes/petty-cash-index.blade.php new file mode 100644 index 00000000..ff279627 --- /dev/null +++ b/resources/views/petty-cashes/petty-cash-index.blade.php @@ -0,0 +1,165 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('pettyCash.pettyCash') }}

+ + + {{ __('pettyCash.create') }} + +
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + @forelse($pettyCashes as $pc) + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('pettyCash.title') }}{{ __('pettyCash.project') }}{{ __('pettyCash.amount') }}{{ __('pettyCash.remaining') }}{{ __('pettyCash.request_date') }}{{ __('pettyCash.status') }}{{ __('common.actions') }}
+
+ + {{ $pc->title ?? '-' }} + + {{-- Attachment indicator --}} + @if($pc->attachments_count > 0 || $pc->receipt_path) + + + @if($pc->attachments_count > 0) + {{ $pc->attachments_count }} + @endif + + @endif +
+
{{ $pc->project?->name ?? '-' }} + {{ format_currency((float)$pc->amount, $pc->currency?->code) }} + @if($pc->original_amount && $pc->original_amount != $pc->amount) + ({{ format_currency((float)$pc->original_amount, $pc->currency?->code) }}) + @endif + + @if($pc->remaining !== null) + {{ format_currency((float)$pc->remaining, $pc->currency?->code) }} + @else + - + @endif + {{ $pc->request_date ? calendar_date($pc->request_date) : '-' }} + + {{ $pc->status_label }} + + +
+ {{-- View button --}} + + + + {{-- Edit button --}} + + + + @if($pc->status === 'pending') + + + @elseif($pc->status === 'approved') + + @endif +
+
{{ __('pettyCash.no_petty_cashes') }}
+
+
+ + +
+ {{ $pettyCashes->withQueryString()->links() }} +
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/petty-cashes/show.blade.php b/resources/views/petty-cashes/show.blade.php new file mode 100644 index 00000000..3a66abf1 --- /dev/null +++ b/resources/views/petty-cashes/show.blade.php @@ -0,0 +1,291 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

{{ $pettyCash->title ?? __('pettyCash.pettyCash') }}

+ + {{ $pettyCash->status_label }} + +
+ + +
+
+

{{ __('pettyCash.amount') }}

+

{{ format_currency((float)$pettyCash->amount, $pettyCash->currency?->code) }}

+ @if($pettyCash->original_amount && $pettyCash->original_amount != $pettyCash->amount) +

{{ __('pettyCash.original_amount') }}: {{ format_currency((float)$pettyCash->original_amount, $pettyCash->currency?->code) }}

+ @endif +
+
+

{{ __('pettyCash.total_expenses') }}

+

{{ format_currency((float)$pettyCash->total_expenses, $pettyCash->currency?->code) }}

+
+
+

{{ __('pettyCash.remaining') }}

+

+ {{ format_currency((float)$pettyCash->remaining, $pettyCash->currency?->code) }} +

+
+
+ + +
+
+ {{ __('pettyCash.utilization') }} + {{ $pettyCash->utilization }}% +
+
+
+
+
+ + +
+
+
+

{{ __('pettyCash.project') }}

+

{{ $pettyCash->project?->name ?? '-' }}

+
+
+

{{ __('pettyCash.request_date') }}

+

{{ $pettyCash->request_date ? calendar_date($pettyCash->request_date) : '-' }}

+
+
+

{{ __('pettyCash.requested_by') }}

+

{{ $pettyCash->requestedBy?->name ?? '-' }}

+
+ @if($pettyCash->approved_by) +
+

{{ __('pettyCash.approved_by') }}

+

{{ $pettyCash->approvedBy?->name ?? '-' }}

+
+ @endif + @if($pettyCash->approval_date) +
+

{{ __('pettyCash.approval_date') }}

+

{{ calendar_date($pettyCash->approval_date) }}

+
+ @endif + @if($pettyCash->description) +
+

{{ __('pettyCash.description') }}

+

{{ $pettyCash->description }}

+
+ @endif +
+ + +
+ @if($pettyCash->status === 'pending') + + + @elseif($pettyCash->status === 'approved') + + + {{ __('pettyCash.add_expense') }} + + + @endif + @if($pettyCash->status === 'pending') + + {{ __('common.edit') }} + + @endif +
+
+ + +
+
+

{{ __('pettyCash.expenses') }} ({{ $pettyCash->expenses->count() }})

+ @if($pettyCash->status === 'approved') + + + {{ __('pettyCash.add_expense') }} + + @endif +
+ @if($pettyCash->expenses->count() > 0) +
+ + + + + + + + + + + + @foreach($pettyCash->expenses as $expense) + + + + + + + + @endforeach + +
{{ __('expense.title') }}{{ __('expense.category') }}{{ __('expense.amount') }}{{ __('expense.date') }}{{ __('expense.status') }}
{{ $expense->title ?? '-' }}{{ __('expense.category_' . $expense->category) }}{{ format_currency((float)$expense->amount, $expense->currency?->code) }}{{ $expense->expense_date ? calendar_date($expense->expense_date) : '-' }} + + {{ $expense->status_label ?? $expense->status }} + +
+
+ @else +
+ {{ __('pettyCash.no_expenses_yet') }} +
+ @endif +
+ + +
+
+

{{ __('attachment.attachments') }}

+ +
+
+ @if($pettyCash->attachments->count() > 0) +
+ @foreach($pettyCash->attachments as $attachment) +
+
+ @if($attachment->is_image) +
+ {{ $attachment->file_name }} +
+ @elseif($attachment->is_pdf) +
+ +
+ @else +
+ +
+ @endif +
+ {{ $attachment->file_name }} +

{{ $attachment->formatted_size }}

+
+
+ +
+ @endforeach +
+ @else +
+ {{ __('attachment.no_attachments') }} +
+ @endif +
+
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/projects/form.blade.php b/resources/views/projects/form.blade.php new file mode 100644 index 00000000..8b4a585d --- /dev/null +++ b/resources/views/projects/form.blade.php @@ -0,0 +1,118 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($project) ? __('project.edit_project') : __('project.create_project') }} +

+
+ + +
+ @csrf + @if(isset($project)) + @method('PUT') + @endif + +
+ +
+ + + @error('name')

{{ $message }}

@enderror +
+ + +
+ + + @error('code')

{{ $message }}

@enderror +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/projects/index.blade.php b/resources/views/projects/index.blade.php new file mode 100644 index 00000000..d809dc36 --- /dev/null +++ b/resources/views/projects/index.blade.php @@ -0,0 +1,95 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('project.projects') }}

+ + + {{ __('project.new') }} + +
+ + +
+
+
+ + +
+
+ + +
+ + {{ __('common.reset') }} +
+
+ + + + + +
+ {{ $projects->withQueryString()->links() }} +
+
+@endsection diff --git a/resources/views/projects/show.blade.php b/resources/views/projects/show.blade.php new file mode 100644 index 00000000..644a798c --- /dev/null +++ b/resources/views/projects/show.blade.php @@ -0,0 +1,208 @@ +@extends('layouts.app') + +@section('content') +
+ +
+
+ + + +
+

{{ $project->name }}

+

{{ $project->code }}

+
+
+
+ + {{ __('common.edit') }} + +
+ @csrf @method('DELETE') + +
+
+
+ + +
+ +
+ +
+
+
+

{{ __('project.status') }}

+ + {{ $project->status_label }} + +
+
+

{{ __('project.budget') }}

+

{{ format_currency((float)$project->budget) }}

+
+
+

{{ __('project.manager') }}

+

{{ $project->manager?->name ?? '-' }}

+
+
+

{{ __('project.progress') }}

+
+
+
+
+
+ {{ $project->progress }}% +
+
+
+
+

{{ __('project.start_date') }}

+

{{ $project->start_date ? calendar_date($project->start_date) : '-' }}

+
+
+

{{ __('project.end_date') }}

+

{{ $project->end_date ? calendar_date($project->end_date) : '-' }}

+
+
+

{{ __('project.location') }}

+

{{ $project->location ?? '-' }}

+
+
+

{{ __('project.currency') }}

+

{{ $project->defaultCurrency?->code ?? '-' }}

+
+
+ + @if($project->description) +
+

{{ __('project.description') }}

+

{{ $project->description }}

+
+ @endif +
+ + @php + $projectEmployees = $project->tasks->pluck('assignee')->filter()->unique('id'); + @endphp + + + +
+
+ +
+ +
+ +
+ @forelse($project->tasks as $task) +
+ + {{ $task->title }} + + {{ $task->status_label }} + +
+ @empty +

{{ __('task.no_tasks') }}

+ @endforelse +
+ + +
+ @forelse($project->expenses as $expense) +
+ {{ $expense->title }} + {{ format_currency((float)$expense->amount, $expense->currency?->code) }} + {{ $expense->status_label }} +
+ @empty +

{{ __('expense.no_expenses') }}

+ @endforelse +
+ + + +
+
+
+ + +
+ +
+

{{ __('project.task_overview') }}

+
+
+ {{ __('task.status_pending') }} + {{ $taskStats['pending'] }} +
+
+ {{ __('task.status_in_progress') }} + {{ $taskStats['in_progress'] }} +
+
+ {{ __('task.status_review') }} + {{ $taskStats['review'] }} +
+
+ {{ __('task.status_done') }} + {{ $taskStats['done'] }} +
+
+
+ + +
+

{{ __('project.team') }}

+
+ @forelse($project->users as $member) +
+
+ {{ mb_strtoupper(mb_substr($member->name, 0, 1)) }} +
+ {{ $member->name }} +
+ @empty +

{{ __('project.no_team_members') }}

+ @endforelse +
+
+
+
+
+@endsection diff --git a/resources/views/reports/employee-worklog.blade.php b/resources/views/reports/employee-worklog.blade.php new file mode 100644 index 00000000..6bc34c72 --- /dev/null +++ b/resources/views/reports/employee-worklog.blade.php @@ -0,0 +1,13 @@ +@extends('layouts.app') + +@section('content') +
+
+ + + +

{{ __('reports.employee_worklog') }}

+
+

{{ __('reports.detailed_view_coming_soon') }}

+
+@endsection diff --git a/resources/views/reports/financial-pdf.blade.php b/resources/views/reports/financial-pdf.blade.php new file mode 100644 index 00000000..1c7aa2ac --- /dev/null +++ b/resources/views/reports/financial-pdf.blade.php @@ -0,0 +1,295 @@ + + + + + + {{ __('reports.financial') }} - Vernova + + + +
+ +
+

{{ __('reports.financial') }}

+
{{ __('reports.financial_description') }}
+
+ + +
+
+ {{ __('reports.date_range') }}: + + @isset($dateFrom) + {{ calendar_date($dateFrom) }} + @else + — + @endisset +  {{ __('reports.to') }}  + @isset($dateTo) + {{ calendar_date($dateTo) }} + @else + — + @endisset + +
+
+ {{ __('reports.generated_at') }}: + {{ calendar_date(now()) }} - {{ persian_num(now()->format('H:i')) }} +
+
+ + + @if(count($reportData ?? []) > 0) + + + + + + + + + + + + + + + + @foreach($reportData as $index => $row) + + + + + + + + + + + + @endforeach + + + + + + + + + + + + + +
#{{ __('project.code') }}{{ __('project.name') }}{{ __('project.budget') }}{{ __('reports.total_expenses') }}{{ __('reports.total_petty_cash') }}{{ __('reports.remaining') }}{{ __('reports.utilization') }}{{ __('project.manager') }}
{{ persian_num($loop->iteration) }}{{ $row['code'] ?? '-' }}{{ $row['name'] ?? '-' }}{{ format_currency((float)($row['budget'] ?? 0)) }}{{ format_currency((float)($row['total_expenses'] ?? 0)) }}{{ format_currency((float)($row['total_petty_cash'] ?? 0)) }} + {{ format_currency((float)($row['remaining'] ?? 0)) }} + +
+
+
+
+ {{ persian_num(round($row['utilization'] ?? 0)) }}% +
+
{{ $row['manager'] ?? '-' }}
{{ __('reports.total') }}{{ format_currency((float)($summary['total_budget'] ?? 0)) }}{{ format_currency((float)($summary['total_expenses'] ?? 0)) }}{{ format_currency((float)($summary['total_petty_cash'] ?? 0)) }} + {{ format_currency((float)($summary['total_remaining'] ?? 0)) }} + + {{ persian_num(round($summary['avg_utilization'] ?? 0)) }}% +
+ @else +
+ {{ __('reports.no_financial_data') }} +
+ @endif + + + +
+ + diff --git a/resources/views/reports/financial.blade.php b/resources/views/reports/financial.blade.php new file mode 100644 index 00000000..65b0fbc7 --- /dev/null +++ b/resources/views/reports/financial.blade.php @@ -0,0 +1,30 @@ +@extends('layouts.app') + +@section('content') +
+
+ + + +

{{ __('reports.financial') }}

+
+ + +
+
+

{{ __('reports.total_expenses') }}

+

{{ number_format($totalAmount) }}

+
+
+

{{ __('reports.approved_amount') }}

+

{{ number_format($approvedAmount) }}

+
+
+

{{ __('reports.pending_amount') }}

+

{{ number_format($pendingAmount) }}

+
+
+ +

{{ __('reports.detailed_view_coming_soon') }}

+
+@endsection diff --git a/resources/views/reports/index.blade.php b/resources/views/reports/index.blade.php new file mode 100644 index 00000000..9364ce38 --- /dev/null +++ b/resources/views/reports/index.blade.php @@ -0,0 +1,41 @@ +@extends('layouts.app') + +@section('content') + +@endsection diff --git a/resources/views/reports/inventory.blade.php b/resources/views/reports/inventory.blade.php new file mode 100644 index 00000000..4d98aaff --- /dev/null +++ b/resources/views/reports/inventory.blade.php @@ -0,0 +1,13 @@ +@extends('layouts.app') + +@section('content') +
+
+ + + +

{{ __('reports.inventory') }}

+
+

{{ __('reports.detailed_view_coming_soon') }}

+
+@endsection diff --git a/resources/views/reports/payroll.blade.php b/resources/views/reports/payroll.blade.php new file mode 100644 index 00000000..2344712c --- /dev/null +++ b/resources/views/reports/payroll.blade.php @@ -0,0 +1,13 @@ +@extends('layouts.app') + +@section('content') +
+
+ + + +

{{ __('reports.payroll') }}

+
+

{{ __('reports.detailed_view_coming_soon') }}

+
+@endsection diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php new file mode 100644 index 00000000..7679fd02 --- /dev/null +++ b/resources/views/settings/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

{{ __('settings.settings') }}

+

{{ __('settings.full_settings_coming_soon') }}

+
+@endsection diff --git a/resources/views/settings/partials/currencies.blade.php b/resources/views/settings/partials/currencies.blade.php new file mode 100644 index 00000000..60f50940 --- /dev/null +++ b/resources/views/settings/partials/currencies.blade.php @@ -0,0 +1,173 @@ +
+ +
+ +
+ + +
+ + + + + + + + + + + + + + @forelse($allCurrencies as $currency) + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('settings.currency_code') }}{{ __('settings.currency_name') }}{{ __('settings.currency_symbol') }}{{ __('settings.exchange_rate') }}{{ __('settings.decimal_places') }}{{ __('common.status') }}{{ __('common.actions') }}
{{ $currency->code }}{{ $currency->name }}{{ $currency->symbol }}{{ number_format($currency->exchange_rate_to_usd, 4) }}{{ persian_num($currency->decimal_places) }} + + {{ $currency->is_active ? __('settings.active') : __('settings.inactive') }} + + +
+ +
+ @csrf @method('PATCH') + +
+
+
{{ __('settings.no_currencies') }}
+
+
+ + + + + + + +@push('scripts') + +@endpush diff --git a/resources/views/settings/partials/organization.blade.php b/resources/views/settings/partials/organization.blade.php new file mode 100644 index 00000000..bdc67aff --- /dev/null +++ b/resources/views/settings/partials/organization.blade.php @@ -0,0 +1,84 @@ +
+ @csrf + @method('PUT') + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ allow_multi_currency ?? true) ? 'checked' : '' }} + class="w-4 h-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500"> + +
+
+ + +
+ +
+ @if($tenant?->logo_path) + Logo + @endif + +
+

{{ __('settings.logo_help') }}

+
+ +
+ +
+
diff --git a/resources/views/settings/partials/users.blade.php b/resources/views/settings/partials/users.blade.php new file mode 100644 index 00000000..90ad6739 --- /dev/null +++ b/resources/views/settings/partials/users.blade.php @@ -0,0 +1,203 @@ +
+ +
+ +
+ + +
+ + + + + + + + + + + + @forelse($users as $user) + + + + + + + + @empty + + + + @endforelse + +
{{ __('settings.user_name') }}{{ __('settings.user_email') }}{{ __('settings.user_role') }}{{ __('common.status') }}{{ __('common.actions') }}
+
+
+ {{ mb_strtoupper(mb_substr($user->name, 0, 1)) }} +
+ {{ $user->name }} +
+
{{ $user->email }} + @foreach($user->roles as $role) + + {{ $role->name }} + + @endforeach + @if($user->roles->isEmpty()) + + @endif + + + {{ $user->is_active ? __('settings.active') : __('settings.inactive') }} + + +
+ + @if($user->id !== auth()->id()) +
+ @csrf @method('PATCH') + +
+ @endif +
+
{{ __('settings.no_users') }}
+
+
+ + + + + + + +@push('scripts') + +@endpush diff --git a/resources/views/tasks/form.blade.php b/resources/views/tasks/form.blade.php new file mode 100644 index 00000000..974d1394 --- /dev/null +++ b/resources/views/tasks/form.blade.php @@ -0,0 +1,113 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +

+ {{ isset($task) ? __('task.edit_task') : __('task.create_task') }} +

+
+ + +
+ @csrf + @if(isset($task)) + @method('PUT') + @endif + +
+ +
+ + + @error('title')

{{ $message }}

@enderror +
+ + +
+ + + @error('project_id')

{{ $message }}

@enderror +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection diff --git a/resources/views/tasks/gantt.blade.php b/resources/views/tasks/gantt.blade.php new file mode 100644 index 00000000..4b10e7c5 --- /dev/null +++ b/resources/views/tasks/gantt.blade.php @@ -0,0 +1,150 @@ +@extends('layouts.app') + +@section('content') +
+ +
+

{{ __('task.gantt') }}

+
+ +
+
+ + @if(empty($ganttData)) +
+ +

{{ __('task.gantt_no_tasks') }}

+
+ @else + +
+
+
+ + +
+

{{ __('task.gantt_legend') }}

+
+ {{ __('task.status_pending') }} + {{ __('task.status_in_progress') }} + {{ __('task.status_review') }} + {{ __('task.status_done') }} + {{ __('task.status_cancelled') }} +
+
+ @endif +
+ +@if(!empty($ganttData)) +@push('styles') + + +@endpush + +@push('scripts') + + + +@endpush +@endif +@endsection diff --git a/resources/views/tasks/index.blade.php b/resources/views/tasks/index.blade.php new file mode 100644 index 00000000..b862c114 --- /dev/null +++ b/resources/views/tasks/index.blade.php @@ -0,0 +1,135 @@ +@extends('layouts.app') + +@section('content') +
+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {{ __('common.reset') }} +
+
+ + +
+
+ @forelse($tasks as $task) +
+ + + + +
+ {{ $task->title }} +
+ {{ $task->project?->name }} + @if($task->due_date) + + {{ calendar_date($task->due_date) }} + @endif + @if($task->assignee) + + {{ $task->assignee->name }} + @endif +
+
+ + + +
+ @empty +
+ +

{{ __('task.no_tasks') }}

+
+ @endforelse +
+
+ + +
+ {{ $tasks->withQueryString()->links() }} +
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/tasks/kanban.blade.php b/resources/views/tasks/kanban.blade.php new file mode 100644 index 00000000..3cc2e486 --- /dev/null +++ b/resources/views/tasks/kanban.blade.php @@ -0,0 +1,153 @@ +@extends('layouts.app') + +@section('content') +
+ +
+
+

{{ __('task.kanban') }}

+ {{ __('task.listView') }} +
+
+ + + + {{ __('task.create') }} + +
+
+ + +
+ @foreach([ + 'pending' => ['color' => 'gray', 'icon' => 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'], + 'in_progress' => ['color' => 'blue', 'icon' => 'M13 10V3L4 14h7v7l9-11h-7z'], + 'review' => ['color' => 'amber', 'icon' => 'M15 12a3 3 0 11-6 0 3 3 0 016 0z'], + 'done' => ['color' => 'emerald', 'icon' => 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'] + ] as $status => $config) +
+ +
+
+ + + +
+

{{ __('task.status_' . $status) }}

+ {{ $kanbanColumns[$status]->count() }} +
+ + +
+ @forelse($kanbanColumns[$status] as $task) +
+
+ + {{ $task->title }} + + + {{ __('task.' . $task->priority) }} + +
+ + @if($task->project) +

{{ $task->project->name }}

+ @endif + +
+
+ @if($task->due_date) + + + {{ calendar_date($task->due_date) }} + + @endif +
+
+ @if($task->assignee) +
+ {{ mb_strtoupper(mb_substr($task->assignee->name, 0, 1)) }} +
+ @endif + {{-- Quick status change buttons --}} +
+ @if($status !== 'pending') + + @endif + @if($status !== 'in_progress') + + @endif + @if($status !== 'review') + + @endif + @if($status !== 'done') + + @endif +
+
+
+
+ @empty +
+ {{ __('task.no_tasks') }} +
+ @endforelse +
+
+ @endforeach +
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/tasks/show.blade.php b/resources/views/tasks/show.blade.php new file mode 100644 index 00000000..e052699a --- /dev/null +++ b/resources/views/tasks/show.blade.php @@ -0,0 +1,158 @@ +@extends('layouts.app') + +@section('content') +
+ +
+ + + +
+

{{ $task->title }}

+
+ @if($task->project) + {{ $task->project->name }} + + @endif + + {{ __('task.status_' . $task->status) }} + + + {{ __('task.' . $task->priority) }} + +
+
+ +
+ + +
+
+
+

{{ __('task.assignee') }}

+

{{ $task->assignee?->name ?? '-' }}

+
+
+

{{ __('task.dueDate') }}

+

+ {{ $task->due_date ? calendar_date($task->due_date) : '-' }} +

+
+ @if($task->start_date) +
+

{{ __('project.startDate') }}

+

{{ calendar_date($task->start_date) }}

+
+ @endif + @if($task->completed_at) +
+

{{ __('task.completedAt') }}

+

{{ calendar_date($task->completed_at) }}

+
+ @endif + @if($task->description) +
+

{{ __('task.description') }}

+

{{ $task->description }}

+
+ @endif +
+ + +
+

{{ __('task.status') }}:

+ @foreach(['pending', 'in_progress', 'review', 'done'] as $s) + + @endforeach +
+
+ + + @if($task->children->count() > 0) +
+
+

زیروظایف

+
+
+ @foreach($task->children as $child) +
+ + {{ __('task.' . $child->priority) }} + + {{ $child->title }} + + {{ __('task.status_' . $child->status) }} + + {{ $child->assignee?->name }} +
+ @endforeach +
+
+ @endif + + + @if($task->logs->count() > 0) +
+
+

{{ __('task.activityLog') }}

+
+
+ @foreach($task->logs as $log) +
+
+ {{ $log->user?->name ?? '-' }} + {{ $log->created_at ? calendar_date($log->created_at) : '' }} +
+

+ @if($log->field_changed === 'created') + {{ __('task.logCreated') }} + @elseif($log->field_changed === 'status') + {{ __('task.logStatusChanged') }}: {{ __('task.status_' . $log->old_value) }} → {{ __('task.status_' . $log->new_value) }} + @else + {{ $log->field_changed }}: {{ $log->old_value }} → {{ $log->new_value }} + @endif +

+
+ @endforeach +
+
+ @endif +
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/warehouses/form.blade.php b/resources/views/warehouses/form.blade.php new file mode 100644 index 00000000..aed33092 --- /dev/null +++ b/resources/views/warehouses/form.blade.php @@ -0,0 +1,95 @@ +@extends('layouts.app') + +@section('title', isset($warehouse) ? __('warehouse.edit_warehouse') : __('warehouse.add_warehouse')) + +@section('content') +
+
+
+ + + +

+ {{ isset($warehouse) ? __('warehouse.edit_warehouse') : __('warehouse.add_warehouse') }} +

+
+
+ +
+ + @csrf + @if(isset($warehouse)) + @method('PUT') + @endif + + +
+
+ + + @error('name') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('location') +

{{ $message }}

+ @enderror +
+
+ + +
+ + + @error('project_id') +

{{ $message }}

+ @enderror +
+ + +
+ is_active : true) ? 'checked' : '' }} + class="rounded border-gray-300 text-teal-600 focus:ring-teal-500"> + +
+ + +
+ + {{ __('common.cancel') }} + + +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/warehouses/index.blade.php b/resources/views/warehouses/index.blade.php new file mode 100644 index 00000000..e4ccb61e --- /dev/null +++ b/resources/views/warehouses/index.blade.php @@ -0,0 +1,92 @@ +@extends('layouts.app') + +@section('title', __('warehouse.warehouses')) + +@section('content') +
+ +
+

{{ __('warehouse.warehouses') }}

+ + + {{ __('warehouse.add_warehouse') }} + +
+ + +
+
+ + + + +
+
+ + +
+
+ + + + + + + + + + + + @forelse($warehouses as $wh) + + + + + + + + @empty + + + + @endforelse + +
{{ __('warehouse.name') }}{{ __('warehouse.location') }}{{ __('expense.project') }}{{ __('common.status') }}{{ __('common.actions') }}
+ {{ $wh->name }} + {{ $wh->location ?? '-' }}{{ $wh->project?->name ?? '-' }} + @if($wh->is_active) + {{ __('common.active') }} + @else + {{ __('common.inactive') }} + @endif + +
+ {{ __('common.view') }} + {{ __('common.edit') }} +
+ @csrf @method('DELETE') + +
+
+
{{ __('warehouse.no_warehouses') }}
+
+
+ + +
+ {{ $warehouses->withQueryString()->links() }} +
+
+@endsection \ No newline at end of file diff --git a/resources/views/warehouses/show.blade.php b/resources/views/warehouses/show.blade.php new file mode 100644 index 00000000..d2795e65 --- /dev/null +++ b/resources/views/warehouses/show.blade.php @@ -0,0 +1,64 @@ +@extends('layouts.app') + +@section('title', $warehouse->name) + +@section('content') +
+ +
+
+ + + +
+

{{ $warehouse->name }}

+
+
+
+ + {{ __('common.edit') }} + +
+ @csrf @method('DELETE') + +
+
+
+ +
+ +
+
+
+
+

{{ __('warehouse.name') }}

+

{{ $warehouse->name }}

+
+
+

{{ __('warehouse.location') }}

+

{{ $warehouse->location ?? '-' }}

+
+
+

{{ __('expense.project') }}

+ @if($warehouse->project) + {{ $warehouse->project->name }} + @else +

-

+ @endif +
+
+

{{ __('common.status') }}

+ @if($warehouse->is_active) + {{ __('common.active') }} + @else + {{ __('common.inactive') }} + @endif +
+
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php deleted file mode 100644 index 7b626b14..00000000 --- a/resources/views/welcome.blade.php +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - Laravel - - - - - - - - - -
- - -
- - diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 00000000..498218a8 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,79 @@ +group(function () { + + // Current user with roles, permissions, and tenant + Route::get('/user', function (\Illuminate\Http\Request $request) { + return $request->user()->load('tenant', 'roles.permissions', 'permissions'); + }); + + // Dashboard stats + Route::get('/dashboard/stats', [DashboardController::class, 'index'])->name('api.dashboard.stats'); + + // Projects API + Route::apiResource('projects', ProjectController::class); + Route::put('projects/{project}/status', [ProjectController::class, 'updateStatus']); + Route::get('projects-export/excel', [ProjectController::class, 'exportExcel']); + Route::get('projects-export/pdf', [ProjectController::class, 'exportPdf']); + + // Tasks API + Route::apiResource('tasks', TaskController::class); + Route::put('tasks/{task}/status', [TaskController::class, 'updateStatus']); + Route::get('tasks-kanban', [TaskController::class, 'kanban']); + + // Employees API + Route::apiResource('employees', EmployeeController::class); + Route::get('employees/{employee}/work-logs', [EmployeeController::class, 'workLogs']); + Route::get('employees/{employee}/financials', [EmployeeController::class, 'financials']); + + // Expenses API + Route::apiResource('expenses', ExpenseController::class); + Route::put('expenses/{expense}/approve', [ExpenseController::class, 'approve']); + Route::put('expenses/{expense}/reject', [ExpenseController::class, 'reject']); + + // Inventory API + Route::prefix('inventory')->group(function () { + Route::get('items', [InventoryController::class, 'items']); + Route::get('warehouses', [InventoryController::class, 'warehouses']); + Route::get('transactions', [InventoryController::class, 'transactions']); + Route::post('stock-in', [InventoryController::class, 'stockIn']); + Route::post('stock-out', [InventoryController::class, 'stockOut']); + Route::get('current-stock', [InventoryController::class, 'currentStock']); + }); + + // Locale/Settings API + Route::post('/locale', function (\Illuminate\Http\Request $request) { + $locale = $request->validate(['locale' => 'required|in:fa,en,ar'])['locale']; + session(['locale' => $locale]); + app()->setLocale($locale); + if ($request->user()) { + $request->user()->update(['locale' => $locale]); + } + return response()->json(['locale' => $locale]); + }); + + Route::post('/calendar', function (\Illuminate\Http\Request $request) { + $calendar = $request->validate(['calendar' => 'required|in:jalali,gregorian'])['calendar']; + session(['calendar' => $calendar]); + return response()->json(['calendar' => $calendar]); + }); +}); diff --git a/routes/console.php b/routes/console.php index eff2ed24..3c9adf1a 100644 --- a/routes/console.php +++ b/routes/console.php @@ -5,4 +5,4 @@ use Illuminate\Support\Facades\Artisan; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); -})->purpose('Display an inspiring quote')->hourly(); +})->purpose('Display an inspiring quote'); diff --git a/routes/letters-routes.php b/routes/letters-routes.php new file mode 100644 index 00000000..78f4a26c --- /dev/null +++ b/routes/letters-routes.php @@ -0,0 +1,10 @@ +name('letters.archive'); +Route::get('letters-attachment/{attachment}/download', [LetterController::class, 'downloadAttachment'])->name('letters.attachment.download'); +Route::delete('letters-attachment/{attachment}', [LetterController::class, 'deleteAttachment'])->name('letters.attachment.delete'); diff --git a/routes/web.php b/routes/web.php index 86a06c53..47a6b25a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,188 @@ name('login'); +Route::post('/login', [AuthController::class, 'login'])->name('login.post'); +Route::post('/logout', [AuthController::class, 'logout'])->name('logout'); + +// Protected Routes (auth required) +Route::middleware(['auth', \App\Http\Middleware\SetTenant::class, \App\Http\Middleware\SetLocale::class, \App\Http\Middleware\AuditLogMiddleware::class])->group(function () { + + // Dashboard + Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); + Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard.index'); + + // Locale & Settings + Route::post('/locale', function (\Illuminate\Http\Request $request) { + $locale = $request->validate(['locale' => 'required|in:fa,en,ar'])['locale']; + session(['locale' => $locale]); + app()->setLocale($locale); + if ($request->user()) { + $request->user()->update(['locale' => $locale]); + } + return redirect()->back(); + })->name('locale.update'); + + Route::post('/calendar', function (\Illuminate\Http\Request $request) { + $calendar = $request->validate(['calendar' => 'required|in:jalali,gregorian'])['calendar']; + session(['calendar' => $calendar]); + if ($request->user()) { + $request->user()->update(['preferred_calendar' => $calendar]); + } + return redirect()->back(); + })->name('calendar.update'); + + Route::post('/currency', function (\Illuminate\Http\Request $request) { + $currency = $request->validate(['currency' => 'required|in:IRR,USD,EUR,AED'])['currency']; + session(['currency' => $currency]); + return redirect()->back(); + })->name('currency.update'); + + // Projects + Route::resource('projects', ProjectController::class); + Route::put('projects/{project}/status', [ProjectController::class, 'updateStatus'])->name('projects.update-status'); + Route::get('projects-export/excel', [ProjectController::class, 'exportExcel'])->name('projects.export.excel'); + Route::get('projects-export/pdf', [ProjectController::class, 'exportPdf'])->name('projects.export.pdf'); + + // Tasks + Route::resource('tasks', TaskController::class); + Route::put('tasks/{task}/status', [TaskController::class, 'updateStatus'])->name('tasks.update-status'); + Route::get('tasks-kanban', [TaskController::class, 'kanban'])->name('tasks.kanban'); + Route::get('tasks-gantt', [TaskController::class, 'gantt'])->name('tasks.gantt'); + Route::get('tasks/{task}/gantt', [TaskController::class, 'gantt'])->name('tasks.gantt-project'); + + // Employees + Route::resource('employees', EmployeeController::class); + Route::get('employees/{employee}/work-logs', [EmployeeController::class, 'workLogs'])->name('employees.work-logs'); + Route::get('employees/{employee}/financials', [EmployeeController::class, 'financials'])->name('employees.financials'); + + // Expenses + Route::resource('expenses', ExpenseController::class); + Route::put('expenses/{expense}/approve', [ExpenseController::class, 'approve'])->name('expenses.approve'); + Route::put('expenses/{expense}/reject', [ExpenseController::class, 'reject'])->name('expenses.reject'); + + // Petty Cash + Route::resource('petty-cashes', PettyCashController::class); + Route::get('petty-cashes/{pettyCash}/create-expense', [PettyCashController::class, 'createExpense'])->name('petty-cashes.create-expense'); + Route::post('petty-cashes/{pettyCash}/store-expense', [PettyCashController::class, 'storeExpense'])->name('petty-cashes.store-expense'); + Route::put('petty-cashes/{pettyCash}/approve', [PettyCashController::class, 'approve'])->name('petty-cashes.approve'); + Route::put('petty-cashes/{pettyCash}/reject', [PettyCashController::class, 'reject'])->name('petty-cashes.reject'); + Route::put('petty-cashes/{pettyCash}/settle', [PettyCashController::class, 'settle'])->name('petty-cashes.settle'); + + // Inventory + Route::prefix('inventory')->name('inventory.')->group(function () { + // Dashboard + Route::get('/', [InventoryController::class, 'index'])->name('index'); + + // Items CRUD + Route::get('items', [InventoryController::class, 'items'])->name('items'); + Route::get('items/create', [InventoryController::class, 'createItem'])->name('items.create'); + Route::post('items', [InventoryController::class, 'storeItem'])->name('items.store'); + Route::get('items/{item}/edit', [InventoryController::class, 'editItem'])->name('items.edit'); + Route::put('items/{item}', [InventoryController::class, 'updateItem'])->name('items.update'); + Route::delete('items/{item}', [InventoryController::class, 'destroyItem'])->name('items.destroy'); + + // Warehouses CRUD + Route::get('warehouses', [InventoryController::class, 'warehouses'])->name('warehouses'); + Route::get('warehouses/create', [InventoryController::class, 'createWarehouse'])->name('warehouses.create'); + Route::post('warehouses', [InventoryController::class, 'storeWarehouse'])->name('warehouses.store'); + Route::get('warehouses/{warehouse}/edit', [InventoryController::class, 'editWarehouse'])->name('warehouses.edit'); + Route::put('warehouses/{warehouse}', [InventoryController::class, 'updateWarehouse'])->name('warehouses.update'); + Route::delete('warehouses/{warehouse}', [InventoryController::class, 'destroyWarehouse'])->name('warehouses.destroy'); + + // Transactions + Route::get('transactions', [InventoryController::class, 'transactions'])->name('transactions'); + + // Stock operations + Route::post('stock-in', [InventoryController::class, 'stockIn'])->name('stock-in'); + Route::post('stock-out', [InventoryController::class, 'stockOut'])->name('stock-out'); + Route::post('transfer', [InventoryController::class, 'stockTransfer'])->name('transfer'); + Route::get('current-stock', [InventoryController::class, 'currentStock'])->name('current-stock'); + }); + + // Letters (Correspondence) + Route::resource('letters', LetterController::class); + Route::put('letters/{letter}/archive', [LetterController::class, 'archive'])->name('letters.archive'); + + // Letters (Correspondence) + Route::resource('letters', LetterController::class); + Route::put('letters/{letter}/archive', [LetterController::class, 'archive'])->name('letters.archive'); + + // Documents + Route::resource('documents', DocumentController::class); + Route::get('documents/{document}/download', [DocumentController::class, 'download'])->name('documents.download'); + + // Reports + Route::prefix('reports')->name('reports.')->group(function () { + Route::get('/', [ReportController::class, 'index'])->name('index'); + Route::get('financial', [ReportController::class, 'financial'])->name('financial'); + Route::get('financial/excel', [ReportController::class, 'financialExportExcel'])->name('financial.excel'); + Route::get('financial/pdf', [ReportController::class, 'financialExportPdf'])->name('financial.pdf'); + Route::get('employee-worklog', [ReportController::class, 'employeeWorklog'])->name('employee-worklog'); + Route::get('employee-worklog/excel', [ReportController::class, 'employeeWorklogExportExcel'])->name('employee-worklog.excel'); + Route::get('inventory', [ReportController::class, 'inventory'])->name('inventory'); + Route::get('inventory/excel', [ReportController::class, 'inventoryExportExcel'])->name('inventory.excel'); + Route::get('payroll', [ReportController::class, 'payroll'])->name('payroll'); + Route::get('payroll/excel', [ReportController::class, 'payrollExportExcel'])->name('payroll.excel'); + }); + + Route::prefix('payroll')->name('payroll.')->group(function () { + Route::get('/', [PayrollController::class, 'index'])->name('index'); + Route::get('/calculate', [PayrollController::class, 'calculate'])->name('calculate'); + Route::post('/auto-calculate', [PayrollController::class, 'autoCalculate'])->name('auto-calculate'); + Route::post('/store', [PayrollController::class, 'store'])->name('store'); + Route::get('/settlement', [PayrollController::class, 'settlement'])->name('settlement'); + Route::post('/preview-settlement', [PayrollController::class, 'previewSettlement'])->name('preview-settlement'); + Route::post('/process-settlement', [PayrollController::class, 'processSettlement'])->name('process-settlement'); + Route::post('/pay-bulk', [PayrollController::class, 'payBulk'])->name('pay-bulk'); + Route::get('/{employee}', [PayrollController::class, 'show'])->name('show'); + Route::post('/{financial}/pay', [PayrollController::class, 'pay'])->name('pay'); + Route::delete('/{financial}', [PayrollController::class, 'destroy'])->name('destroy'); + }); + + // Settings + Route::prefix('settings')->name('settings.')->group(function () { + Route::get('/', [SettingsController::class, 'index'])->name('index'); + Route::put('organization', [SettingsController::class, 'updateOrganization'])->name('organization.update'); + Route::post('users', [SettingsController::class, 'storeUser'])->name('users.store'); + Route::put('users/{user}', [SettingsController::class, 'updateUser'])->name('users.update'); + Route::patch('users/{user}/toggle', [SettingsController::class, 'toggleUser'])->name('users.toggle'); + Route::post('currencies', [SettingsController::class, 'storeCurrency'])->name('currencies.store'); + Route::put('currencies/{currency}', [SettingsController::class, 'updateCurrency'])->name('currencies.update'); + Route::patch('currencies/{currency}/toggle', [SettingsController::class, 'toggleCurrency'])->name('currencies.toggle'); + }); + + + // Attachments + Route::post('attachments', [AttachmentController::class, 'store'])->name('attachments.store'); + Route::delete('attachments/{attachment}', [AttachmentController::class, 'destroy'])->name('attachments.destroy'); + Route::get('attachments/{attachment}/download', [AttachmentController::class, 'download'])->name('attachments.download'); }); diff --git a/sidebar-addition.txt b/sidebar-addition.txt new file mode 100644 index 00000000..f25e93a4 --- /dev/null +++ b/sidebar-addition.txt @@ -0,0 +1,16 @@ +This file contains the sidebar section to be added. +Replace the section between "{{-- Warehouse with sub-menu --}}" and "{{-- Remaining nav items --}}" +with the following additional nav item added AFTER the warehouse section: + + {{-- Letters --}} + @php + $isLettersActive = request()->routeIs('letters.*'); + @endphp + + + + + {{ __('nav.letters') }} + diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/storage/framework/views/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/storage/framework/views/0815a55745834eb105f466067e7b4e86.php b/storage/framework/views/0815a55745834eb105f466067e7b4e86.php new file mode 100644 index 00000000..df31cade --- /dev/null +++ b/storage/framework/views/0815a55745834eb105f466067e7b4e86.php @@ -0,0 +1,55 @@ + 'medium'])); + +foreach ($attributes->all() as $__key => $__value) { + if (in_array($__key, $__propNames)) { + $$__key = $$__key ?? $__value; + } else { + $__newAttributes[$__key] = $__value; + } +} + +$attributes = new \Illuminate\View\ComponentAttributeBag($__newAttributes); + +unset($__propNames); +unset($__newAttributes); + +foreach (array_filter((['priority' => 'medium']), 'is_string', ARRAY_FILTER_USE_KEY) as $__key => $__value) { + $$__key = $$__key ?? $__value; +} + +$__defined_vars = get_defined_vars(); + +foreach ($attributes->all() as $__key => $__value) { + if (array_key_exists($__key, $__defined_vars)) unset($$__key); +} + +unset($__defined_vars); ?> + + 'bg-gray-100 text-gray-600', + 'medium' => 'bg-amber-50 text-amber-600', + 'high' => 'bg-orange-50 text-orange-600', + 'urgent' => 'bg-rose-50 text-rose-600', + ]; + + $dots = [ + 'low' => 'bg-gray-400', + 'medium' => 'bg-amber-400', + 'high' => 'bg-orange-500', + 'urgent' => 'bg-rose-500', + ]; + + $colorClass = $colors[$priority] ?? $colors['medium']; + $dotClass = $dots[$priority] ?? $dots['medium']; +?> + + + + + + + \ No newline at end of file diff --git a/storage/framework/views/0dc35d574885e9ed87093ad3cc8d1de5.php b/storage/framework/views/0dc35d574885e9ed87093ad3cc8d1de5.php new file mode 100644 index 00000000..ed1f4fdc --- /dev/null +++ b/storage/framework/views/0dc35d574885e9ed87093ad3cc8d1de5.php @@ -0,0 +1,183 @@ +startSection('content'); ?> +
+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + 'components.date-input','data' => ['name' => 'from_date','value' => request('from_date')]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'from_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(request('from_date'))]); ?> +renderComponent(); ?> + + + + + + + + + +
+
+ + + + 'components.date-input','data' => ['name' => 'to_date','value' => request('to_date')]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'to_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(request('to_date'))]); ?> +renderComponent(); ?> + + + + + + + + + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $letter): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
+ + type)); ?> + + + reference_number ?? '-'); ?>subject); ?>sender); ?>recipient); ?> + + status)); ?> + + + letter_date ? calendar_date($letter->letter_date) : '-'); ?> +
+ attachments->count() > 0): ?> + + + + + + + +
+
+
+
+ + +
+ withQueryString()->links()); ?> + +
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/15dc9805bd50ffbccd898a04c0572745.php b/storage/framework/views/15dc9805bd50ffbccd898a04c0572745.php new file mode 100644 index 00000000..59777464 --- /dev/null +++ b/storage/framework/views/15dc9805bd50ffbccd898a04c0572745.php @@ -0,0 +1,183 @@ +startSection('content'); ?> +
+ +
+

+ + + + + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + + 'components.date-input','data' => ['name' => 'from_date','value' => request('from_date')]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'from_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(request('from_date'))]); ?> +renderComponent(); ?> + + + + + + + + + +
+
+ + + + 'components.date-input','data' => ['name' => 'to_date','value' => request('to_date')]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'to_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(request('to_date'))]); ?> +renderComponent(); ?> + + + + + + + + + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $expense): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
+ title); ?> + project?->name); ?> + amount, $expense->currency?->code)); ?> + + original_amount): ?> + (original_amount, $expense->currency?->code)); ?>) + + category)); ?>expense_date)); ?> + + status_label); ?> + + + +
+ status === 'pending'): ?> + + + +
+
+
+
+ + +
+ withQueryString()->links()); ?> + +
+
+ +startPush('scripts'); ?> + +stopPush(); ?> +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/42fce2afce1274cac2c89c79739f9fc3.php b/storage/framework/views/42fce2afce1274cac2c89c79739f9fc3.php new file mode 100644 index 00000000..c234138b --- /dev/null +++ b/storage/framework/views/42fce2afce1274cac2c89c79739f9fc3.php @@ -0,0 +1,118 @@ + +
+
+ + + + + + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $curr): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+ + +
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ + + +
+
+
+ \ No newline at end of file diff --git a/storage/framework/views/448a787e655d73dc75d1ab0fcc84b86f.php b/storage/framework/views/448a787e655d73dc75d1ab0fcc84b86f.php new file mode 100644 index 00000000..ded36391 --- /dev/null +++ b/storage/framework/views/448a787e655d73dc75d1ab0fcc84b86f.php @@ -0,0 +1,188 @@ +startSection('content'); ?> +
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
/
+
+
+ + +
+
+ + : | : +
+ 0 ? round(($totalPaid / $totalGross) * 100) : 0; ?> +
+
+
+
%
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+ + + +
+ count() > 0): ?> +
+ + +
+ +
+ + + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $financial): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
+ +
+ is_paid): ?> + + + + + employee->first_name ?? ''); ?> employee->last_name ?? ''); ?> + + + + + type)); ?> + + + + type, ['deduction', 'advance']) ? '-' : ''); ?>amount)); ?> + + effective_date ? calendar_date($financial->effective_date) : '-'); ?> + is_paid): ?> + + + + + + + + + + + + description ?? '-'); ?> +
+ is_paid): ?> + + + + +
+
+
+
+
+ + +
+ withQueryString()->links()); ?> + +
+
+ + +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/46e7c1d03b179af2b30bd5f0a8a09de0.php b/storage/framework/views/46e7c1d03b179af2b30bd5f0a8a09de0.php new file mode 100644 index 00000000..1c79d07a --- /dev/null +++ b/storage/framework/views/46e7c1d03b179af2b30bd5f0a8a09de0.php @@ -0,0 +1,269 @@ +startSection('content'); ?> +
+ +
+ + + +

title); ?>

+ + status_label); ?> + + +
+ + + + + status === 'pending'): ?> + + + +
+
+ + +
+
+ +
+
+

+

project?->name ?? '-'); ?>

+
+
+

+

category)); ?>

+
+
+

+

amount, $expense->currency?->code)); ?>

+ original_amount && $expense->original_amount != $expense->amount): ?> +

(original_amount, $expense->currency?->code)); ?>)

+ +
+
+

+

expense_date ? calendar_date($expense->expense_date) : '-'); ?>

+
+
+ + +
+ invoice_number): ?> +
+

+

invoice_number); ?>

+
+ +
+

+

requestedBy?->name ?? '-'); ?>

+
+ approvedBy): ?> +
+

+

approvedBy?->name); ?>

+
+ + approval_date): ?> +
+

+

approval_date)); ?>

+
+ +
+
+ + + description): ?> +
+

+

description); ?>

+
+ +
+ + + receipt_path): ?> +
+

+
+ receipt_path), '.pdf')): ?> +
+ +
+ +
+ Receipt +
+ +
+

receipt_path)); ?>

+
+ + + + +
+
+ + + +
+
+

+ +
+ + + + + +
+ attachments; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $attachment): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> +
+ is_image): ?> +
+ <?php echo e($attachment->file_name); ?> +
+ is_pdf): ?> +
+ +
+ +
+ +
+ + +
+

file_name); ?>

+

formatted_size); ?> • created_at?->format('Y/m/d H:i')); ?>

+
+ +
+ + + + +
+
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +
+ +

+
+ +
+
+ + + +
+ +startPush('scripts'); ?> + +stopPush(); ?> +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/4ec001dbe225074190f1867b3ff08122.php b/storage/framework/views/4ec001dbe225074190f1867b3ff08122.php new file mode 100644 index 00000000..0c49d771 --- /dev/null +++ b/storage/framework/views/4ec001dbe225074190f1867b3ff08122.php @@ -0,0 +1,8 @@ +startSection('content'); ?> +
+

+

+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/5625be61c087bf17bbc0b8985f4cf4b2.php b/storage/framework/views/5625be61c087bf17bbc0b8985f4cf4b2.php new file mode 100644 index 00000000..f5a44357 --- /dev/null +++ b/storage/framework/views/5625be61c087bf17bbc0b8985f4cf4b2.php @@ -0,0 +1,30 @@ +startSection('content'); ?> +
+
+ + + +

+
+ + +
+
+

+

+
+
+

+

+
+
+

+

+
+
+ +

+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/5c57a35cb8207405007d3ffb6056da18.php b/storage/framework/views/5c57a35cb8207405007d3ffb6056da18.php new file mode 100644 index 00000000..1071f16d --- /dev/null +++ b/storage/framework/views/5c57a35cb8207405007d3ffb6056da18.php @@ -0,0 +1,35 @@ + + + + + + + <?php echo $__env->yieldContent('title'); ?> + + + + + + +
+
+
+
+ yieldContent('code'); ?> +
+ +
+ yieldContent('message'); ?> +
+
+
+
+ + + \ No newline at end of file diff --git a/storage/framework/views/63827679d7679f25341f7abe0c3ec203.php b/storage/framework/views/63827679d7679f25341f7abe0c3ec203.php new file mode 100644 index 00000000..d3b6bf4a --- /dev/null +++ b/storage/framework/views/63827679d7679f25341f7abe0c3ec203.php @@ -0,0 +1,99 @@ + + + + + + + + <?php echo e(isset($pageTitle) ? $pageTitle . ' - ' : ''); ?><?php echo e(config('app.name')); ?> + + + + + + + + + + + + yieldPushContent('styles'); ?> + + + + + + + + + +
+ + + make('layouts.sidebar', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> + + +
+ + + make('layouts.header', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> + + +
+ +
+ + +
+ + + +
+ + +
+ + + yieldContent('content'); ?> +
+ + +
+
+ © Vernova — +
+ + + + + + + + + + + getLocale() === 'fa' ? 'فارسی' : 'English'); ?> +
+
+
+
+
+ + yieldPushContent('scripts'); ?> + + + + + + + + \ No newline at end of file diff --git a/storage/framework/views/65f6c99804a6a0edbd8042acc1b80c49.php b/storage/framework/views/65f6c99804a6a0edbd8042acc1b80c49.php new file mode 100644 index 00000000..e82eb74e --- /dev/null +++ b/storage/framework/views/65f6c99804a6a0edbd8042acc1b80c49.php @@ -0,0 +1,250 @@ + + + + +
+ \ No newline at end of file diff --git a/storage/framework/views/663a1a1fea235ffe5ad3f8ff45ecda31.php b/storage/framework/views/663a1a1fea235ffe5ad3f8ff45ecda31.php new file mode 100644 index 00000000..41e2faec --- /dev/null +++ b/storage/framework/views/663a1a1fea235ffe5ad3f8ff45ecda31.php @@ -0,0 +1,93 @@ +startSection('content'); ?> +
+
+

+ + + + + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $document): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
+ title); ?> + + file_type)); ?> + project?->name ?? '-'); ?>uploader?->name ?? '-'); ?> +
+ + +
+ + +
+
+
+
+
+ +
+ withQueryString()->links()); ?> + +
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/7379735e393d6b0e3866bee72506a75c.php b/storage/framework/views/7379735e393d6b0e3866bee72506a75c.php new file mode 100644 index 00000000..12e6b7d5 --- /dev/null +++ b/storage/framework/views/7379735e393d6b0e3866bee72506a75c.php @@ -0,0 +1,41 @@ +startSection('content'); ?> + +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/86a592b10d7a65e83deaa9802eeb20fb.php b/storage/framework/views/86a592b10d7a65e83deaa9802eeb20fb.php new file mode 100644 index 00000000..dae3dc89 --- /dev/null +++ b/storage/framework/views/86a592b10d7a65e83deaa9802eeb20fb.php @@ -0,0 +1,163 @@ +startSection('content'); ?> +
+ +
+ + + +

+ + +

+
+ + + +
+
+ + : + subject); ?> +
+
+ + + +
+ + + + + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + + 'components.date-input','data' => ['name' => 'letter_date','value' => old('letter_date', $letter?->letter_date?->format('Y-m-d')),'required' => 'true']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'letter_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(old('letter_date', $letter?->letter_date?->format('Y-m-d'))),'required' => 'true']); ?> +renderComponent(); ?> + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + +

+
+ + + + + + +
+ + +
+ + + + + +
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/8978f0f93ca9bd010ca93ff26a7c11ff.php b/storage/framework/views/8978f0f93ca9bd010ca93ff26a7c11ff.php new file mode 100644 index 00000000..b16ee077 --- /dev/null +++ b/storage/framework/views/8978f0f93ca9bd010ca93ff26a7c11ff.php @@ -0,0 +1,118 @@ +startSection('content'); ?> +
+ +
+
+ + + +
+

+

first_name); ?> last_name); ?>

+
+
+
+ + +
+
+

+

currency?->code)); ?>

+
+
+

+

currency?->code)); ?>

+
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $fin): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
effective_date ? calendar_date($fin->effective_date) : '-'); ?> + + type)); ?> + + + + type, ['deduction', 'advance']) ? '-' : '+'); ?>amount, $fin->currency?->code)); ?> + + + is_paid)): ?> + + is_paid ? __('employee.isPaid') : __('employee.isUnpaid')); ?> + + + + - + + description ?? '-'); ?>
+
+
+ + +
+ withQueryString()->links()); ?> + +
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/90d43165439e87034f1fc57adfb2dd86.php b/storage/framework/views/90d43165439e87034f1fc57adfb2dd86.php new file mode 100644 index 00000000..97ad23c2 --- /dev/null +++ b/storage/framework/views/90d43165439e87034f1fc57adfb2dd86.php @@ -0,0 +1,150 @@ +startSection('content'); ?> +
+ +
+

+
+ +
+
+ + +
+ +

+
+ + +
+
+
+ + +
+

+
+ + + + + +
+
+ +
+ + +startPush('styles'); ?> + + +stopPush(); ?> + +startPush('scripts'); ?> + + + +stopPush(); ?> + +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/95971f91e644c2c50f9234aa4ccd2329.php b/storage/framework/views/95971f91e644c2c50f9234aa4ccd2329.php new file mode 100644 index 00000000..d11d8bfe --- /dev/null +++ b/storage/framework/views/95971f91e644c2c50f9234aa4ccd2329.php @@ -0,0 +1,103 @@ +startSection('content'); ?> +
+ +
+

+ + + + + +
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $warehouse): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
name); ?>location ?? '-'); ?>project?->name ?? '-'); ?>inventoryTransactions()->distinct('item_id')->count('item_id'))); ?> + + is_active ? __('inventory.active') : __('inventory.inactive')); ?> + + + +
+ + + + inventoryTransactions()->count() === 0): ?> +
+ + +
+ +
+
+
+
+ + +
+ withQueryString()->links()); ?> + +
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/98463ca068fc45f46d96fd75afccf591.php b/storage/framework/views/98463ca068fc45f46d96fd75afccf591.php new file mode 100644 index 00000000..0fe1893c --- /dev/null +++ b/storage/framework/views/98463ca068fc45f46d96fd75afccf591.php @@ -0,0 +1,168 @@ +startSection('content'); ?> +
+ +
+ + + +

+ + +

+
+ + +
+ + + + + +
+ +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?>

+
+ + +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?>

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + 'components.date-input','data' => ['name' => 'start_date','value' => old('start_date', isset($task) && $task->start_date ? $task->start_date->format('Y-m-d') : '')]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'start_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(old('start_date', isset($task) && $task->start_date ? $task->start_date->format('Y-m-d') : ''))]); ?> +renderComponent(); ?> + + + + + + + + + +
+ + +
+ + + + 'components.date-input','data' => ['name' => 'due_date','value' => old('due_date', isset($task) && $task->due_date ? $task->due_date->format('Y-m-d') : '')]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'due_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(old('due_date', isset($task) && $task->due_date ? $task->due_date->format('Y-m-d') : ''))]); ?> +renderComponent(); ?> + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + + + + +
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/9b62b89896774f58ae7160b9f9c3f8f5.php b/storage/framework/views/9b62b89896774f58ae7160b9f9c3f8f5.php new file mode 100644 index 00000000..4d7374a8 --- /dev/null +++ b/storage/framework/views/9b62b89896774f58ae7160b9f9c3f8f5.php @@ -0,0 +1,7 @@ + + +startSection('title', __('Forbidden')); ?> +startSection('code', '403'); ?> +startSection('message', __($exception->getMessage() ?: 'Forbidden')); ?> + +make('errors::minimal', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/a03636601cf2476624659c1e63087b5d.php b/storage/framework/views/a03636601cf2476624659c1e63087b5d.php new file mode 100644 index 00000000..ef8ca49a --- /dev/null +++ b/storage/framework/views/a03636601cf2476624659c1e63087b5d.php @@ -0,0 +1,251 @@ +startSection('content'); ?> +
+ +
+
+

+

+
+ +
+ + +
+ +
+
+
+
+

+

+ getLocale() === 'fa' ? persian_num($activeProjectsCount) : $activeProjectsCount); ?> + +

+
+
+ + + +
+
+ +

+ 0 ? '↑' : '↓'); ?> % + +

+ +
+ + +
+
+
+
+

+

+
+
+ + + +
+
+
+ + +
+
+
+
+

+

+ getLocale() === 'fa' ? persian_num($pendingTasksCount) : $pendingTasksCount); ?> + +

+
+
+ + + +
+
+ +

+ 0 ? '↑' : '↓'); ?> % + +

+ +
+ + +
+
+
+
+

+

+ getLocale() === 'fa' ? persian_num($activeEmployeesCount) : $activeEmployeesCount); ?> + +

+
+
+ + + +
+
+ +

+ 0 ? '↑' : '↓'); ?> % + +

+ +
+
+ + +
+ +
+
+

+ +
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $project): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> +
+
+
+ + name); ?> + + + + getLocale() === 'fa' ? persian_num($project->progress) : $project->progress); ?>% + +
+
+
+
+
+ code); ?> + manager): ?> + manager->name); ?> + +
+
+
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +

+ +
+
+ + +
+ +
+
+

+ +
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $task): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + +
+ title); ?> + priority === 'urgent'): ?> + + +
+
+ project?->name); ?> + due_date): ?> + + due_date)); ?> + +
+
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +

+ +
+
+ + +
+
+

+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $activity): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> +
+
+ user?->name ?? '?', 0, 1))); ?> +
+
+

user?->name); ?> — description); ?>

+

created_at)); ?>

+
+
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +

+ +
+
+
+
+ + +
+

+
+
+

+

+

:

+
+
+

+

+ getLocale() === 'fa' ? persian_num(1234567.89) : number_format(1234567.89, 2)); ?> + +

+
+
+

+

+
+
+
+ + + +
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/a11f4e6f418ca75d82ec12f95844d120.php b/storage/framework/views/a11f4e6f418ca75d82ec12f95844d120.php new file mode 100644 index 00000000..2d19419c --- /dev/null +++ b/storage/framework/views/a11f4e6f418ca75d82ec12f95844d120.php @@ -0,0 +1,174 @@ +startSection('content'); ?> +
+ +
+ + + +

+ + +

+
+ + +
+ + + + + +
+ +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?>

+
+ + +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?>

+
+ + +
+ + +
+ + +
+ + + + 'components.date-input','data' => ['name' => 'hire_date','value' => old('hire_date', isset($employee) && $employee->hire_date ? $employee->hire_date->format('Y-m-d') : ''),'required' => 'true']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'hire_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(old('hire_date', isset($employee) && $employee->hire_date ? $employee->hire_date->format('Y-m-d') : '')),'required' => 'true']); ?> +renderComponent(); ?> + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + + + + +
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/a767be18f85f06a40736e43baa9c1c94.php b/storage/framework/views/a767be18f85f06a40736e43baa9c1c94.php new file mode 100644 index 00000000..8e09b411 --- /dev/null +++ b/storage/framework/views/a767be18f85f06a40736e43baa9c1c94.php @@ -0,0 +1,100 @@ +startSection('content'); ?> +
+ +
+

+ + + + + +
+ + +
+
+
+ + +
+
+ + +
+ + +
+
+ + +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $project): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + +
+
+ code); ?> + + status_label); ?> + + +
+

name); ?>

+ + +
+
+ + progress); ?>% +
+
+
+
+
+ +
+ : budget)); ?> + manager): ?> + manager->name); ?> + +
+ + start_date): ?> +
+ start_date)); ?> — end_date ? calendar_date($project->end_date) : '...'); ?> + +
+ +
+
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +
+ +

+
+ +
+ + +
+ withQueryString()->links()); ?> + +
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/abc1086b0925e0d145a47b081ba4eb8c.php b/storage/framework/views/abc1086b0925e0d145a47b081ba4eb8c.php new file mode 100644 index 00000000..0bd7f329 --- /dev/null +++ b/storage/framework/views/abc1086b0925e0d145a47b081ba4eb8c.php @@ -0,0 +1,164 @@ +startSection('content'); ?> +
+ +
+

+
+ + +
+ +
+
+
+ +
+
+

+

+
+
+
+ + +
+
+
+ +
+
+

+

+
+
+
+ + +
+
+
+ +
+
+

+

+
+
+
+ + +
+
+
+ +
+
+

+

count())); ?>

+
+
+
+
+ + + + +
+ +
+
+

+ +
+ count() > 0): ?> +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $txn): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+ + type)); ?> + + +
+

item?->name ?? '-'); ?>

+

warehouse?->name ?? '-'); ?>

+
+
+

+ type === 'out' ? '-' : '+'); ?>quantity, 2))); ?> + +

+

created_at ? calendar_date($txn->created_at) : ''); ?>

+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+ +
+ + +
+ +
+ + +
+
+

+ +
+ count() > 0): ?> +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+ +
+
+

name); ?>

+

code ?? '-'); ?> · unit ?? '-'); ?>

+
+
+

+ current_stock, 2))); ?> + +

+

: min_stock ?? 0, 2))); ?>

+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+ +
+ +

+
+ +
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/b1e4ba57b9976591108a1c9b3ffe68a8.php b/storage/framework/views/b1e4ba57b9976591108a1c9b3ffe68a8.php new file mode 100644 index 00000000..ad063ce6 --- /dev/null +++ b/storage/framework/views/b1e4ba57b9976591108a1c9b3ffe68a8.php @@ -0,0 +1,83 @@ +startSection('content'); ?> +
+ +
+

+ + + + + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $employee): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + +
+ full_name, 0, 1))); ?> + +
+

full_name); ?>

+

job_title ?? $employee->contract_type_label); ?>

+
+ + status === 'active' ? __('employee.active') : __('employee.inactive')); ?> + + +
+ project): ?> +

project->name); ?>

+ +
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +
+ +

+
+ +
+ + +
+ withQueryString()->links()); ?> + +
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/b3640ad9e480045721de9d9873d8e305.php b/storage/framework/views/b3640ad9e480045721de9d9873d8e305.php new file mode 100644 index 00000000..db8ed1ea --- /dev/null +++ b/storage/framework/views/b3640ad9e480045721de9d9873d8e305.php @@ -0,0 +1,131 @@ +startSection('content'); ?> +
+ +
+ + + +

+ + +

+
+ + +
+ + + + + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

+
+ + +
+ + + 'components.date-input','data' => ['name' => 'request_date','value' => old('request_date', $pettyCash?->request_date?->format('Y-m-d')),'required' => 'true']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'request_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(old('request_date', $pettyCash?->request_date?->format('Y-m-d'))),'required' => 'true']); ?> +renderComponent(); ?> + + + + + + + + + +
+ + +
+ + +
+ + +
+ + receipt_path): ?> +
+ + +
+ + +

+
+ + +
+ + +

+
+
+ + +
+ + + + + +
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/b70330c10889c719d7f5f7c4f8975d50.php b/storage/framework/views/b70330c10889c719d7f5f7c4f8975d50.php new file mode 100644 index 00000000..79b57f60 --- /dev/null +++ b/storage/framework/views/b70330c10889c719d7f5f7c4f8975d50.php @@ -0,0 +1,235 @@ +startSection('content'); ?> +
+ +
+
+ + + +
+

name); ?>

+

code); ?>

+
+
+
+ + + + +
+ + +
+
+
+ + +
+ +
+ +
+
+
+

+ + status_label); ?> + + +
+
+

+

budget)); ?>

+
+
+

+

manager?->name ?? '-'); ?>

+
+
+

+
+
+
+
+
+ progress); ?>% +
+
+
+
+

+

start_date ? calendar_date($project->start_date) : '-'); ?>

+
+
+

+

end_date ? calendar_date($project->end_date) : '-'); ?>

+
+
+

+

location ?? '-'); ?>

+
+
+

+

defaultCurrency?->code ?? '-'); ?>

+
+
+ + description): ?> +
+

+

description); ?>

+
+ +
+ + tasks->pluck('assignee')->filter()->unique('id'); + ?> + + + +
+
+ +
+ +
+ +
+ tasks; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $task): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> +
+ + + 'components.priority-badge','data' => ['priority' => $task->priority]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('priority-badge'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['priority' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($task->priority)]); ?> +renderComponent(); ?> + + + + + + + + + + title); ?> + + status_label); ?> + + +
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +

+ +
+ + +
+ expenses; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $expense): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> +
+ title); ?> + amount, $expense->currency?->code)); ?> + status_label); ?> +
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +

+ +
+ + +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $employee): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + +
+ full_name, 0, 1))); ?> + +
+
+

full_name); ?>

+

position ?? $employee->contract_type_label); ?>

+
+
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +

+ +
+
+
+
+ + +
+ +
+

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

+
+ users; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $member): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> +
+
+ name, 0, 1))); ?> + +
+ name); ?> +
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +

+ +
+
+
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/c2413d3aa9744e18fe67bd7a0d816197.php b/storage/framework/views/c2413d3aa9744e18fe67bd7a0d816197.php new file mode 100644 index 00000000..ace7bcdd --- /dev/null +++ b/storage/framework/views/c2413d3aa9744e18fe67bd7a0d816197.php @@ -0,0 +1,85 @@ + null, 'id' => null, 'class' => '', 'required' => false])); + +foreach ($attributes->all() as $__key => $__value) { + if (in_array($__key, $__propNames)) { + $$__key = $$__key ?? $__value; + } else { + $__newAttributes[$__key] = $__value; + } +} + +$attributes = new \Illuminate\View\ComponentAttributeBag($__newAttributes); + +unset($__propNames); +unset($__newAttributes); + +foreach (array_filter((['name', 'value' => null, 'id' => null, 'class' => '', 'required' => false]), 'is_string', ARRAY_FILTER_USE_KEY) as $__key => $__value) { + $$__key = $$__key ?? $__value; +} + +$__defined_vars = get_defined_vars(); + +foreach ($attributes->all() as $__key => $__value) { + if (array_key_exists($__key, $__defined_vars)) unset($$__key); +} + +unset($__defined_vars); ?> + +format('Y-m-d'); + } elseif (is_string($value) && strlen($value) >= 10) { + $gregorianValue = substr($value, 0, 10); + } + + if ($calendar === 'jalali' && $gregorianValue) { + try { + $displayValue = \Morilog\Jalali\Jalalian::fromDateTime($gregorianValue)->format('Y/m/d'); + } catch (\Exception $e) { + $displayValue = $gregorianValue; + } + } else { + $displayValue = $gregorianValue ?? ''; + } +} + +$defaultClass = 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 focus:border-teal-500'; +$cssClass = $class ?: $defaultClass; +?> + + +
+ + + dir="ltr" /> + +
+ + /> + + \ No newline at end of file diff --git a/storage/framework/views/cc7cee66c4891fd4b5e2d2b84bf0b502.php b/storage/framework/views/cc7cee66c4891fd4b5e2d2b84bf0b502.php new file mode 100644 index 00000000..5c4e4b6d --- /dev/null +++ b/storage/framework/views/cc7cee66c4891fd4b5e2d2b84bf0b502.php @@ -0,0 +1,171 @@ +startSection('content'); ?> +
+ +
+

+ + + + + +
+ + +
+
+

+

+
+
+

+

+
+
+

+

+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $pc): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
+ title ?? '-'); ?> + project?->name ?? '-'); ?> + amount, $pc->currency?->code)); ?> + + + total_expenses, $pc->currency?->code)); ?> + + + remaining !== null ? format_currency((float)$pc->remaining, $pc->currency?->code) : '-'); ?> + + request_date ? calendar_date($pc->request_date) : '-'); ?> + + status_label); ?> + + + +
+ status === 'pending'): ?> + + + status === 'approved'): ?> + + + +
+
+
+
+ + +
+ withQueryString()->links()); ?> + +
+
+ +startPush('scripts'); ?> + +stopPush(); ?> +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/cf61b670207f0f276880b2ae809d4364.php b/storage/framework/views/cf61b670207f0f276880b2ae809d4364.php new file mode 100644 index 00000000..338c877f --- /dev/null +++ b/storage/framework/views/cf61b670207f0f276880b2ae809d4364.php @@ -0,0 +1,130 @@ +startSection('content'); ?> +
+ +
+ + + +
+

full_name); ?>

+

job_title ?? '-'); ?>

+
+ +
+ + +
+
+
+

+

first_name); ?>

+
+
+

+

last_name); ?>

+
+
+

+

job_title ?? '-'); ?>

+
+
+

+

contract_type) ?? $employee->contract_type ?? '-'); ?>

+
+
+

+

phone ?? '-'); ?>

+
+
+

+

national_code ?? '-'); ?>

+
+
+

+

hire_date ? calendar_date($employee->hire_date) : '-'); ?>

+
+
+

+

project?->name ?? '-'); ?>

+
+
+

+

base_salary ? format_currency((float)$employee->base_salary, $employee->currency?->code) : '-'); ?>

+
+
+

+

bank_account ?? '-'); ?>

+
+
+

+ + status) ?? $employee->status); ?> + + +
+
+
+ + + workLogs->count() > 0): ?> +
+
+

+ +
+
+ workLogs->take(5); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $log): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+

date)); ?>

+

project?->name ?? '-'); ?>

+
+ hours_worked, 1))); ?> +
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ + + + financials->count() > 0): ?> +
+
+

+ +
+
+ financials->take(5); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $fin): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+

type)); ?>

+

effective_date ? calendar_date($fin->effective_date) : '-'); ?>

+
+ + amount, $fin->currency?->code)); ?> + + +
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ +
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/d0cbbc094801c6a7d98901eb18134bcd.php b/storage/framework/views/d0cbbc094801c6a7d98901eb18134bcd.php new file mode 100644 index 00000000..3aafa222 --- /dev/null +++ b/storage/framework/views/d0cbbc094801c6a7d98901eb18134bcd.php @@ -0,0 +1,99 @@ + + + + + + + <?php echo e(__('auth.login')); ?> - Vernova + + + + + + + +
+ +
+
+ + + +
+

Vernova

+

+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?> +

+ +
+ + +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?> +

+ +
+ + +
+ +
+ + + +
+
+ + +

© Vernova.

+
+ + + \ No newline at end of file diff --git a/storage/framework/views/d1152c3fd7ed43b88a51d21a99ef2690.php b/storage/framework/views/d1152c3fd7ed43b88a51d21a99ef2690.php new file mode 100644 index 00000000..34b1e571 --- /dev/null +++ b/storage/framework/views/d1152c3fd7ed43b88a51d21a99ef2690.php @@ -0,0 +1,173 @@ +startSection('content'); ?> +
+ +
+ + + +

+ + +

+
+ + +
+ + + + + +
+ +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?>

+
+ + +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?>

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + 'components.date-input','data' => ['name' => 'start_date','value' => old('start_date', $project?->start_date?->format('Y-m-d'))]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'start_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(old('start_date', $project?->start_date?->format('Y-m-d')))]); ?> +renderComponent(); ?> + + + + + + + + + +
+ + +
+ + + + 'components.date-input','data' => ['name' => 'end_date','value' => old('end_date', $project?->end_date?->format('Y-m-d'))]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'end_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(old('end_date', $project?->end_date?->format('Y-m-d')))]); ?> +renderComponent(); ?> + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + + + + +
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/d2d7153ed22b6afa438b863b8619689d.php b/storage/framework/views/d2d7153ed22b6afa438b863b8619689d.php new file mode 100644 index 00000000..c16ac2f4 --- /dev/null +++ b/storage/framework/views/d2d7153ed22b6afa438b863b8619689d.php @@ -0,0 +1,141 @@ +startSection('content'); ?> +
+ +
+ + + +

+ + +

+
+ + +
+ + + + + +
+ +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?> +

+ +
+ + +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?> +

+ +
+ + +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?> +

+ +
+ + +
+ + + + addLoop($__currentLoopData); foreach($__currentLoopData as $cat): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + +
+ + +
+ + +

+
+ + +
+ +
+ + +
+ + +
+
+ + +
+ + + + + +
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/d4837b0709b873e59cd24d661c7a1525.php b/storage/framework/views/d4837b0709b873e59cd24d661c7a1525.php new file mode 100644 index 00000000..0a9f73e9 --- /dev/null +++ b/storage/framework/views/d4837b0709b873e59cd24d661c7a1525.php @@ -0,0 +1,148 @@ +startSection('content'); ?> +
+ +
+ + + +

+ + +

+
+ + +
+ + + + + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

+
+ + +
+ + + + 'components.date-input','data' => ['name' => 'expense_date','value' => old('expense_date', $expense?->expense_date?->format('Y-m-d')),'required' => 'true']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'expense_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(old('expense_date', $expense?->expense_date?->format('Y-m-d'))),'required' => 'true']); ?> +renderComponent(); ?> + + + + + + + + + +
+ + +
+ + +
+ + +
+ + receipt_path): ?> +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

+
+
+ + +
+ + + + + +
+
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/e3253f4ba065d81f5eeecfaef7fa0887.php b/storage/framework/views/e3253f4ba065d81f5eeecfaef7fa0887.php new file mode 100644 index 00000000..da4b0104 --- /dev/null +++ b/storage/framework/views/e3253f4ba065d81f5eeecfaef7fa0887.php @@ -0,0 +1,159 @@ +startSection('content'); ?> +
+ +
+
+

+ +
+
+ + + + + + +
+
+ + +
+ ['color' => 'gray', 'icon' => 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'], + 'in_progress' => ['color' => 'blue', 'icon' => 'M13 10V3L4 14h7v7l9-11h-7z'], + 'review' => ['color' => 'amber', 'icon' => 'M15 12a3 3 0 11-6 0 3 3 0 016 0z'], + 'done' => ['color' => 'emerald', 'icon' => 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'] + ]; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $status => $config): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+ +
+
+ + + +
+

+ count()); ?> +
+ + +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $task): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> +
+
+ + title); ?> + + + + priority)); ?> + + +
+ + project): ?> +

project->name); ?>

+ + +
+
+ due_date): ?> + + + due_date)); ?> + + + +
+
+ assignee): ?> +
+ assignee->name, 0, 1))); ?> + +
+ + +
+ + + + + + + + + + + + +
+
+
+
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +
+ + +
+ +
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ +startPush('scripts'); ?> + +stopPush(); ?> +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/e3358e6b2ca544b1228f9c327b88a6e0.php b/storage/framework/views/e3358e6b2ca544b1228f9c327b88a6e0.php new file mode 100644 index 00000000..cf600c2f --- /dev/null +++ b/storage/framework/views/e3358e6b2ca544b1228f9c327b88a6e0.php @@ -0,0 +1,125 @@ +startSection('content'); ?> +
+ +
+

+ + + + + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
code ?? '-'); ?>name); ?>category ?? '-'); ?>unit ?? '-'); ?> + current_stock ?? 0; + $minStock = $item->min_stock ?? 0; + ?> + + + + + 0): ?> + + + + + + + is_active ? __('inventory.active') : __('inventory.inactive')); ?> + + + +
+ + + + inventoryTransactions()->count() === 0): ?> +
+ + +
+ +
+
+
+
+ + +
+ withQueryString()->links()); ?> + +
+
+stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/eb3da118726a3243dc4a5dd762b87bb8.php b/storage/framework/views/eb3da118726a3243dc4a5dd762b87bb8.php new file mode 100644 index 00000000..9b9eb19e --- /dev/null +++ b/storage/framework/views/eb3da118726a3243dc4a5dd762b87bb8.php @@ -0,0 +1,528 @@ +startSection('content'); ?> +
+ +
+

+
+ + + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + 'components.date-input','data' => ['name' => 'from_date','value' => request('from_date')]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'from_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(request('from_date'))]); ?> +renderComponent(); ?> + + + + + + + + + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+
+ + + + 'components.date-input','data' => ['name' => 'to_date','value' => request('to_date')]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('date-input'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['name' => 'to_date','value' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(request('to_date'))]); ?> +renderComponent(); ?> + + + + + + + + + + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500"> +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $txn): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
+ + type)); ?> + + + item?->name ?? '-'); ?>warehouse?->name ?? '-'); ?> + type === 'out' ? '-' : '+'); ?>quantity, 2))); ?> + + item?->unit): ?> + item->unit); ?> + + + unit_price): ?> + unit_price, $txn->currency?->code)); ?> + + + - + + + total_price): ?> + total_price, $txn->currency?->code)); ?> + + + - + + reference_number ?? $txn->reference ?? '-'); ?>created_at ? calendar_date($txn->created_at) : '-'); ?>
+
+
+ + +
+ withQueryString()->links()); ?> + +
+
+ + + + + + + + + + +startPush('scripts'); ?> + +stopPush(); ?> +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/ebeebd8922e18d3f7f95a6b1c67f8bfe.php b/storage/framework/views/ebeebd8922e18d3f7f95a6b1c67f8bfe.php new file mode 100644 index 00000000..5121b3ba --- /dev/null +++ b/storage/framework/views/ebeebd8922e18d3f7f95a6b1c67f8bfe.php @@ -0,0 +1,117 @@ +hasPages()): ?> + + + \ No newline at end of file diff --git a/storage/framework/views/f65f51438d6e40d1a0e9db5fe3643c14.php b/storage/framework/views/f65f51438d6e40d1a0e9db5fe3643c14.php new file mode 100644 index 00000000..691cc9f7 --- /dev/null +++ b/storage/framework/views/f65f51438d6e40d1a0e9db5fe3643c14.php @@ -0,0 +1,158 @@ +startSection('content'); ?> +
+ +
+

+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $task): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> +
+ + + + 'components.priority-badge','data' => ['priority' => $task->priority]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> +withName('priority-badge'); ?> +shouldRender()): ?> +startComponent($component->resolveView(), $component->data()); ?> + +except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?> + +withAttributes(['priority' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($task->priority)]); ?> +renderComponent(); ?> + + + + + + + + + + + +
+ title); ?> +
+ project?->name); ?> + due_date): ?> + + due_date)); ?> + + assignee): ?> + + assignee->name); ?> + +
+
+ + + +
+ popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> +
+ +

+
+ +
+
+ + +
+ withQueryString()->links()); ?> + +
+
+ +startPush('scripts'); ?> + +stopPush(); ?> +stopSection(); ?> + +make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..e7cd7eb5 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,54 @@ +import defaultTheme from 'tailwindcss/defaultTheme'; +import forms from '@tailwindcss/forms'; + +export default { + content: [ + './resources/**/*.blade.php', + './resources/**/*.js', + './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', + ], + darkMode: 'class', + theme: { + extend: { + fontFamily: { + sans: ['Vazirmatn', 'Inter', ...defaultTheme.fontFamily.sans], + }, + colors: { + brand: { + 50: '#fef3e2', + 100: '#fde4b9', + 200: '#fbc96f', + 300: '#f9ae2a', + 400: '#e89510', + 500: '#cf7a08', + 600: '#a95c06', + 700: '#834106', + 800: '#6e360b', + 900: '#5e2f0d', + }, + accent: { + 50: '#f0fdfa', + 100: '#ccfbf1', + 200: '#99f6e4', + 300: '#5eead4', + 400: '#2dd4bf', + 500: '#14b8a6', + 600: '#0d9488', + 700: '#0f766e', + 800: '#115e59', + 900: '#134e4a', + }, + sidebar: { + DEFAULT: '#1e293b', + foreground: '#f8fafc', + primary: '#14b8a6', + 'primary-foreground': '#ffffff', + accent: '#334155', + 'accent-foreground': '#f8fafc', + border: '#334155', + }, + }, + }, + }, + plugins: [forms], +}; diff --git a/vite.config.js b/vite.config.js index 421b5695..6a93a7c4 100644 --- a/vite.config.js +++ b/vite.config.js @@ -8,4 +8,9 @@ export default defineConfig({ refresh: true, }), ], + resolve: { + alias: { + '@': '/resources', + }, + }, });