From 135ffdb08597001b8db964778e81c43191689d6c Mon Sep 17 00:00:00 2001 From: upstream-mirror Date: Fri, 18 Sep 2026 04:37:41 +0500 Subject: [PATCH] snapshot of https://github.com/krayin/laravel-crm.git (2.2, 2026-09-18; blobs > 131072B omitted) --- .ai/skills | 1 + .claude/skills | 1 + .codex/skills | 1 + .cursor/skills | 1 + .editorconfig | 15 + .env.example | 71 + .gitattributes | 11 + .github/FUNDING.yml | 3 + .github/ISSUE_TEMPLATE/1_Bug_report.md | 36 + .github/ISSUE_TEMPLATE/2_Feature_request.md | 17 + .github/ISSUE_TEMPLATE/3_Support_question.md | 11 + .../4_Security_vulnerabilities.md | 9 + .github/PULL_REQUEST_TEMPLATE.md | 19 + .github/skills/README.md | 51 + .../skills/crm-package-development/SKILL.md | 222 + .github/skills/pest-testing/SKILL.md | 144 + .github/workflows/admin_playwright_tests.yml | 122 + .github/workflows/auto_commits.yml | 40 + .github/workflows/ci.yml | 55 + .github/workflows/skills-consistency.yml | 41 + .gitignore | 36 + .kilocode/skills | 1 + .styleci.yml | 9 + AGENTS.md | 70 + CHANGELOG.md | 237 + CODE_OF_CONDUCT.md | 76 + LICENSE | 20 + README.md | 152 + UPGRADE.md | 42 + app/Http/Controllers/Controller.php | 8 + app/Models/User.php | 48 + app/Providers/AppServiceProvider.php | 24 + artisan | 15 + bin/validate-skills.sh | 76 + bootstrap/app.php | 36 + bootstrap/cache/.gitignore | 2 + bootstrap/providers.php | 64 + composer.json | 115 + config/app.php | 189 + config/auth.php | 107 + config/breadcrumbs.php | 78 + config/broadcasting.php | 64 + config/cache.php | 106 + config/concord.php | 41 + config/cors.php | 62 + config/database.php | 166 + config/filesystems.php | 72 + config/hashing.php | 52 + config/imap.php | 207 + config/krayin-vite.php | 32 + config/logging.php | 104 + config/mail-receiver.php | 17 + config/mail.php | 130 + config/queue.php | 89 + config/repository.php | 249 ++ config/sanctum.php | 68 + config/services.php | 39 + config/session.php | 201 + config/tinker.php | 50 + config/view.php | 36 + database/.gitignore | 2 + database/factories/UserFactory.php | 47 + ..._08_19_000000_create_failed_jobs_table.php | 36 + ...01_create_personal_access_tokens_table.php | 33 + ..._09_09_094040_create_job_batches_table.php | 39 + .../2024_09_09_094042_create_jobs_table.php | 36 + database/seeders/DatabaseSeeder.php | 19 + example.txt | 1 + lang/en/auth.php | 20 + lang/en/pagination.php | 19 + lang/en/passwords.php | 22 + lang/en/validation.php | 152 + package.json | 13 + packages/Webkul/Activity/composer.json | 28 + .../Activity/src/Contracts/Activity.php | 5 + .../Webkul/Activity/src/Contracts/File.php | 5 + .../Activity/src/Contracts/Participant.php | 5 + ...1_05_12_150329_create_activities_table.php | 41 + ..._15_151855_create_activity_files_table.php | 37 + ...453_create_activity_participants_table.php | 39 + ...dd_location_column_in_activities_table.php | 32 + ...25_01_17_151632_alter_activities_table.php | 52 + .../Webkul/Activity/src/Models/Activity.php | 111 + .../Activity/src/Models/ActivityProxy.php | 7 + packages/Webkul/Activity/src/Models/File.php | 71 + .../Webkul/Activity/src/Models/FileProxy.php | 7 + .../Activity/src/Models/Participant.php | 52 + .../Activity/src/Models/ParticipantProxy.php | 7 + .../src/Providers/ActivityServiceProvider.php | 60 + .../src/Providers/ModuleServiceProvider.php | 17 + .../src/Repositories/ActivityRepository.php | 175 + .../src/Repositories/FileRepository.php | 19 + .../Repositories/ParticipantRepository.php | 18 + .../Activity/src/Traits/LogsActivity.php | 183 + packages/Webkul/Admin/.gitignore | 5 + packages/Webkul/Admin/composer.json | 35 + packages/Webkul/Admin/example.txt | 1 + packages/Webkul/Admin/package.json | 36 + packages/Webkul/Admin/postcss.config.cjs | 3 + packages/Webkul/Admin/src/Bouncer.php | 60 + packages/Webkul/Admin/src/Config/acl.php | 590 +++ .../src/Config/attribute_entity_types.php | 33 + .../Admin/src/Config/attribute_lookups.php | 54 + .../Webkul/Admin/src/Config/core_config.php | 353 ++ packages/Webkul/Admin/src/Config/menu.php | 301 ++ packages/Webkul/Admin/src/Config/support.php | 70 + .../DataGrids/Activity/ActivityDataGrid.php | 237 + .../Contact/OrganizationDataGrid.php | 120 + .../src/DataGrids/Contact/PersonDataGrid.php | 240 + .../Admin/src/DataGrids/Lead/LeadDataGrid.php | 388 ++ .../src/DataGrids/Mail/EmailDataGrid.php | 226 + .../src/DataGrids/Product/ProductDataGrid.php | 174 + .../src/DataGrids/Quote/QuoteDataGrid.php | 244 + .../DataGrids/Settings/AttributeDataGrid.php | 165 + .../Settings/DataTransfer/ImportDataGrid.php | 166 + .../Settings/EmailTemplateDataGrid.php | 87 + .../src/DataGrids/Settings/GroupDataGrid.php | 84 + .../Settings/Marketing/CampaignDatagrid.php | 110 + .../Settings/Marketing/EventDataGrid.php | 110 + .../DataGrids/Settings/PipelineDataGrid.php | 93 + .../src/DataGrids/Settings/RoleDataGrid.php | 120 + .../src/DataGrids/Settings/SourceDataGrid.php | 74 + .../src/DataGrids/Settings/TagDataGrid.php | 124 + .../src/DataGrids/Settings/TypeDataGrid.php | 76 + .../src/DataGrids/Settings/UserDataGrid.php | 190 + .../DataGrids/Settings/WarehouseDataGrid.php | 159 + .../DataGrids/Settings/WebhookDataGrid.php | 96 + .../DataGrids/Settings/WorkflowDataGrid.php | 76 + ..._view_permission_column_in_users_table.php | 32 + ...closed_date_column_in_attributes_table.php | 42 + ...ke_value_nullable_in_core_config_table.php | 28 + .../Webkul/Admin/src/Exceptions/Handler.php | 119 + packages/Webkul/Admin/src/Facades/Bouncer.php | 18 + .../Webkul/Admin/src/Helpers/Dashboard.php | 149 + .../Helpers/Reporting/AbstractReporting.php | 381 ++ .../Admin/src/Helpers/Reporting/Activity.php | 5 + .../Admin/src/Helpers/Reporting/Lead.php | 503 +++ .../src/Helpers/Reporting/Organization.php | 81 + .../Admin/src/Helpers/Reporting/Person.php | 82 + .../Admin/src/Helpers/Reporting/Product.php | 94 + .../Admin/src/Helpers/Reporting/Quote.php | 45 + .../Activity/ActivityController.php | 286 ++ .../Configuration/ConfigurationController.php | 85 + .../Contact/OrganizationController.php | 145 + .../Contact/Persons/ActivityController.php | 39 + .../Persons/GoogleContactExportController.php | 88 + .../Contact/Persons/PersonController.php | 260 ++ .../Contact/Persons/TagController.php | 56 + .../Admin/src/Http/Controllers/Controller.php | 67 + .../Http/Controllers/DashboardController.php | 131 + .../DataGrid/SavedFilterController.php | 117 + .../Http/Controllers/DataGridController.php | 42 + .../src/Http/Controllers/HelpController.php | 18 + .../Controllers/Lead/ActivityController.php | 39 + .../Http/Controllers/Lead/EmailController.php | 84 + .../Http/Controllers/Lead/LeadController.php | 848 ++++ .../Http/Controllers/Lead/QuoteController.php | 186 + .../Http/Controllers/Lead/TagController.php | 62 + .../Http/Controllers/Mail/EmailController.php | 378 ++ .../Http/Controllers/Mail/TagController.php | 56 + .../Products/ActivityController.php | 29 + .../Products/ProductController.php | 231 + .../Controllers/Products/TagController.php | 62 + .../Controllers/Quote/QuoteController.php | 324 ++ .../Settings/AttributeController.php | 265 ++ .../DataTransfer/ImportController.php | 531 +++ .../Settings/EmailTemplateController.php | 133 + .../Settings/GoogleContactController.php | 130 + .../Controllers/Settings/GroupController.php | 128 + .../Settings/LocationController.php | 80 + .../Marketing/CampaignsController.php | 157 + .../Settings/Marketing/EventController.php | 169 + .../Settings/PipelineController.php | 152 + .../Controllers/Settings/RoleController.php | 283 ++ .../Settings/SettingController.php | 98 + .../Controllers/Settings/SourceController.php | 117 + .../Controllers/Settings/TagController.php | 177 + .../Controllers/Settings/TypeController.php | 110 + .../Controllers/Settings/UserController.php | 518 +++ .../Settings/Warehouse/ActivityController.php | 29 + .../Settings/Warehouse/TagController.php | 62 + .../Warehouse/WarehouseController.php | 159 + .../Settings/WebFormController.php | 168 + .../Settings/WebhookController.php | 97 + .../Settings/WorkflowController.php | 119 + .../Http/Controllers/TinyMCEController.php | 77 + .../Controllers/User/AccountController.php | 98 + .../User/ForgotPasswordController.php | 76 + .../User/ResetPasswordController.php | 110 + .../Controllers/User/SessionController.php | 187 + .../Admin/src/Http/Middleware/Bouncer.php | 191 + .../Admin/src/Http/Middleware/Locale.php | 40 + .../Admin/src/Http/Middleware/SanitizeUrl.php | 60 + .../Admin/src/Http/Requests/AttributeForm.php | 120 + .../src/Http/Requests/ConfigurationForm.php | 77 + .../Admin/src/Http/Requests/LeadForm.php | 152 + .../src/Http/Requests/MassDestroyRequest.php | 31 + .../src/Http/Requests/MassUpdateRequest.php | 32 + .../Admin/src/Http/Requests/PipelineForm.php | 89 + .../Admin/src/Http/Requests/UserForm.php | 43 + .../Http/Resources/ActivityFileResource.php | 27 + .../Resources/ActivityParticipantResource.php | 23 + .../src/Http/Resources/ActivityResource.php | 36 + .../Resources/EmailAttachmentResource.php | 30 + .../src/Http/Resources/EmailResource.php | 44 + .../Admin/src/Http/Resources/LeadResource.php | 38 + .../Http/Resources/OrganizationResource.php | 26 + .../src/Http/Resources/PersonResource.php | 28 + .../src/Http/Resources/PipelineResource.php | 28 + .../src/Http/Resources/ProductResource.php | 28 + .../src/Http/Resources/QuoteResource.php | 33 + .../src/Http/Resources/SourceResource.php | 25 + .../src/Http/Resources/StageResource.php | 31 + .../Admin/src/Http/Resources/TagResource.php | 27 + .../Admin/src/Http/Resources/TypeResource.php | 25 + .../Admin/src/Http/Resources/UserResource.php | 27 + packages/Webkul/Admin/src/Http/helpers.php | 8 + .../Webkul/Admin/src/Listeners/Activity.php | 56 + packages/Webkul/Admin/src/Listeners/Lead.php | 30 + .../Webkul/Admin/src/Listeners/Person.php | 30 + .../Webkul/Admin/src/Notifications/Common.php | 38 + .../Admin/src/Notifications/User/Create.php | 27 + .../Notifications/User/UserResetPassword.php | 28 + .../Notifications/User/UserUpdatePassword.php | 34 + .../src/Providers/AdminServiceProvider.php | 111 + .../src/Providers/EventServiceProvider.php | 31 + .../src/Providers/ModuleServiceProvider.php | 10 + .../Admin/src/Requests/WebhookRequest.php | 109 + .../Admin/src/Resources/assets/css/app.css | 675 +++ .../src/Resources/assets/fonts/icomoon.woff | Bin 0 -> 37964 bytes .../Resources/assets/images/corner-clip.svg | 11 + .../assets/images/dark-corner-clip.svg | 21 + .../src/Resources/assets/images/dark-logo.svg | 6 + .../images/empty-placeholders/activities.svg | 23 + .../images/empty-placeholders/calls.svg | 21 + .../images/empty-placeholders/default.svg | 26 + .../images/empty-placeholders/description.svg | 20 + .../images/empty-placeholders/emails.svg | 19 + .../images/empty-placeholders/files.svg | 18 + .../images/empty-placeholders/lunches.svg | 19 + .../images/empty-placeholders/meetings.svg | 23 + .../images/empty-placeholders/notes.svg | 17 + .../empty-placeholders/organizations.svg | 32 + .../images/empty-placeholders/pipedrive.svg | 25 + .../images/empty-placeholders/plans.svg | 29 + .../images/empty-placeholders/products.svg | 24 + .../images/empty-placeholders/quotes.svg | 17 + .../images/empty-placeholders/users.svg | 19 + .../src/Resources/assets/images/error.svg | 29 + .../src/Resources/assets/images/favicon.ico | Bin 0 -> 1150 bytes .../src/Resources/assets/images/logo.svg | 6 + .../assets/images/mobile-dark-logo.svg | 5 + .../assets/images/mobile-light-logo.svg | 5 + .../Admin/src/Resources/assets/js/app.js | 141 + .../assets/js/directives/debounce.js | 17 + .../assets/js/directives/dompurify.js | 10 + .../Resources/assets/js/directives/tooltip.js | 143 + .../src/Resources/assets/js/plugins/admin.js | 148 + .../src/Resources/assets/js/plugins/axios.js | 14 + .../assets/js/plugins/createElement.js | 15 + .../Resources/assets/js/plugins/draggable.js | 10 + .../Resources/assets/js/plugins/emitter.js | 11 + .../Resources/assets/js/plugins/flatpickr.js | 56 + .../assets/js/plugins/vee-validate.js | 194 + .../Resources/assets/js/plugins/vue-cal.js | 16 + .../src/Resources/assets/locales/hi_IN.json | 32 + .../src/Resources/assets/locales/sin.json | 32 + .../Admin/src/Resources/lang/en/app.php | 2483 ++++++++++ .../Admin/src/Resources/lang/es/app.php | 2481 ++++++++++ .../Admin/src/Resources/lang/ja/app.php | 2483 ++++++++++ .../Admin/src/Resources/lang/ko/app.php | 2477 ++++++++++ .../Admin/src/Resources/lang/pt_BR/app.php | 2481 ++++++++++ .../Admin/src/Resources/lang/tr/app.php | 2481 ++++++++++ .../Admin/src/Resources/lang/vi/app.php | 2481 ++++++++++ .../Admin/src/Resources/lang/zh_CN/app.php | 2477 ++++++++++ .../activities/datagrid/is-done.blade.php | 13 + .../Resources/views/activities/edit.blade.php | 463 ++ .../views/activities/index.blade.php | 1161 +++++ .../components/accordion/index.blade.php | 84 + .../activities/actions/activity.blade.php | 285 ++ .../actions/activity/participants.blade.php | 245 + .../activities/actions/file.blade.php | 216 + .../activities/actions/mail.blade.php | 300 ++ .../activities/actions/note.blade.php | 175 + .../components/activities/index.blade.php | 741 +++ .../components/attachments/index.blade.php | 184 + .../attributes/edit/address.blade.php | 165 + .../attributes/edit/boolean.blade.php | 20 + .../attributes/edit/checkbox.blade.php | 32 + .../components/attributes/edit/date.blade.php | 19 + .../attributes/edit/datetime.blade.php | 8 + .../attributes/edit/email.blade.php | 179 + .../components/attributes/edit/file.blade.php | 39 + .../attributes/edit/image.blade.php | 42 + .../attributes/edit/index.blade.php | 144 + .../attributes/edit/lookup.blade.php | 314 ++ .../attributes/edit/multiselect.blade.php | 32 + .../attributes/edit/phone.blade.php | 186 + .../attributes/edit/price.blade.php | 32 + .../attributes/edit/select.blade.php | 21 + .../components/attributes/edit/text.blade.php | 8 + .../attributes/edit/textarea.blade.php | 8 + .../components/attributes/index.blade.php | 53 + .../components/attributes/view.blade.php | 25 + .../attributes/view/address.blade.php | 11 + .../attributes/view/boolean.blade.php | 11 + .../attributes/view/checkbox.blade.php | 20 + .../components/attributes/view/date.blade.php | 21 + .../attributes/view/datetime.blade.php | 11 + .../attributes/view/email.blade.php | 11 + .../components/attributes/view/file.blade.php | 11 + .../attributes/view/image.blade.php | 11 + .../attributes/view/lookup.blade.php | 16 + .../attributes/view/multiselect.blade.php | 20 + .../attributes/view/phone.blade.php | 11 + .../attributes/view/price.blade.php | 14 + .../attributes/view/select.blade.php | 18 + .../components/attributes/view/text.blade.php | 13 + .../attributes/view/textarea.blade.php | 12 + .../views/components/avatar/index.blade.php | 69 + .../components/breadcrumbs/index.blade.php | 15 + .../views/components/button/index.blade.php | 40 + .../views/components/charts/bar.blade.php | 100 + .../components/charts/doughnut.blade.php | 68 + .../views/components/charts/line.blade.php | 97 + .../datagrid/column-settings.blade.php | 93 + .../datagrid/export/index.blade.php | 308 ++ .../components/datagrid/export/temp.blade.php | 23 + .../views/components/datagrid/index.blade.php | 635 +++ .../views/components/datagrid/table.blade.php | 377 ++ .../components/datagrid/toolbar.blade.php | 94 + .../datagrid/toolbar/filter.blade.php | 1530 +++++++ .../datagrid/toolbar/mass-action.blade.php | 220 + .../datagrid/toolbar/pagination.blade.php | 157 + .../datagrid/toolbar/search.blade.php | 157 + .../views/components/drawer/index.blade.php | 201 + .../views/components/dropdown/index.blade.php | 170 + .../components/dropdown/menu/item.blade.php | 3 + .../views/components/example.blade.php | 12 + .../components/flash-group/index.blade.php | 64 + .../components/flash-group/item.blade.php | 205 + .../components/flat-picker/date.blade.php | 81 + .../components/flat-picker/datetime.blade.php | 83 + .../form/control-group/control.blade.php | 361 ++ .../controls/inline/address.blade.php | 370 ++ .../controls/inline/boolean.blade.php | 250 ++ .../controls/inline/date.blade.php | 245 + .../controls/inline/datetime.blade.php | 245 + .../controls/inline/email.blade.php | 320 ++ .../controls/inline/file.blade.php | 245 + .../controls/inline/image.blade.php | 242 + .../controls/inline/lookup.blade.php | 448 ++ .../controls/inline/multiselect.blade.php | 367 ++ .../controls/inline/phone.blade.php | 321 ++ .../controls/inline/select.blade.php | 270 ++ .../controls/inline/text.blade.php | 300 ++ .../control-group/controls/tags.blade.php | 183 + .../form/control-group/error.blade.php | 16 + .../form/control-group/index.blade.php | 3 + .../form/control-group/label.blade.php | 3 + .../views/components/form/index.blade.php | 40 + .../components/layouts/anonymous.blade.php | 124 + .../header/desktop/mega-search.blade.php | 567 +++ .../components/layouts/header/index.blade.php | 201 + .../header/mobile/mega-search.blade.php | 593 +++ .../layouts/header/quick-creation.blade.php | 443 ++ .../views/components/layouts/index.blade.php | 169 + .../layouts/sidebar/desktop/index.blade.php | 134 + .../layouts/sidebar/mobile/index.blade.php | 120 + .../views/components/layouts/tabs.blade.php | 20 + .../views/components/lookup/index.blade.php | 365 ++ .../views/components/media/images.blade.php | 334 ++ .../views/components/media/videos.blade.php | 307 ++ .../views/components/modal/confirm.blade.php | 140 + .../views/components/modal/index.blade.php | 225 + .../shimmer/accordion/index.blade.php | 17 + .../shimmer/activities/index.blade.php | 41 + .../components/shimmer/charts/bar.blade.php | 28 + .../shimmer/common/address.blade.php | 15 + .../index/open-leads-by-states.blade.php | 16 + .../dashboard/index/over-all.blade.php | 13 + .../index/revenue-by-sources.blade.php | 21 + .../index/revenue-by-types.blade.php | 21 + .../shimmer/dashboard/index/revenue.blade.php | 46 + .../dashboard/index/top-persons.blade.php | 25 + .../index/top-selling-products.blade.php | 25 + .../dashboard/index/total-leads.blade.php | 18 + .../shimmer/datagrid/index.blade.php | 15 + .../shimmer/datagrid/table/body.blade.php | 53 + .../shimmer/datagrid/table/head.blade.php | 29 + .../shimmer/datagrid/toolbar.blade.php | 27 + .../shimmer/datagrid/toolbar/filter.blade.php | 3 + .../datagrid/toolbar/pagination.blade.php | 17 + .../shimmer/datagrid/toolbar/search.blade.php | 5 + .../mega-search/configurations.blade.php | 12 + .../header/mega-search/leads.blade.php | 17 + .../header/mega-search/persons.blade.php | 17 + .../header/mega-search/products.blade.php | 17 + .../header/mega-search/quotes.blade.php | 12 + .../header/mega-search/settings.blade.php | 12 + .../components/shimmer/image/index.blade.php | 44 + .../shimmer/leads/datagrid.blade.php | 146 + .../shimmer/leads/index/kanban.blade.php | 60 + .../leads/index/kanban/toolbar.blade.php | 17 + .../shimmer/leads/view/mail/index.blade.php | 111 + .../shimmer/leads/view/stages.blade.php | 9 + .../shimmer/mail/datagrid/index.blade.php | 13 + .../mail/datagrid/table/body.blade.php | 31 + .../mail/datagrid/table/head.blade.php | 13 + .../person/view/activities/index.blade.php | 41 + .../shimmer/person/view/stages.blade.php | 9 + .../shimmer/person/view/tags.blade.php | 9 + .../shimmer/pipelines/kanban.blade.php | 33 + .../components/shimmer/quotes/index.blade.php | 116 + .../shimmer/settings/attributes.blade.php | 62 + .../shimmer/settings/web-forms/body.blade.php | 13 + .../shimmer/settings/web-forms/head.blade.php | 7 + .../settings/web-forms/index.blade.php | 13 + .../components/shimmer/tabs/index.blade.php | 9 + .../components/shimmer/tags/index.blade.php | 9 + .../shimmer/tinymce/index.blade.php | 3 + .../components/shimmer/tree/index.blade.php | 31 + .../views/components/spinner/index.blade.php | 27 + .../views/components/table/index.blade.php | 3 + .../components/table/tbody/index.blade.php | 3 + .../views/components/table/tbody/tr.blade.php | 3 + .../views/components/table/td.blade.php | 4 + .../views/components/table/th.blade.php | 3 + .../components/table/thead/index.blade.php | 3 + .../views/components/table/thead/tr.blade.php | 3 + .../views/components/tabs/index.blade.php | 65 + .../views/components/tabs/item.blade.php | 51 + .../views/components/tags/index.blade.php | 398 ++ .../views/components/tinymce/index.blade.php | 219 + .../views/components/tree/checkbox.blade.php | 55 + .../views/components/tree/radio.blade.php | 54 + .../views/components/tree/view.blade.php | 393 ++ .../views/configuration/edit.blade.php | 85 + .../views/configuration/field-type.blade.php | 524 +++ .../views/configuration/index.blade.php | 196 + .../contacts/organizations/create.blade.php | 74 + .../contacts/organizations/edit.blade.php | 76 + .../contacts/organizations/index.blade.php | 50 + .../views/contacts/persons/create.blade.php | 117 + .../views/contacts/persons/edit.blade.php | 120 + .../views/contacts/persons/index.blade.php | 344 ++ .../views/contacts/persons/view.blade.php | 100 + .../persons/view/attributes.blade.php | 41 + .../persons/view/organization.blade.php | 73 + .../Resources/views/dashboard/index.blade.php | 203 + .../index/open-leads-by-states.blade.php | 168 + .../views/dashboard/index/over-all.blade.php | 231 + .../index/revenue-by-sources.blade.php | 154 + .../index/revenue-by-types.blade.php | 154 + .../views/dashboard/index/revenue.blade.php | 235 + .../dashboard/index/top-persons.blade.php | 120 + .../index/top-selling-products.blade.php | 129 + .../dashboard/index/total-leads.blade.php | 128 + .../Resources/views/dashboard/pdf.blade.php | 468 ++ .../views/emails/common/index.blade.php | 3 + .../Resources/views/emails/layout.blade.php | 36 + .../views/emails/users/create.blade.php | 11 + .../emails/users/forget-password.blade.php | 28 + .../Resources/views/errors/index.blade.php | 64 + .../src/Resources/views/help/card.blade.php | 29 + .../src/Resources/views/help/index.blade.php | 124 + .../views/leads/common/contact.blade.php | 142 + .../views/leads/common/products.blade.php | 271 ++ .../Resources/views/leads/create.blade.php | 240 + .../src/Resources/views/leads/edit.blade.php | 229 + .../src/Resources/views/leads/index.blade.php | 66 + .../views/leads/index/kanban.blade.php | 881 ++++ .../index/kanban/card-settings.blade.php | 120 + .../views/leads/index/kanban/filter.blade.php | 1093 +++++ .../views/leads/index/kanban/search.blade.php | 98 + .../leads/index/kanban/toolbar.blade.php | 37 + .../views/leads/index/table.blade.php | 14 + .../views/leads/index/upload.blade.php | 141 + .../views/leads/index/view-switcher.blade.php | 95 + .../src/Resources/views/leads/view.blade.php | 144 + .../views/leads/view/attributes.blade.php | 49 + .../views/leads/view/person.blade.php | 357 ++ .../views/leads/view/products.blade.php | 445 ++ .../views/leads/view/quotes.blade.php | 273 ++ .../views/leads/view/stages.blade.php | 257 ++ .../src/Resources/views/mail/index.blade.php | 712 +++ .../src/Resources/views/mail/view.blade.php | 2011 +++++++++ .../views/partials/breadcrumbs.blade.php | 27 + .../Resources/views/products/create.blade.php | 107 + .../Resources/views/products/edit.blade.php | 107 + .../Resources/views/products/index.blade.php | 45 + .../Resources/views/products/view.blade.php | 112 + .../views/products/view/attributes.blade.php | 44 + .../views/products/view/inventory.blade.php | 505 +++ .../Resources/views/quotes/create.blade.php | 943 ++++ .../src/Resources/views/quotes/edit.blade.php | 940 ++++ .../Resources/views/quotes/index.blade.php | 88 + .../src/Resources/views/quotes/pdf.blade.php | 430 ++ .../views/sessions/forgot-password.blade.php | 86 + .../Resources/views/sessions/login.blade.php | 125 + .../views/sessions/reset-password.blade.php | 131 + .../settings/attributes/create.blade.php | 688 +++ .../views/settings/attributes/edit.blade.php | 765 ++++ .../views/settings/attributes/index.blade.php | 51 + .../data-transfer/imports/create.blade.php | 232 + .../data-transfer/imports/edit.blade.php | 251 ++ .../data-transfer/imports/import.blade.php | 561 +++ .../data-transfer/imports/index.blade.php | 50 + .../settings/email-templates/create.blade.php | 234 + .../settings/email-templates/edit.blade.php | 238 + .../settings/email-templates/index.blade.php | 51 + .../settings/google-contacts/index.blade.php | 50 + .../views/settings/groups/index.blade.php | 352 ++ .../Resources/views/settings/index.blade.php | 58 + .../marketing/campaigns/index.blade.php | 525 +++ .../settings/marketing/events/index.blade.php | 410 ++ .../views/settings/pipelines/create.blade.php | 427 ++ .../views/settings/pipelines/edit.blade.php | 389 ++ .../views/settings/pipelines/index.blade.php | 52 + .../views/settings/roles/create.blade.php | 214 + .../views/settings/roles/edit.blade.php | 222 + .../views/settings/roles/index.blade.php | 47 + .../views/settings/sources/index.blade.php | 317 ++ .../views/settings/tags/index.blade.php | 392 ++ .../views/settings/types/index.blade.php | 311 ++ .../views/settings/users/index.blade.php | 605 +++ .../settings/warehouses/create.blade.php | 107 + .../views/settings/warehouses/edit.blade.php | 112 + .../views/settings/warehouses/index.blade.php | 47 + .../views/settings/warehouses/view.blade.php | 117 + .../view/contact-information.blade.php | 38 + .../view/general-information.blade.php | 38 + .../warehouses/view/locations.blade.php | 208 + .../views/settings/web-forms/create.blade.php | 731 +++ .../views/settings/web-forms/edit.blade.php | 866 ++++ .../views/settings/web-forms/index.blade.php | 120 + .../views/settings/webhook/create.blade.php | 699 +++ .../views/settings/webhook/edit.blade.php | 712 +++ .../views/settings/webhook/index.blade.php | 46 + .../views/settings/workflows/create.blade.php | 1277 ++++++ .../views/settings/workflows/edit.blade.php | 1296 ++++++ .../views/settings/workflows/index.blade.php | 50 + .../views/user/account/edit.blade.php | 201 + .../src/Routes/Admin/activities-routes.php | 24 + .../Admin/src/Routes/Admin/auth-routes.php | 51 + .../src/Routes/Admin/configuration-routes.php | 16 + .../src/Routes/Admin/contacts-routes.php | 77 + .../Admin/src/Routes/Admin/help-routes.php | 8 + .../Admin/src/Routes/Admin/leads-routes.php | 66 + .../Admin/src/Routes/Admin/mail-routes.php | 33 + .../src/Routes/Admin/products-routes.php | 42 + .../Admin/src/Routes/Admin/quote-routes.php | 26 + .../Admin/src/Routes/Admin/rest-routes.php | 56 + .../src/Routes/Admin/settings-routes.php | 391 ++ .../Webkul/Admin/src/Routes/Admin/web.php | 56 + .../Webkul/Admin/src/Routes/Front/web.php | 9 + .../src/Traits/HandlesActivityTimeline.php | 190 + .../src/Traits/ProvideDropdownOptions.php | 284 ++ packages/Webkul/Admin/tailwind.config.js | 49 + packages/Webkul/Admin/tests/e2e-pw/.gitignore | 2 + .../Admin/tests/e2e-pw/.state/admin-auth.json | 35 + .../tests/e2e-pw/data/data-transfer/leads.csv | 3 + .../data/data-transfer/leads_updated.csv | 3 + .../e2e-pw/data/data-transfer/persons.csv | 3 + .../data/data-transfer/persons_updated.csv | 3 + .../e2e-pw/data/data-transfer/products.csv | 3 + .../data/data-transfer/products_updated.csv | 3 + .../Admin/tests/e2e-pw/data/images/10.webp | Bin 0 -> 29098 bytes .../Admin/tests/e2e-pw/data/images/11.webp | Bin 0 -> 21748 bytes .../Admin/tests/e2e-pw/data/images/12.webp | Bin 0 -> 24766 bytes .../Admin/tests/e2e-pw/data/images/3.webp | Bin 0 -> 123028 bytes .../Admin/tests/e2e-pw/data/images/4.webp | Bin 0 -> 90892 bytes .../Admin/tests/e2e-pw/data/images/6.webp | Bin 0 -> 36952 bytes .../Admin/tests/e2e-pw/data/images/7.webp | Bin 0 -> 24766 bytes .../Admin/tests/e2e-pw/data/images/8.webp | Bin 0 -> 29098 bytes .../Admin/tests/e2e-pw/data/images/9.webp | Bin 0 -> 27758 bytes .../tests/e2e-pw/fixtures/AdminFixtures.ts | 80 + .../tests/e2e-pw/locator/CoreLocators.ts | 597 +++ .../Admin/tests/e2e-pw/pages/AclPage.ts | 146 + .../Admin/tests/e2e-pw/pages/AdminPage.ts | 47 + .../tests/e2e-pw/pages/ConfigurationPage.ts | 0 .../Admin/tests/e2e-pw/pages/LeadPage.ts | 168 + .../Admin/tests/e2e-pw/pages/MailPage.ts | 139 + .../tests/e2e-pw/pages/OrganizationPage.ts | 99 + .../Admin/tests/e2e-pw/pages/PersonsPage.ts | 121 + .../Admin/tests/e2e-pw/pages/ProductPage.ts | 86 + .../Admin/tests/e2e-pw/pages/QuickAddPage.ts | 199 + .../Admin/tests/e2e-pw/pages/QuotesPage.ts | 204 + .../Admin/tests/e2e-pw/pages/SettingsPage.ts | 96 + .../e2e-pw/pages/settings/AttributesPage.ts | 78 + .../e2e-pw/pages/settings/AutomationPage.ts | 13 + .../e2e-pw/pages/settings/CampaignsPage.ts | 114 + .../e2e-pw/pages/settings/DataTransferPage.ts | 134 + .../pages/settings/EmailTemplatesPage.ts | 69 + .../tests/e2e-pw/pages/settings/EventsPage.ts | 82 + .../tests/e2e-pw/pages/settings/GroupsPage.ts | 71 + .../pages/settings/OtherSettingsPage.ts | 13 + .../e2e-pw/pages/settings/PipelinesPage.ts | 13 + .../tests/e2e-pw/pages/settings/RolesPage.ts | 212 + .../e2e-pw/pages/settings/SourcesPage.ts | 39 + .../tests/e2e-pw/pages/settings/TagsPage.ts | 64 + .../tests/e2e-pw/pages/settings/UsersPage.ts | 236 + .../e2e-pw/pages/settings/WarehousesPage.ts | 104 + .../e2e-pw/pages/settings/WebFormsPage.ts | 13 + .../e2e-pw/pages/settings/WebhooksPage.ts | 100 + .../e2e-pw/pages/settings/WorkflowsPage.ts | 13 + .../Admin/tests/e2e-pw/playwright.config.ts | 56 + .../Admin/tests/e2e-pw/tests/admin.spec.ts | 23 + .../tests/e2e-pw/tests/lang/lang.spec.ts | 82 + .../Admin/tests/e2e-pw/tests/lead.spec.ts | 56 + .../Admin/tests/e2e-pw/tests/mail.spec.ts | 89 + .../tests/e2e-pw/tests/organization.spec.ts | 51 + .../Admin/tests/e2e-pw/tests/persons.spec.ts | 51 + .../Admin/tests/e2e-pw/tests/product.spec.ts | 31 + .../tests/e2e-pw/tests/quick-add.spec.ts | 72 + .../Admin/tests/e2e-pw/tests/quotes.spec.ts | 55 + .../tests/security/acl-permission.spec.ts | 245 + .../tests/security/auth-security.spec.ts | 149 + .../security/privilege-escalation.spec.ts | 333 ++ .../tests/security/view-permission.spec.ts | 283 ++ .../e2e-pw/tests/settings/attritube.spec.ts | 31 + .../e2e-pw/tests/settings/campaign.spec.ts | 44 + .../tests/settings/data-transfer.spec.ts | 135 + .../tests/settings/email-tamplet.spec.ts | 22 + .../e2e-pw/tests/settings/events.spec.ts | 28 + .../tests/e2e-pw/tests/settings/group.spec.ts | 25 + .../tests/e2e-pw/tests/settings/roles.spec.ts | 117 + .../e2e-pw/tests/settings/source.spec.ts | 31 + .../tests/e2e-pw/tests/settings/tag.spec.ts | 34 + .../e2e-pw/tests/settings/warehouse.spec.ts | 37 + .../e2e-pw/tests/settings/webhook.spec.ts | 23 + .../Webkul/Admin/tests/e2e-pw/utils/acl.ts | 276 ++ .../Admin/tests/e2e-pw/utils/address.ts | 58 + .../Admin/tests/e2e-pw/utils/customer.ts | 159 + .../Webkul/Admin/tests/e2e-pw/utils/faker.ts | 856 ++++ .../Webkul/Admin/tests/e2e-pw/utils/form.ts | 198 + .../Webkul/Admin/tests/e2e-pw/utils/gdpr.ts | 221 + .../Webkul/Admin/tests/e2e-pw/utils/lang.ts | 17 + .../Admin/tests/e2e-pw/utils/savedata.ts | 49 + .../Admin/tests/e2e-pw/utils/tinymce.ts | 29 + packages/Webkul/Admin/vite.config.js | 48 + packages/Webkul/Attribute/composer.json | 27 + .../src/Config/attribute_lookups.php | 5 + .../Attribute/src/Contracts/Attribute.php | 5 + .../src/Contracts/AttributeOption.php | 5 + .../src/Contracts/AttributeValue.php | 5 + ...1_04_02_080709_create_attributes_table.php | 43 + ..._080837_create_attribute_options_table.php | 34 + ...6_122751_create_attribute_values_table.php | 44 + ...02_191710_alter_attribute_values_table.php | 39 + .../Webkul/Attribute/src/Models/Attribute.php | 35 + .../Attribute/src/Models/AttributeOption.php | 30 + .../src/Models/AttributeOptionProxy.php | 7 + .../Attribute/src/Models/AttributeProxy.php | 7 + .../Attribute/src/Models/AttributeValue.php | 85 + .../src/Models/AttributeValueProxy.php | 7 + .../Providers/AttributeServiceProvider.php | 41 + .../src/Providers/ModuleServiceProvider.php | 24 + .../AttributeOptionRepository.php | 18 + .../src/Repositories/AttributeRepository.php | 220 + .../Repositories/AttributeValueRepository.php | 268 ++ .../Attribute/src/Traits/CustomAttribute.php | 176 + .../Automation/src/Config/workflows.php | 74 + .../Automation/src/Contracts/Webhook.php | 5 + .../Automation/src/Contracts/Workflow.php | 5 + ...21_08_26_133538_create_workflows_table.php | 38 + ...024_07_24_150821_create_webhooks_table.php | 37 + .../Webkul/Automation/src/Helpers/Entity.php | 103 + .../src/Helpers/Entity/AbstractEntity.php | 232 + .../src/Helpers/Entity/Activity.php | 324 ++ .../Automation/src/Helpers/Entity/Lead.php | 210 + .../Automation/src/Helpers/Entity/Person.php | 140 + .../Automation/src/Helpers/Entity/Quote.php | 172 + .../Automation/src/Helpers/Validator.php | 185 + .../Automation/src/Listeners/Entity.php | 45 + .../Webkul/Automation/src/Models/Webhook.php | 41 + .../Automation/src/Models/WebhookProxy.php | 7 + .../Webkul/Automation/src/Models/Workflow.php | 24 + .../Automation/src/Models/WorkflowProxy.php | 7 + .../src/Providers/ModuleServiceProvider.php | 20 + .../src/Providers/WorkflowServiceProvider.php | 48 + .../src/Repositories/WebhookRepository.php | 17 + .../src/Repositories/WorkflowRepository.php | 17 + .../src/Services/WebhookService.php | 508 +++ packages/Webkul/Contact/composer.json | 28 + .../Contact/src/Contracts/Organization.php | 5 + .../Webkul/Contact/src/Contracts/Person.php | 5 + .../src/Database/Factories/PersonFactory.php | 30 + ...4_09_051326_create_organizations_table.php | 34 + ...2021_04_09_065617_create_persons_table.php | 37 + ...e_index_to_name_in_organizations_table.php | 32 + ..._092951_add_job_title_in_persons_table.php | 28 + ..._08_06_145943_create_person_tags_table.php | 30 + ..._161212_create_person_activities_table.php | 30 + ...16_add_user_id_column_in_persons_table.php | 30 + ..._user_id_column_in_organizations_table.php | 30 + ...9_112201_add_unique_id_to_person_table.php | 41 + ...rganization_id_column_in_persons_table.php | 32 + .../Contact/src/Models/Organization.php | 47 + .../Contact/src/Models/OrganizationProxy.php | 7 + packages/Webkul/Contact/src/Models/Person.php | 109 + .../Webkul/Contact/src/Models/PersonProxy.php | 7 + .../src/Providers/ContactServiceProvider.php | 26 + .../src/Providers/ModuleServiceProvider.php | 15 + .../Repositories/OrganizationRepository.php | 119 + .../src/Repositories/PersonRepository.php | 184 + packages/Webkul/Core/composer.json | 26 + packages/Webkul/Core/src/Acl.php | 169 + packages/Webkul/Core/src/Acl/AclItem.php | 19 + packages/Webkul/Core/src/Config/concord.php | 42 + packages/Webkul/Core/src/Config/cors.php | 62 + packages/Webkul/Core/src/Config/sanctum.php | 68 + .../Core/src/Console/Commands/Version.php | 42 + .../Webkul/Core/src/Contracts/CoreConfig.php | 5 + .../Webkul/Core/src/Contracts/Country.php | 5 + .../Core/src/Contracts/CountryState.php | 5 + .../Core/src/Contracts/Validations/Code.php | 30 + .../src/Contracts/Validations/Decimal.php | 22 + packages/Webkul/Core/src/Core.php | 247 + ..._03_12_060658_create_core_config_table.php | 33 + ...21_04_12_173232_create_countries_table.php | 32 + ..._12_173344_create_country_states_table.php | 36 + ..._text_column_type_in_core_config_table.php | 28 + .../Webkul/Core/src/Eloquent/Repository.php | 146 + .../Core/src/Eloquent/TranslatableModel.php | 23 + .../Core/src/Exceptions/ViterNotFound.php | 17 + packages/Webkul/Core/src/Facades/Acl.php | 18 + packages/Webkul/Core/src/Facades/Core.php | 18 + packages/Webkul/Core/src/Facades/Menu.php | 18 + .../Webkul/Core/src/Facades/SystemConfig.php | 18 + packages/Webkul/Core/src/Helpers/Helper.php | 27 + packages/Webkul/Core/src/Http/helpers.php | 74 + packages/Webkul/Core/src/Menu.php | 239 + packages/Webkul/Core/src/Menu/MenuItem.php | 200 + .../Webkul/Core/src/Models/CoreConfig.php | 24 + .../Core/src/Models/CoreConfigProxy.php | 7 + packages/Webkul/Core/src/Models/Country.php | 11 + .../Webkul/Core/src/Models/CountryProxy.php | 7 + .../Webkul/Core/src/Models/CountryState.php | 11 + .../Core/src/Models/CountryStateProxy.php | 7 + .../Providers/BaseModuleServiceProvider.php | 32 + .../src/Providers/CoreServiceProvider.php | 89 + .../src/Providers/ModuleServiceProvider.php | 16 + .../src/Repositories/CoreConfigRepository.php | 284 ++ .../src/Repositories/CountryRepository.php | 21 + .../Repositories/CountryStateRepository.php | 21 + packages/Webkul/Core/src/SystemConfig.php | 188 + .../Webkul/Core/src/SystemConfig/Item.php | 123 + .../Core/src/SystemConfig/ItemField.php | 244 + .../Webkul/Core/src/Traits/PDFHandler.php | 67 + packages/Webkul/Core/src/Traits/Sanitizer.php | 140 + .../Core/src/ViewRenderEventManager.php | 89 + packages/Webkul/Core/src/Vite.php | 49 + packages/Webkul/DataGrid/src/Action.php | 34 + packages/Webkul/DataGrid/src/Column.php | 374 ++ .../DataGrid/src/ColumnTypes/Aggregate.php | 41 + .../DataGrid/src/ColumnTypes/Boolean.php | 68 + .../Webkul/DataGrid/src/ColumnTypes/Date.php | 70 + .../DataGrid/src/ColumnTypes/Datetime.php | 58 + .../DataGrid/src/ColumnTypes/Decimal.php | 26 + .../DataGrid/src/ColumnTypes/Integer.php | 26 + .../Webkul/DataGrid/src/ColumnTypes/Text.php | 41 + .../DataGrid/src/Contracts/SavedFilter.php | 5 + packages/Webkul/DataGrid/src/DataGrid.php | 606 +++ ...5_10_152848_create_saved_filters_table.php | 33 + .../DataGrid/src/Enums/ColumnTypeEnum.php | 67 + .../src/Enums/DateRangeOptionEnum.php | 103 + .../DataGrid/src/Enums/FilterTypeEnum.php | 21 + .../src/Exceptions/InvalidColumnException.php | 7 + .../Exceptions/InvalidColumnTypeException.php | 7 + .../Exceptions/InvalidDataGridException.php | 7 + .../DataGrid/src/Exports/DataGridExport.php | 78 + packages/Webkul/DataGrid/src/Http/helpers.php | 18 + packages/Webkul/DataGrid/src/MassAction.php | 34 + .../DataGrid/src/Models/SavedFilter.php | 40 + .../DataGrid/src/Models/SavedFilterProxy.php | 7 + .../src/Providers/DataGridServiceProvider.php | 23 + .../src/Providers/ModuleServiceProvider.php | 13 + .../Repositories/SavedFilterRepository.php | 17 + .../DataTransfer/src/Config/importers.php | 21 + .../DataTransfer/src/Contracts/Import.php | 5 + .../src/Contracts/ImportBatch.php | 5 + ...2024_01_11_154640_create_imports_table.php | 45 + ..._11_154741_create_import_batches_table.php | 32 + .../Webkul/DataTransfer/src/Helpers/Error.php | 186 + .../DataTransfer/src/Helpers/Import.php | 617 +++ .../Helpers/Importers/AbstractImporter.php | 553 +++ .../src/Helpers/Importers/Leads/Importer.php | 623 +++ .../src/Helpers/Importers/Leads/Storage.php | 97 + .../Helpers/Importers/Persons/Importer.php | 704 +++ .../src/Helpers/Importers/Persons/Storage.php | 99 + .../Helpers/Importers/Products/Importer.php | 369 ++ .../Helpers/Importers/Products/SKUStorage.php | 107 + .../src/Helpers/Sources/AbstractSource.php | 120 + .../DataTransfer/src/Helpers/Sources/CSV.php | 79 + .../src/Helpers/Sources/Excel.php | 70 + .../src/Jobs/Import/Completed.php | 38 + .../src/Jobs/Import/ImportBatch.php | 41 + .../src/Jobs/Import/IndexBatch.php | 41 + .../DataTransfer/src/Jobs/Import/Indexing.php | 38 + .../src/Jobs/Import/LinkBatch.php | 41 + .../DataTransfer/src/Jobs/Import/Linking.php | 38 + .../Webkul/DataTransfer/src/Models/Import.php | 63 + .../DataTransfer/src/Models/ImportBatch.php | 49 + .../src/Models/ImportBatchProxy.php | 7 + .../DataTransfer/src/Models/ImportProxy.php | 7 + .../Providers/DataTransferServiceProvider.php | 24 + .../src/Providers/ModuleServiceProvider.php | 20 + .../Repositories/ImportBatchRepository.php | 17 + .../src/Repositories/ImportRepository.php | 17 + packages/Webkul/Email/composer.json | 28 + .../Console/Commands/ProcessInboundEmails.php | 48 + .../Webkul/Email/src/Contracts/Attachment.php | 5 + packages/Webkul/Email/src/Contracts/Email.php | 5 + .../2021_05_24_075618_create_emails_table.php | 58 + ..._072700_create_email_attachments_table.php | 40 + ...4_08_27_091619_create_email_tags_table.php | 30 + .../Email/src/Enums/SupportedFolderEnum.php | 46 + .../Webkul/Email/src/Helpers/Attachment.php | 108 + packages/Webkul/Email/src/Helpers/Charset.php | 353 ++ .../src/Helpers/Contracts/CharsetManager.php | 20 + .../Webkul/Email/src/Helpers/HtmlFilter.php | 1207 +++++ packages/Webkul/Email/src/Helpers/Parser.php | 888 ++++ .../Contracts/InboundEmailProcessor.php | 20 + .../SendgridEmailProcessor.php | 125 + .../WebklexImapEmailProcessor.php | 231 + packages/Webkul/Email/src/Mails/Email.php | 56 + .../Webkul/Email/src/Models/Attachment.php | 67 + .../Email/src/Models/AttachmentProxy.php | 7 + packages/Webkul/Email/src/Models/Email.php | 127 + .../Webkul/Email/src/Models/EmailProxy.php | 7 + .../src/Providers/EmailServiceProvider.php | 58 + .../src/Providers/ModuleServiceProvider.php | 15 + .../src/Repositories/AttachmentRepository.php | 103 + .../src/Repositories/EmailRepository.php | 95 + .../src/Contracts/EmailTemplate.php | 5 + ...03_172713_create_email_templates_table.php | 34 + ..._09_133553_alter_email_templates_table.php | 30 + .../src/Models/EmailTemplate.php | 15 + .../src/Models/EmailTemplateProxy.php | 7 + .../EmailTemplateServiceProvider.php | 18 + .../src/Providers/ModuleServiceProvider.php | 13 + .../Repositories/EmailTemplateRepository.php | 18 + .../Webkul/GoogleContact/src/Config/acl.php | 15 + .../Webkul/GoogleContact/src/Config/menu.php | 12 + .../src/Contracts/ContactExportBatch.php | 5 + .../src/Contracts/ContactExportBatchItem.php | 5 + .../src/Contracts/GoogleContactAccount.php | 5 + ...1_create_google_contact_accounts_table.php | 35 + ...01_create_contact_export_batches_table.php | 38 + ...reate_contact_export_batch_items_table.php | 34 + .../src/Jobs/Export/CompleteExportBatch.php | 36 + .../src/Jobs/Export/ExportChunk.php | 110 + .../src/Jobs/Export/PrepareExportBatch.php | 115 + .../src/Models/ContactExportBatch.php | 91 + .../src/Models/ContactExportBatchItem.php | 55 + .../Models/ContactExportBatchItemProxy.php | 7 + .../src/Models/ContactExportBatchProxy.php | 7 + .../src/Models/GoogleContactAccount.php | 62 + .../src/Models/GoogleContactAccountProxy.php | 7 + .../GoogleContactServiceProvider.php | 38 + .../src/Providers/ModuleServiceProvider.php | 17 + .../ContactExportBatchItemRepository.php | 17 + .../ContactExportBatchRepository.php | 17 + .../GoogleContactAccountRepository.php | 17 + .../src/Resources/lang/ar/app.php | 12 + .../src/Resources/lang/en/app.php | 12 + .../src/Resources/lang/es/app.php | 12 + .../src/Resources/lang/fa/app.php | 12 + .../src/Resources/lang/ja/app.php | 12 + .../src/Resources/lang/ko/app.php | 12 + .../src/Resources/lang/pt_BR/app.php | 12 + .../src/Resources/lang/tr/app.php | 12 + .../src/Resources/lang/vi/app.php | 12 + .../src/Services/GoogleClientFactory.php | 46 + .../src/Services/GoogleContactsService.php | 153 + .../src/Services/TokenRefresher.php | 65 + packages/Webkul/Installer/.gitignore | 3 + packages/Webkul/Installer/composer.json | 26 + packages/Webkul/Installer/package.json | 24 + packages/Webkul/Installer/postcss.config.js | 6 + .../src/Console/Commands/Installer.php | 507 +++ .../Webkul/Installer/src/Data/countries.json | 1277 ++++++ .../Webkul/Installer/src/Data/states.json | 3978 +++++++++++++++++ ...rehouse_attributes_in_attributes_table.php | 117 + ...es_owner_attribute_in_attributes_table.php | 40 + ...ob_title_attribute_in_attributes_table.php | 40 + ...es_owner_attribute_in_attributes_table.php | 40 + ...dd_leads_stage_and_pipeline_attributes.php | 57 + .../Seeders/Attribute/AttributeSeeder.php | 645 +++ .../Seeders/Attribute/DatabaseSeeder.php | 19 + .../Database/Seeders/Core/CountriesSeeder.php | 24 + .../Database/Seeders/Core/DatabaseSeeder.php | 20 + .../Database/Seeders/Core/StatesSeeder.php | 24 + .../src/Database/Seeders/DatabaseSeeder.php | 30 + .../Seeders/EmailTemplate/DatabaseSeeder.php | 19 + .../EmailTemplate/EmailTemplateSeeder.php | 85 + .../Database/Seeders/Lead/DatabaseSeeder.php | 21 + .../Database/Seeders/Lead/PipelineSeeder.php | 83 + .../Database/Seeders/Lead/SourceSeeder.php | 54 + .../src/Database/Seeders/Lead/TypeSeeder.php | 39 + .../Database/Seeders/User/DatabaseSeeder.php | 20 + .../src/Database/Seeders/User/RoleSeeder.php | 31 + .../src/Database/Seeders/User/UserSeeder.php | 34 + .../Seeders/Workflow/DatabaseSeeder.php | 19 + .../Seeders/Workflow/WorkflowSeeder.php | 51 + .../Installer/src/Events/ComposerEvents.php | 60 + .../Installer/src/Helpers/DatabaseManager.php | 221 + .../src/Helpers/EnvironmentManager.php | 94 + .../src/Helpers/ServerRequirements.php | 96 + .../src/Http/Controllers/Controller.php | 12 + .../Http/Controllers/ImageCacheController.php | 109 + .../Http/Controllers/InstallerController.php | 181 + .../src/Http/Middleware/CanInstall.php | 79 + .../Installer/src/Http/Middleware/Locale.php | 28 + .../Installer/src/Listeners/Installer.php | 53 + .../Providers/InstallerServiceProvider.php | 62 + .../src/Resources/assets/css/app.css | 182 + .../src/Resources/assets/fonts/icomoon.eot | Bin 0 -> 35796 bytes .../src/Resources/assets/fonts/icomoon.ttf | Bin 0 -> 35632 bytes .../src/Resources/assets/fonts/icomoon.woff | Bin 0 -> 35708 bytes .../src/Resources/assets/images/favicon.ico | Bin 0 -> 1150 bytes .../Resources/assets/images/krayin-logo.svg | 7 + .../src/Resources/assets/images/spinner.svg | 18 + .../Installer/src/Resources/assets/js/app.js | 89 + .../src/Resources/assets/js/plugins/axios.js | 9 + .../Installer/src/Resources/lang/ar/app.php | 300 ++ .../Installer/src/Resources/lang/en/app.php | 300 ++ .../Installer/src/Resources/lang/es/app.php | 300 ++ .../Installer/src/Resources/lang/fa/app.php | 300 ++ .../Installer/src/Resources/lang/ja/app.php | 300 ++ .../Installer/src/Resources/lang/ko/app.php | 300 ++ .../src/Resources/lang/pt_BR/app.php | 300 ++ .../Installer/src/Resources/lang/tr/app.php | 300 ++ .../Installer/src/Resources/lang/vi/app.php | 300 ++ .../src/Resources/lang/zh_CN/app.php | 300 ++ .../views/components/button/index.blade.php | 63 + .../form/control-group/control.blade.php | 70 + .../form/control-group/error.blade.php | 13 + .../form/control-group/index.blade.php | 3 + .../form/control-group/label.blade.php | 3 + .../views/components/form/index.blade.php | 34 + .../Resources/views/installer/index.blade.php | 1246 ++++++ packages/Webkul/Installer/src/Routes/web.php | 23 + .../Installer/src/Templates/on-boarding.php | 16 + packages/Webkul/Installer/tailwind.config.js | 47 + packages/Webkul/Installer/vite.config.js | 46 + packages/Webkul/Lead/composer.json | 35 + packages/Webkul/Lead/src/Contracts/Lead.php | 5 + .../Webkul/Lead/src/Contracts/Pipeline.php | 5 + .../Webkul/Lead/src/Contracts/Product.php | 5 + packages/Webkul/Lead/src/Contracts/Source.php | 5 + packages/Webkul/Lead/src/Contracts/Stage.php | 5 + packages/Webkul/Lead/src/Contracts/Type.php | 5 + ...04_21_172825_create_lead_sources_table.php | 32 + ...1_04_21_172847_create_lead_types_table.php | 32 + ..._04_22_153258_create_lead_stages_table.php | 34 + ..._22_155706_create_lead_pipelines_table.php | 33 + ...5838_create_lead_pipeline_stages_table.php | 38 + .../2021_04_22_164215_create_leads_table.php | 56 + ...4_22_171805_create_lead_products_table.php | 40 + ...12_150329_create_lead_activities_table.php | 34 + ...21_05_20_141240_create_lead_tags_table.php | 34 + ..._07_02_201822_create_lead_quotes_table.php | 34 + ...umn_expected_close_date_in_leads_table.php | 32 + ...mn_rotten_days_in_lead_pipelines_table.php | 32 + ...54222_alter_lead_pipeline_stages_table.php | 57 + .../2021_09_30_161722_alter_leads_table.php | 50 + ...nge_user_id_to_nullable_in_leads_table.php | 32 + ...ine_stage_id_constraint_in_leads_table.php | 36 + ...302_modify_foreign_keys_in_leads_table.php | 80 + ...7_01_133612_alter_lead_pipelines_table.php | 30 + packages/Webkul/Lead/src/Helpers/MagicAI.php | 128 + packages/Webkul/Lead/src/Models/Lead.php | 173 + packages/Webkul/Lead/src/Models/LeadProxy.php | 7 + packages/Webkul/Lead/src/Models/Pipeline.php | 38 + .../Webkul/Lead/src/Models/PipelineProxy.php | 7 + packages/Webkul/Lead/src/Models/Product.php | 61 + .../Webkul/Lead/src/Models/ProductProxy.php | 7 + packages/Webkul/Lead/src/Models/Source.php | 28 + .../Webkul/Lead/src/Models/SourceProxy.php | 7 + packages/Webkul/Lead/src/Models/Stage.php | 42 + .../Webkul/Lead/src/Models/StageProxy.php | 7 + packages/Webkul/Lead/src/Models/Type.php | 28 + packages/Webkul/Lead/src/Models/TypeProxy.php | 7 + .../src/Providers/LeadServiceProvider.php | 26 + .../src/Providers/ModuleServiceProvider.php | 23 + .../Lead/src/Repositories/LeadRepository.php | 267 ++ .../src/Repositories/PipelineRepository.php | 115 + .../src/Repositories/ProductRepository.php | 18 + .../src/Repositories/SourceRepository.php | 18 + .../Lead/src/Repositories/StageRepository.php | 18 + .../Lead/src/Repositories/TypeRepository.php | 18 + .../Lead/src/Services/MagicAIService.php | 238 + .../src/Console/Commands/CampaignCommand.php | 49 + .../Marketing/src/Contracts/Campaign.php | 5 + .../Webkul/Marketing/src/Contracts/Event.php | 5 + ...9_044744_create_marketing_events_table.php | 30 + ...22500_create_marketing_campaigns_table.php | 32 + .../Webkul/Marketing/src/Helpers/Campaign.php | 56 + .../Marketing/src/Mail/CampaignMail.php | 45 + .../Webkul/Marketing/src/Models/Campaign.php | 47 + .../Marketing/src/Models/CampaignProxy.php | 7 + .../Webkul/Marketing/src/Models/Event.php | 32 + .../Marketing/src/Models/EventProxy.php | 7 + .../Providers/MarketingServiceProvider.php | 44 + .../src/Providers/ModuleServiceProvider.php | 20 + .../src/Repositories/CampaignRepository.php | 17 + .../src/Repositories/EventRepository.php | 17 + packages/Webkul/Product/composer.json | 28 + .../Webkul/Product/src/Contracts/Product.php | 5 + .../src/Contracts/ProductInventory.php | 5 + ...021_04_09_065617_create_products_table.php | 36 + ...54009_create_product_inventories_table.php | 39 + ...150329_create_product_activities_table.php | 34 + ...08_10_150340_create_product_tags_table.php | 34 + ...065808_alter_product_inventories_table.php | 26 + .../Webkul/Product/src/Models/Product.php | 72 + .../Product/src/Models/ProductInventory.php | 61 + .../src/Models/ProductInventoryProxy.php | 7 + .../Product/src/Models/ProductProxy.php | 7 + .../src/Providers/ModuleServiceProvider.php | 15 + .../src/Providers/ProductServiceProvider.php | 26 + .../ProductInventoryRepository.php | 18 + .../src/Repositories/ProductRepository.php | 192 + packages/Webkul/Quote/src/Contracts/Quote.php | 5 + .../Webkul/Quote/src/Contracts/QuoteItem.php | 5 + .../2021_07_01_230345_create_quotes_table.php | 52 + ..._07_01_231317_create_quote_items_table.php | 49 + ...ge_description_to_text_in_quotes_table.php | 32 + packages/Webkul/Quote/src/Models/Quote.php | 76 + .../Webkul/Quote/src/Models/QuoteItem.php | 39 + .../Quote/src/Models/QuoteItemProxy.php | 7 + .../Webkul/Quote/src/Models/QuoteProxy.php | 7 + .../src/Providers/ModuleServiceProvider.php | 15 + .../src/Providers/QuoteServiceProvider.php | 26 + .../src/Repositories/QuoteItemRepository.php | 69 + .../src/Repositories/QuoteRepository.php | 177 + packages/Webkul/Tag/composer.json | 28 + packages/Webkul/Tag/src/Contracts/Tag.php | 5 + .../2021_05_20_141230_create_tags_table.php | 37 + packages/Webkul/Tag/src/Models/Tag.php | 31 + packages/Webkul/Tag/src/Models/TagProxy.php | 7 + .../src/Providers/ModuleServiceProvider.php | 13 + .../Tag/src/Providers/TagServiceProvider.php | 26 + .../Tag/src/Repositories/TagRepository.php | 27 + packages/Webkul/User/.gitignore | 1 + packages/Webkul/User/composer.json | 27 + packages/Webkul/User/src/Contracts/Group.php | 5 + packages/Webkul/User/src/Contracts/Role.php | 5 + packages/Webkul/User/src/Contracts/User.php | 5 + .../User/src/Database/Migrations/.gitkeep | 0 .../2021_03_12_074578_create_groups_table.php | 33 + .../2021_03_12_074597_create_roles_table.php | 35 + .../2021_03_12_074857_create_users_table.php | 38 + ..._03_12_074867_create_user_groups_table.php | 34 + ...4957_create_user_password_resets_table.php | 32 + ...d_unique_index_to_name_in_groups_table.php | 32 + ...171510_add_image_column_in_users_table.php | 32 + ...0_add_created_by_column_in_users_table.php | 36 + ...1_add_created_by_column_in_roles_table.php | 36 + packages/Webkul/User/src/Models/Group.php | 27 + .../Webkul/User/src/Models/GroupProxy.php | 7 + packages/Webkul/User/src/Models/Role.php | 34 + packages/Webkul/User/src/Models/RoleProxy.php | 7 + packages/Webkul/User/src/Models/User.php | 105 + packages/Webkul/User/src/Models/UserProxy.php | 7 + .../src/Providers/ModuleServiceProvider.php | 17 + .../src/Providers/UserServiceProvider.php | 26 + .../User/src/Repositories/GroupRepository.php | 18 + .../User/src/Repositories/RoleRepository.php | 18 + .../User/src/Repositories/UserRepository.php | 46 + .../Warehouse/src/Contracts/Location.php | 5 + .../Warehouse/src/Contracts/Warehouse.php | 5 + ...4_06_21_160707_create_warehouses_table.php | 33 + ...60735_create_warehouse_locations_table.php | 34 + ...0329_create_warehouse_activities_table.php | 34 + ..._10_100340_create_warehouse_tags_table.php | 34 + .../Webkul/Warehouse/src/Models/Location.php | 32 + .../Warehouse/src/Models/LocationProxy.php | 7 + .../Webkul/Warehouse/src/Models/Warehouse.php | 64 + .../Warehouse/src/Models/WarehouseProxy.php | 7 + .../src/Providers/ModuleServiceProvider.php | 15 + .../Providers/WarehouseServiceProvider.php | 26 + .../src/Repositories/LocationRepository.php | 26 + .../src/Repositories/WarehouseRepository.php | 101 + packages/Webkul/WebForm/.gitignore | 3 + packages/Webkul/WebForm/composer.json | 25 + packages/Webkul/WebForm/package.json | 24 + packages/Webkul/WebForm/postcss.config.js | 6 + packages/Webkul/WebForm/src/Config/acl.php | 30 + packages/Webkul/WebForm/src/Config/menu.php | 12 + .../Webkul/WebForm/src/Contracts/WebForm.php | 5 + .../src/Contracts/WebFormAttribute.php | 5 + .../WebForm/src/DataGrids/WebFormDataGrid.php | 84 + ...21_12_14_213049_create_web_forms_table.php | 43 + ...14923_create_web_form_attributes_table.php | 41 + ...dd_lead_pipeline_id_to_web_forms_table.php | 46 + .../src/Http/Controllers/Controller.php | 24 + .../Http/Controllers/WebFormController.php | 170 + .../WebForm/src/Http/Requests/WebForm.php | 127 + .../Webkul/WebForm/src/Models/WebForm.php | 33 + .../WebForm/src/Models/WebFormAttribute.php | 48 + .../src/Models/WebFormAttributeProxy.php | 7 + .../WebForm/src/Models/WebFormProxy.php | 7 + .../src/Providers/ModuleServiceProvider.php | 15 + .../src/Providers/WebFormServiceProvider.php | 45 + .../WebFormAttributeRepository.php | 18 + .../src/Repositories/WebFormRepository.php | 105 + .../WebForm/src/Resources/assets/css/app.css | 128 + .../src/Resources/assets/fonts/icomoon.woff | Bin 0 -> 37396 bytes .../WebForm/src/Resources/assets/js/app.js | 57 + .../src/Resources/assets/js/plugins/axios.js | 14 + .../Resources/assets/js/plugins/emitter.js | 11 + .../Resources/assets/js/plugins/flatpickr.js | 35 + .../assets/js/plugins/vee-validate.js | 285 ++ .../src/Resources/assets/locales/hi_IN.json | 32 + .../src/Resources/assets/locales/sin.json | 32 + .../WebForm/src/Resources/lang/ar/app.php | 20 + .../WebForm/src/Resources/lang/en/app.php | 20 + .../WebForm/src/Resources/lang/es/app.php | 20 + .../WebForm/src/Resources/lang/fa/app.php | 20 + .../WebForm/src/Resources/lang/ja/app.php | 20 + .../WebForm/src/Resources/lang/ko/app.php | 20 + .../WebForm/src/Resources/lang/pt_BR/app.php | 20 + .../WebForm/src/Resources/lang/tr/app.php | 20 + .../WebForm/src/Resources/lang/vi/app.php | 20 + .../WebForm/src/Resources/lang/zh_CN/app.php | 20 + .../views/components/button/index.blade.php | 40 + .../components/flash-group/index.blade.php | 64 + .../components/flash-group/item.blade.php | 205 + .../form/control-group/control.blade.php | 339 ++ .../form/control-group/error.blade.php | 16 + .../form/control-group/index.blade.php | 3 + .../form/control-group/label.blade.php | 3 + .../views/components/form/index.blade.php | 40 + .../views/components/layouts/index.blade.php | 113 + .../views/components/spinner/index.blade.php | 27 + .../settings/web-forms/controls.blade.php | 270 ++ .../views/settings/web-forms/embed.blade.php | 10 + .../settings/web-forms/preview.blade.php | 164 + packages/Webkul/WebForm/src/Routes/routes.php | 16 + .../Webkul/WebForm/src/Rules/PhoneNumber.php | 33 + packages/Webkul/WebForm/tailwind.config.js | 47 + packages/Webkul/WebForm/vite.config.js | 46 + phpunit.xml | 36 + pint.json | 10 + public/.htaccess | 22 + .../build/assets/activities-DpB2XKqX.svg | 23 + public/admin/build/assets/app-CaTJYsX9.css | 1 + public/admin/build/assets/app-oZri8xg_.css | 1 + public/admin/build/assets/ar.es-bitOgb9K.js | 5 + public/admin/build/assets/bg.es-5tp0KQRo.js | 5 + public/admin/build/assets/bn.es-C_qR0y0-.js | 5 + public/admin/build/assets/bs.es-DduooXS8.js | 5 + public/admin/build/assets/ca.es-DYDNDJFJ.js | 5 + public/admin/build/assets/calls-BU_UfDax.svg | 21 + .../build/assets/corner-clip-DsfWoKt7.svg | 11 + public/admin/build/assets/cs.es-CvnAvfoI.js | 5 + public/admin/build/assets/da.es-C_eJXDX3.js | 5 + .../assets/dark-corner-clip-Deux0RQO.svg | 21 + .../admin/build/assets/dark-logo-Dnw3cpYj.svg | 6 + public/admin/build/assets/de.es-Cp-R9E16.js | 5 + .../admin/build/assets/default-DBulZ5O_.svg | 26 + .../build/assets/description-BwifbkWG.svg | 20 + .../build/assets/drag-and-drop.es-CbCm7IHa.js | 5 + public/admin/build/assets/el.es-BZ44a1Tb.js | 5 + public/admin/build/assets/emails-D_qEBj20.svg | 19 + public/admin/build/assets/en.es-DwVNOttH.js | 5 + public/admin/build/assets/error-ClRu8L1K.svg | 29 + public/admin/build/assets/es.es-DO8PaQ_q.js | 5 + public/admin/build/assets/et.es-DdRIcUrz.js | 5 + public/admin/build/assets/fa.es-XN0av8Tx.js | 5 + .../admin/build/assets/favicon-BtbgZBji.ico | Bin 0 -> 1150 bytes public/admin/build/assets/fi.es-B0Iy6aas.js | 5 + public/admin/build/assets/files-Xm-jQ6Fi.svg | 18 + public/admin/build/assets/fr.es-B7WXlprl.js | 5 + public/admin/build/assets/he.es-BWEk5prp.js | 5 + public/admin/build/assets/hr.es-B_iqHQyr.js | 5 + public/admin/build/assets/hu.es-D_t4Ibxf.js | 5 + .../admin/build/assets/icomoon-BbY1GX-d.woff | Bin 0 -> 37964 bytes public/admin/build/assets/id.es-CzHAK-XV.js | 5 + public/admin/build/assets/is.es-DChRpym4.js | 5 + public/admin/build/assets/it.es-CquQA5xx.js | 5 + public/admin/build/assets/ja.es-B19gsG6H.js | 5 + public/admin/build/assets/ka.es-CuOe65jc.js | 5 + public/admin/build/assets/ko.es-CsMl2USK.js | 5 + public/admin/build/assets/logo-Bjh7YAuF.svg | 6 + public/admin/build/assets/lt.es-vqgjGyMx.js | 5 + .../admin/build/assets/lunches-BHeSbGQ3.svg | 19 + .../admin/build/assets/meetings-Cwj897qL.svg | 23 + public/admin/build/assets/mn.es-BmdwFdas.js | 5 + .../assets/mobile-dark-logo-COPgCide.svg | 5 + .../assets/mobile-light-logo-CjoobCkl.svg | 5 + public/admin/build/assets/nl.es-CFxVMCWy.js | 5 + public/admin/build/assets/no.es-qRcuRMwl.js | 5 + public/admin/build/assets/notes-BdIILF0F.svg | 17 + .../build/assets/organizations-aUdNng-O.svg | 32 + .../admin/build/assets/pipedrive-Dk_Mji0J.svg | 25 + public/admin/build/assets/pl.es-BQ3a0ltx.js | 5 + public/admin/build/assets/plans-DIcOmGwv.svg | 29 + .../admin/build/assets/products-zDh6UFtz.svg | 24 + .../admin/build/assets/pt-br.es-U9bVHsgs.js | 5 + .../admin/build/assets/pt-pt.es-U9bVHsgs.js | 5 + public/admin/build/assets/quotes-0H7ZCVr6.svg | 17 + public/admin/build/assets/ro.es-ldjMjt3l.js | 5 + public/admin/build/assets/ru.es-CS-QKsYL.js | 5 + public/admin/build/assets/sk.es-CAe8sEvL.js | 5 + public/admin/build/assets/sl.es-rdzOxu_X.js | 5 + public/admin/build/assets/sq.es-BY5E06HI.js | 5 + public/admin/build/assets/sr.es-QPseRVBy.js | 5 + public/admin/build/assets/sv.es-BfPUYcV1.js | 5 + public/admin/build/assets/tr.es-Cl6kz_IH.js | 5 + public/admin/build/assets/uk.es-DBEtNQnZ.js | 5 + public/admin/build/assets/users-6trD5il1.svg | 19 + public/admin/build/assets/vi.es-Bx2g_1jx.js | 5 + .../admin/build/assets/zh-cn.es-BWMmCH-R.js | 5 + .../admin/build/assets/zh-hk.es-DFZ0bKvi.js | 5 + public/admin/build/manifest.json | 428 ++ public/build/assets/app-DUr89oQr.js | 9 + public/build/assets/app-l0sNRNKZ.js | 1 + public/build/manifest.json | 14 + public/fonts/.gitignore | 12 + public/index.php | 17 + .../installer/build/assets/app-C9-BdERk.css | 1 + .../build/assets/favicon-BtbgZBji.ico | Bin 0 -> 1150 bytes .../build/assets/icomoon-B5aogpno.woff | Bin 0 -> 35708 bytes .../build/assets/icomoon-BFKbaEOo.eot | Bin 0 -> 35796 bytes .../build/assets/icomoon-BgFe9HIr.ttf | Bin 0 -> 35632 bytes .../build/assets/krayin-logo-D5SO44Ys.svg | 7 + .../build/assets/spinner-CWS6ej98.svg | 18 + public/installer/build/manifest.json | 41 + public/robots.txt | 2 + public/web.config | 28 + public/webform/build/assets/app-CWW-WGL4.css | 1 + public/webform/build/assets/app-DsP8OK1c.css | 1 + .../build/assets/icomoon-CO37Y6g-.woff | Bin 0 -> 37396 bytes public/webform/build/manifest.json | 20 + resources/css/app.css | 0 resources/js/app.js | 1 + resources/js/bootstrap.js | 32 + resources/views/.gitignore | 2 + routes/api.php | 19 + routes/breadcrumbs.php | 464 ++ routes/channels.php | 18 + routes/console.php | 22 + routes/web.php | 1 + storage/.gitignore | 1 + storage/app/.gitignore | 3 + storage/app/public/.gitignore | 5 + storage/app/public/data-transfer/.gitignore | 2 + .../public/data-transfer/samples/.gitignore | 4 + .../public/data-transfer/samples/leads.csv | 3 + .../public/data-transfer/samples/persons.csv | 3 + .../public/data-transfer/samples/products.csv | 3 + storage/debugbar/.gitignore | 2 + storage/framework/.gitignore | 10 + storage/framework/cache/.gitignore | 3 + storage/framework/cache/data/.gitignore | 2 + storage/framework/sessions/.gitignore | 2 + storage/framework/testing/.gitignore | 2 + storage/framework/views/.gitignore | 2 + storage/logs/.gitignore | 2 + tests/Feature/AuthenticationTest.php | 28 + tests/Feature/HelpTest.php | 16 + .../Security/AuthenticationHardeningTest.php | 145 + .../Feature/Security/HtmlSanitizationTest.php | 50 + .../Security/InstallerLockdownTest.php | 204 + .../Security/PasswordResetStatusTest.php | 81 + .../Security/SvgUploadSanitizationTest.php | 88 + tests/Pest.php | 92 + tests/TestCase.php | 10 + tests/Unit/BasicTest.php | 7 + vite.config.js | 11 + 1274 files changed, 144880 insertions(+) create mode 120000 .ai/skills create mode 120000 .claude/skills create mode 120000 .codex/skills create mode 120000 .cursor/skills create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/1_Bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/2_Feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/3_Support_question.md create mode 100644 .github/ISSUE_TEMPLATE/4_Security_vulnerabilities.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/skills/README.md create mode 100644 .github/skills/crm-package-development/SKILL.md create mode 100644 .github/skills/pest-testing/SKILL.md create mode 100644 .github/workflows/admin_playwright_tests.yml create mode 100644 .github/workflows/auto_commits.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/skills-consistency.yml create mode 100644 .gitignore create mode 120000 .kilocode/skills create mode 100644 .styleci.yml create mode 100644 AGENTS.md create mode 100755 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 UPGRADE.md create mode 100644 app/Http/Controllers/Controller.php create mode 100644 app/Models/User.php create mode 100644 app/Providers/AppServiceProvider.php create mode 100755 artisan create mode 100755 bin/validate-skills.sh create mode 100755 bootstrap/app.php create mode 100644 bootstrap/cache/.gitignore create mode 100644 bootstrap/providers.php create mode 100644 composer.json create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/breadcrumbs.php create mode 100644 config/broadcasting.php create mode 100644 config/cache.php create mode 100644 config/concord.php create mode 100644 config/cors.php create mode 100644 config/database.php create mode 100644 config/filesystems.php create mode 100644 config/hashing.php create mode 100644 config/imap.php create mode 100755 config/krayin-vite.php create mode 100644 config/logging.php create mode 100644 config/mail-receiver.php create mode 100644 config/mail.php create mode 100644 config/queue.php create mode 100644 config/repository.php create mode 100644 config/sanctum.php create mode 100644 config/services.php create mode 100644 config/session.php create mode 100644 config/tinker.php create mode 100644 config/view.php create mode 100644 database/.gitignore create mode 100644 database/factories/UserFactory.php create mode 100644 database/migrations/2019_08_19_000000_create_failed_jobs_table.php create mode 100644 database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php create mode 100644 database/migrations/2024_09_09_094040_create_job_batches_table.php create mode 100644 database/migrations/2024_09_09_094042_create_jobs_table.php create mode 100644 database/seeders/DatabaseSeeder.php create mode 100644 example.txt create mode 100644 lang/en/auth.php create mode 100644 lang/en/pagination.php create mode 100644 lang/en/passwords.php create mode 100644 lang/en/validation.php create mode 100644 package.json create mode 100755 packages/Webkul/Activity/composer.json create mode 100644 packages/Webkul/Activity/src/Contracts/Activity.php create mode 100644 packages/Webkul/Activity/src/Contracts/File.php create mode 100644 packages/Webkul/Activity/src/Contracts/Participant.php create mode 100644 packages/Webkul/Activity/src/Database/Migrations/2021_05_12_150329_create_activities_table.php create mode 100644 packages/Webkul/Activity/src/Database/Migrations/2021_05_15_151855_create_activity_files_table.php create mode 100644 packages/Webkul/Activity/src/Database/Migrations/2021_07_28_142453_create_activity_participants_table.php create mode 100644 packages/Webkul/Activity/src/Database/Migrations/2021_11_17_190943_add_location_column_in_activities_table.php create mode 100644 packages/Webkul/Activity/src/Database/Migrations/2025_01_17_151632_alter_activities_table.php create mode 100644 packages/Webkul/Activity/src/Models/Activity.php create mode 100644 packages/Webkul/Activity/src/Models/ActivityProxy.php create mode 100644 packages/Webkul/Activity/src/Models/File.php create mode 100644 packages/Webkul/Activity/src/Models/FileProxy.php create mode 100644 packages/Webkul/Activity/src/Models/Participant.php create mode 100644 packages/Webkul/Activity/src/Models/ParticipantProxy.php create mode 100644 packages/Webkul/Activity/src/Providers/ActivityServiceProvider.php create mode 100644 packages/Webkul/Activity/src/Providers/ModuleServiceProvider.php create mode 100755 packages/Webkul/Activity/src/Repositories/ActivityRepository.php create mode 100755 packages/Webkul/Activity/src/Repositories/FileRepository.php create mode 100755 packages/Webkul/Activity/src/Repositories/ParticipantRepository.php create mode 100644 packages/Webkul/Activity/src/Traits/LogsActivity.php create mode 100755 packages/Webkul/Admin/.gitignore create mode 100755 packages/Webkul/Admin/composer.json create mode 100644 packages/Webkul/Admin/example.txt create mode 100644 packages/Webkul/Admin/package.json create mode 100644 packages/Webkul/Admin/postcss.config.cjs create mode 100755 packages/Webkul/Admin/src/Bouncer.php create mode 100644 packages/Webkul/Admin/src/Config/acl.php create mode 100644 packages/Webkul/Admin/src/Config/attribute_entity_types.php create mode 100644 packages/Webkul/Admin/src/Config/attribute_lookups.php create mode 100644 packages/Webkul/Admin/src/Config/core_config.php create mode 100644 packages/Webkul/Admin/src/Config/menu.php create mode 100644 packages/Webkul/Admin/src/Config/support.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Activity/ActivityDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Contact/OrganizationDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Contact/PersonDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Lead/LeadDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Mail/EmailDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Product/ProductDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Quote/QuoteDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/AttributeDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/DataTransfer/ImportDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/EmailTemplateDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/GroupDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/Marketing/CampaignDatagrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/Marketing/EventDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/PipelineDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/RoleDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/SourceDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/TagDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/TypeDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/UserDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/WarehouseDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/WebhookDataGrid.php create mode 100644 packages/Webkul/Admin/src/DataGrids/Settings/WorkflowDataGrid.php create mode 100644 packages/Webkul/Admin/src/Database/Migrations/2021_06_07_162808_add_lead_view_permission_column_in_users_table.php create mode 100644 packages/Webkul/Admin/src/Database/Migrations/2021_10_02_170105_insert_expected_closed_date_column_in_attributes_table.php create mode 100644 packages/Webkul/Admin/src/Database/Migrations/2026_06_10_000000_make_value_nullable_in_core_config_table.php create mode 100755 packages/Webkul/Admin/src/Exceptions/Handler.php create mode 100755 packages/Webkul/Admin/src/Facades/Bouncer.php create mode 100644 packages/Webkul/Admin/src/Helpers/Dashboard.php create mode 100644 packages/Webkul/Admin/src/Helpers/Reporting/AbstractReporting.php create mode 100644 packages/Webkul/Admin/src/Helpers/Reporting/Activity.php create mode 100644 packages/Webkul/Admin/src/Helpers/Reporting/Lead.php create mode 100644 packages/Webkul/Admin/src/Helpers/Reporting/Organization.php create mode 100644 packages/Webkul/Admin/src/Helpers/Reporting/Person.php create mode 100644 packages/Webkul/Admin/src/Helpers/Reporting/Product.php create mode 100644 packages/Webkul/Admin/src/Helpers/Reporting/Quote.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Activity/ActivityController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Configuration/ConfigurationController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Contact/OrganizationController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/ActivityController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/GoogleContactExportController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/PersonController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/TagController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Controller.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/DashboardController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/DataGrid/SavedFilterController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/DataGridController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/HelpController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Lead/ActivityController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Lead/EmailController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Lead/LeadController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Lead/QuoteController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Lead/TagController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Mail/EmailController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Mail/TagController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Products/ActivityController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Products/ProductController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Products/TagController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Quote/QuoteController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/AttributeController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/DataTransfer/ImportController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/EmailTemplateController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/GoogleContactController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/GroupController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/LocationController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/Marketing/CampaignsController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/Marketing/EventController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/PipelineController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/RoleController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/SettingController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/SourceController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/TagController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/TypeController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/UserController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/ActivityController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/TagController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/WarehouseController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/WebFormController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/Settings/WebhookController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/Settings/WorkflowController.php create mode 100644 packages/Webkul/Admin/src/Http/Controllers/TinyMCEController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/User/AccountController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/User/ForgotPasswordController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/User/ResetPasswordController.php create mode 100755 packages/Webkul/Admin/src/Http/Controllers/User/SessionController.php create mode 100755 packages/Webkul/Admin/src/Http/Middleware/Bouncer.php create mode 100644 packages/Webkul/Admin/src/Http/Middleware/Locale.php create mode 100644 packages/Webkul/Admin/src/Http/Middleware/SanitizeUrl.php create mode 100644 packages/Webkul/Admin/src/Http/Requests/AttributeForm.php create mode 100644 packages/Webkul/Admin/src/Http/Requests/ConfigurationForm.php create mode 100644 packages/Webkul/Admin/src/Http/Requests/LeadForm.php create mode 100644 packages/Webkul/Admin/src/Http/Requests/MassDestroyRequest.php create mode 100644 packages/Webkul/Admin/src/Http/Requests/MassUpdateRequest.php create mode 100755 packages/Webkul/Admin/src/Http/Requests/PipelineForm.php create mode 100755 packages/Webkul/Admin/src/Http/Requests/UserForm.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/ActivityFileResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/ActivityParticipantResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/ActivityResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/EmailAttachmentResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/EmailResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/LeadResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/OrganizationResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/PersonResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/PipelineResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/ProductResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/QuoteResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/SourceResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/StageResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/TagResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/TypeResource.php create mode 100644 packages/Webkul/Admin/src/Http/Resources/UserResource.php create mode 100755 packages/Webkul/Admin/src/Http/helpers.php create mode 100644 packages/Webkul/Admin/src/Listeners/Activity.php create mode 100644 packages/Webkul/Admin/src/Listeners/Lead.php create mode 100644 packages/Webkul/Admin/src/Listeners/Person.php create mode 100644 packages/Webkul/Admin/src/Notifications/Common.php create mode 100644 packages/Webkul/Admin/src/Notifications/User/Create.php create mode 100755 packages/Webkul/Admin/src/Notifications/User/UserResetPassword.php create mode 100644 packages/Webkul/Admin/src/Notifications/User/UserUpdatePassword.php create mode 100644 packages/Webkul/Admin/src/Providers/AdminServiceProvider.php create mode 100644 packages/Webkul/Admin/src/Providers/EventServiceProvider.php create mode 100644 packages/Webkul/Admin/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Admin/src/Requests/WebhookRequest.php create mode 100644 packages/Webkul/Admin/src/Resources/assets/css/app.css create mode 100644 packages/Webkul/Admin/src/Resources/assets/fonts/icomoon.woff create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/corner-clip.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/dark-corner-clip.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/dark-logo.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/activities.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/calls.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/default.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/description.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/emails.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/files.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/lunches.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/meetings.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/notes.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/organizations.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/pipedrive.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/plans.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/products.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/quotes.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/users.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/error.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/favicon.ico create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/logo.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/mobile-dark-logo.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/images/mobile-light-logo.svg create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/app.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/directives/debounce.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/directives/dompurify.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/directives/tooltip.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/plugins/admin.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/plugins/axios.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/plugins/createElement.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/plugins/draggable.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/plugins/emitter.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/plugins/flatpickr.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/plugins/vee-validate.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/js/plugins/vue-cal.js create mode 100644 packages/Webkul/Admin/src/Resources/assets/locales/hi_IN.json create mode 100644 packages/Webkul/Admin/src/Resources/assets/locales/sin.json create mode 100644 packages/Webkul/Admin/src/Resources/lang/en/app.php create mode 100644 packages/Webkul/Admin/src/Resources/lang/es/app.php create mode 100644 packages/Webkul/Admin/src/Resources/lang/ja/app.php create mode 100644 packages/Webkul/Admin/src/Resources/lang/ko/app.php create mode 100644 packages/Webkul/Admin/src/Resources/lang/pt_BR/app.php create mode 100644 packages/Webkul/Admin/src/Resources/lang/tr/app.php create mode 100644 packages/Webkul/Admin/src/Resources/lang/vi/app.php create mode 100644 packages/Webkul/Admin/src/Resources/lang/zh_CN/app.php create mode 100644 packages/Webkul/Admin/src/Resources/views/activities/datagrid/is-done.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/activities/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/activities/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/accordion/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/activities/actions/activity.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/activities/actions/activity/participants.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/activities/actions/file.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/activities/actions/mail.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/activities/actions/note.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/activities/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attachments/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/address.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/boolean.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/checkbox.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/date.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/datetime.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/email.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/file.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/image.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/lookup.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/multiselect.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/phone.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/price.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/select.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/text.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/edit/textarea.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/view.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/view/address.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/view/boolean.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/view/checkbox.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/view/date.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/view/datetime.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/view/email.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/view/file.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/view/image.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/view/lookup.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/view/multiselect.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/attributes/view/phone.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/view/price.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/view/select.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/view/text.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/attributes/view/textarea.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/avatar/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/breadcrumbs/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/button/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/charts/bar.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/charts/doughnut.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/charts/line.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/column-settings.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/export/index.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/datagrid/export/temp.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/table.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/filter.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/mass-action.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/pagination.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/search.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/drawer/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/dropdown/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/dropdown/menu/item.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/example.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/flash-group/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/flash-group/item.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/flat-picker/date.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/flat-picker/datetime.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/control.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/address.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/boolean.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/date.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/datetime.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/email.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/file.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/image.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/lookup.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/multiselect.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/phone.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/select.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/text.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/tags.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/error.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/control-group/label.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/form/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/layouts/anonymous.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/layouts/header/desktop/mega-search.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/layouts/header/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/layouts/header/mobile/mega-search.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/layouts/header/quick-creation.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/layouts/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/layouts/sidebar/desktop/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/layouts/sidebar/mobile/index.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/components/layouts/tabs.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/lookup/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/media/images.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/media/videos.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/modal/confirm.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/modal/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/accordion/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/activities/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/charts/bar.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/common/address.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/open-leads-by-states.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/over-all.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue-by-sources.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue-by-types.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/top-persons.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/top-selling-products.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/total-leads.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/table/body.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/table/head.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/filter.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/pagination.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/search.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/configurations.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/leads.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/persons.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/products.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/quotes.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/settings.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/image/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/datagrid.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/index/kanban.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/index/kanban/toolbar.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/view/mail/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/view/stages.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/table/body.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/table/head.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/activities/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/stages.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/tags.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/pipelines/kanban.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/quotes/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/attributes.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/body.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/head.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/tabs/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/tags/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/tinymce/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/shimmer/tree/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/spinner/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/table/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/table/tbody/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/table/tbody/tr.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/table/td.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/table/th.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/table/thead/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/table/thead/tr.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/tabs/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/tabs/item.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/tags/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/tinymce/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/tree/checkbox.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/tree/radio.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/components/tree/view.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/configuration/edit.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/configuration/field-type.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/configuration/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/organizations/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/organizations/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/organizations/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/persons/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/persons/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/persons/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/persons/view.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/persons/view/attributes.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/contacts/persons/view/organization.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index/open-leads-by-states.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index/over-all.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue-by-sources.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue-by-types.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index/top-persons.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index/top-selling-products.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/index/total-leads.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/dashboard/pdf.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/emails/common/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/emails/layout.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/emails/users/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/emails/users/forget-password.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/errors/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/help/card.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/help/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/common/contact.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/common/products.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index/kanban.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index/kanban/card-settings.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index/kanban/filter.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index/kanban/search.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index/kanban/toolbar.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index/table.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index/upload.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/index/view-switcher.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/view.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/view/attributes.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/view/person.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/view/products.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/view/quotes.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/leads/view/stages.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/mail/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/mail/view.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/partials/breadcrumbs.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/products/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/products/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/products/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/products/view.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/products/view/attributes.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/products/view/inventory.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/quotes/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/quotes/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/quotes/index.blade.php create mode 100755 packages/Webkul/Admin/src/Resources/views/quotes/pdf.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/sessions/forgot-password.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/sessions/login.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/sessions/reset-password.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/attributes/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/attributes/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/attributes/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/import.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/email-templates/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/email-templates/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/email-templates/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/google-contacts/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/groups/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/marketing/campaigns/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/marketing/events/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/pipelines/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/pipelines/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/pipelines/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/roles/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/roles/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/roles/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/sources/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/tags/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/types/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/users/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/warehouses/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/warehouses/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/warehouses/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/warehouses/view.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/contact-information.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/general-information.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/locations.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/web-forms/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/web-forms/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/web-forms/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/webhook/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/webhook/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/webhook/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/workflows/create.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/workflows/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/settings/workflows/index.blade.php create mode 100644 packages/Webkul/Admin/src/Resources/views/user/account/edit.blade.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/activities-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/auth-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/configuration-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/contacts-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/help-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/leads-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/mail-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/products-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/quote-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/rest-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/settings-routes.php create mode 100644 packages/Webkul/Admin/src/Routes/Admin/web.php create mode 100644 packages/Webkul/Admin/src/Routes/Front/web.php create mode 100644 packages/Webkul/Admin/src/Traits/HandlesActivityTimeline.php create mode 100644 packages/Webkul/Admin/src/Traits/ProvideDropdownOptions.php create mode 100644 packages/Webkul/Admin/tailwind.config.js create mode 100644 packages/Webkul/Admin/tests/e2e-pw/.gitignore create mode 100644 packages/Webkul/Admin/tests/e2e-pw/.state/admin-auth.json create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/leads.csv create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/leads_updated.csv create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/persons.csv create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/persons_updated.csv create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/products.csv create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/products_updated.csv create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/10.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/11.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/12.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/3.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/4.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/6.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/7.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/8.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/data/images/9.webp create mode 100644 packages/Webkul/Admin/tests/e2e-pw/fixtures/AdminFixtures.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/locator/CoreLocators.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/AclPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/AdminPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/ConfigurationPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/LeadPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/MailPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/OrganizationPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/PersonsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/ProductPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/QuickAddPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/QuotesPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/SettingsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/AttributesPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/AutomationPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/CampaignsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/DataTransferPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/EmailTemplatesPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/EventsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/GroupsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/OtherSettingsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/PipelinesPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/RolesPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/SourcesPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/TagsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/UsersPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/WarehousesPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/WebFormsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/WebhooksPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/pages/settings/WorkflowsPage.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/playwright.config.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/admin.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/lang/lang.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/lead.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/mail.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/organization.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/persons.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/product.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/quick-add.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/quotes.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/security/acl-permission.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/security/auth-security.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/security/privilege-escalation.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/security/view-permission.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/attritube.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/campaign.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/data-transfer.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/email-tamplet.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/events.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/group.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/roles.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/source.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/tag.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/warehouse.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/tests/settings/webhook.spec.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/acl.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/address.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/customer.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/faker.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/form.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/gdpr.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/lang.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/savedata.ts create mode 100644 packages/Webkul/Admin/tests/e2e-pw/utils/tinymce.ts create mode 100644 packages/Webkul/Admin/vite.config.js create mode 100755 packages/Webkul/Attribute/composer.json create mode 100644 packages/Webkul/Attribute/src/Config/attribute_lookups.php create mode 100644 packages/Webkul/Attribute/src/Contracts/Attribute.php create mode 100644 packages/Webkul/Attribute/src/Contracts/AttributeOption.php create mode 100644 packages/Webkul/Attribute/src/Contracts/AttributeValue.php create mode 100644 packages/Webkul/Attribute/src/Database/Migrations/2021_04_02_080709_create_attributes_table.php create mode 100644 packages/Webkul/Attribute/src/Database/Migrations/2021_04_02_080837_create_attribute_options_table.php create mode 100644 packages/Webkul/Attribute/src/Database/Migrations/2021_04_06_122751_create_attribute_values_table.php create mode 100644 packages/Webkul/Attribute/src/Database/Migrations/2025_07_02_191710_alter_attribute_values_table.php create mode 100644 packages/Webkul/Attribute/src/Models/Attribute.php create mode 100644 packages/Webkul/Attribute/src/Models/AttributeOption.php create mode 100644 packages/Webkul/Attribute/src/Models/AttributeOptionProxy.php create mode 100644 packages/Webkul/Attribute/src/Models/AttributeProxy.php create mode 100755 packages/Webkul/Attribute/src/Models/AttributeValue.php create mode 100644 packages/Webkul/Attribute/src/Models/AttributeValueProxy.php create mode 100644 packages/Webkul/Attribute/src/Providers/AttributeServiceProvider.php create mode 100644 packages/Webkul/Attribute/src/Providers/ModuleServiceProvider.php create mode 100755 packages/Webkul/Attribute/src/Repositories/AttributeOptionRepository.php create mode 100755 packages/Webkul/Attribute/src/Repositories/AttributeRepository.php create mode 100755 packages/Webkul/Attribute/src/Repositories/AttributeValueRepository.php create mode 100644 packages/Webkul/Attribute/src/Traits/CustomAttribute.php create mode 100644 packages/Webkul/Automation/src/Config/workflows.php create mode 100644 packages/Webkul/Automation/src/Contracts/Webhook.php create mode 100644 packages/Webkul/Automation/src/Contracts/Workflow.php create mode 100644 packages/Webkul/Automation/src/Database/Migrations/2021_08_26_133538_create_workflows_table.php create mode 100644 packages/Webkul/Automation/src/Database/Migrations/2024_07_24_150821_create_webhooks_table.php create mode 100644 packages/Webkul/Automation/src/Helpers/Entity.php create mode 100644 packages/Webkul/Automation/src/Helpers/Entity/AbstractEntity.php create mode 100644 packages/Webkul/Automation/src/Helpers/Entity/Activity.php create mode 100644 packages/Webkul/Automation/src/Helpers/Entity/Lead.php create mode 100644 packages/Webkul/Automation/src/Helpers/Entity/Person.php create mode 100644 packages/Webkul/Automation/src/Helpers/Entity/Quote.php create mode 100644 packages/Webkul/Automation/src/Helpers/Validator.php create mode 100644 packages/Webkul/Automation/src/Listeners/Entity.php create mode 100644 packages/Webkul/Automation/src/Models/Webhook.php create mode 100644 packages/Webkul/Automation/src/Models/WebhookProxy.php create mode 100644 packages/Webkul/Automation/src/Models/Workflow.php create mode 100644 packages/Webkul/Automation/src/Models/WorkflowProxy.php create mode 100644 packages/Webkul/Automation/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Automation/src/Providers/WorkflowServiceProvider.php create mode 100644 packages/Webkul/Automation/src/Repositories/WebhookRepository.php create mode 100644 packages/Webkul/Automation/src/Repositories/WorkflowRepository.php create mode 100644 packages/Webkul/Automation/src/Services/WebhookService.php create mode 100755 packages/Webkul/Contact/composer.json create mode 100644 packages/Webkul/Contact/src/Contracts/Organization.php create mode 100644 packages/Webkul/Contact/src/Contracts/Person.php create mode 100644 packages/Webkul/Contact/src/Database/Factories/PersonFactory.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2021_04_09_051326_create_organizations_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2021_04_09_065617_create_persons_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2021_09_22_194103_add_unique_index_to_name_in_organizations_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2024_07_31_092951_add_job_title_in_persons_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2024_08_06_145943_create_person_tags_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2024_08_06_161212_create_person_activities_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2024_08_14_102116_add_user_id_column_in_persons_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2024_08_14_102136_add_user_id_column_in_organizations_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2024_09_09_112201_add_unique_id_to_person_table.php create mode 100644 packages/Webkul/Contact/src/Database/Migrations/2025_03_19_132236_update_organization_id_column_in_persons_table.php create mode 100644 packages/Webkul/Contact/src/Models/Organization.php create mode 100644 packages/Webkul/Contact/src/Models/OrganizationProxy.php create mode 100644 packages/Webkul/Contact/src/Models/Person.php create mode 100644 packages/Webkul/Contact/src/Models/PersonProxy.php create mode 100644 packages/Webkul/Contact/src/Providers/ContactServiceProvider.php create mode 100644 packages/Webkul/Contact/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Contact/src/Repositories/OrganizationRepository.php create mode 100644 packages/Webkul/Contact/src/Repositories/PersonRepository.php create mode 100755 packages/Webkul/Core/composer.json create mode 100644 packages/Webkul/Core/src/Acl.php create mode 100644 packages/Webkul/Core/src/Acl/AclItem.php create mode 100644 packages/Webkul/Core/src/Config/concord.php create mode 100644 packages/Webkul/Core/src/Config/cors.php create mode 100644 packages/Webkul/Core/src/Config/sanctum.php create mode 100644 packages/Webkul/Core/src/Console/Commands/Version.php create mode 100644 packages/Webkul/Core/src/Contracts/CoreConfig.php create mode 100644 packages/Webkul/Core/src/Contracts/Country.php create mode 100644 packages/Webkul/Core/src/Contracts/CountryState.php create mode 100755 packages/Webkul/Core/src/Contracts/Validations/Code.php create mode 100755 packages/Webkul/Core/src/Contracts/Validations/Decimal.php create mode 100644 packages/Webkul/Core/src/Core.php create mode 100755 packages/Webkul/Core/src/Database/Migrations/2021_03_12_060658_create_core_config_table.php create mode 100644 packages/Webkul/Core/src/Database/Migrations/2021_04_12_173232_create_countries_table.php create mode 100644 packages/Webkul/Core/src/Database/Migrations/2021_04_12_173344_create_country_states_table.php create mode 100644 packages/Webkul/Core/src/Database/Migrations/2025_01_29_133500_update_text_column_type_in_core_config_table.php create mode 100755 packages/Webkul/Core/src/Eloquent/Repository.php create mode 100755 packages/Webkul/Core/src/Eloquent/TranslatableModel.php create mode 100755 packages/Webkul/Core/src/Exceptions/ViterNotFound.php create mode 100644 packages/Webkul/Core/src/Facades/Acl.php create mode 100755 packages/Webkul/Core/src/Facades/Core.php create mode 100644 packages/Webkul/Core/src/Facades/Menu.php create mode 100644 packages/Webkul/Core/src/Facades/SystemConfig.php create mode 100644 packages/Webkul/Core/src/Helpers/Helper.php create mode 100755 packages/Webkul/Core/src/Http/helpers.php create mode 100644 packages/Webkul/Core/src/Menu.php create mode 100644 packages/Webkul/Core/src/Menu/MenuItem.php create mode 100755 packages/Webkul/Core/src/Models/CoreConfig.php create mode 100644 packages/Webkul/Core/src/Models/CoreConfigProxy.php create mode 100755 packages/Webkul/Core/src/Models/Country.php create mode 100644 packages/Webkul/Core/src/Models/CountryProxy.php create mode 100755 packages/Webkul/Core/src/Models/CountryState.php create mode 100644 packages/Webkul/Core/src/Models/CountryStateProxy.php create mode 100644 packages/Webkul/Core/src/Providers/BaseModuleServiceProvider.php create mode 100644 packages/Webkul/Core/src/Providers/CoreServiceProvider.php create mode 100644 packages/Webkul/Core/src/Providers/ModuleServiceProvider.php create mode 100755 packages/Webkul/Core/src/Repositories/CoreConfigRepository.php create mode 100755 packages/Webkul/Core/src/Repositories/CountryRepository.php create mode 100755 packages/Webkul/Core/src/Repositories/CountryStateRepository.php create mode 100644 packages/Webkul/Core/src/SystemConfig.php create mode 100644 packages/Webkul/Core/src/SystemConfig/Item.php create mode 100644 packages/Webkul/Core/src/SystemConfig/ItemField.php create mode 100644 packages/Webkul/Core/src/Traits/PDFHandler.php create mode 100644 packages/Webkul/Core/src/Traits/Sanitizer.php create mode 100755 packages/Webkul/Core/src/ViewRenderEventManager.php create mode 100644 packages/Webkul/Core/src/Vite.php create mode 100644 packages/Webkul/DataGrid/src/Action.php create mode 100644 packages/Webkul/DataGrid/src/Column.php create mode 100644 packages/Webkul/DataGrid/src/ColumnTypes/Aggregate.php create mode 100644 packages/Webkul/DataGrid/src/ColumnTypes/Boolean.php create mode 100644 packages/Webkul/DataGrid/src/ColumnTypes/Date.php create mode 100644 packages/Webkul/DataGrid/src/ColumnTypes/Datetime.php create mode 100644 packages/Webkul/DataGrid/src/ColumnTypes/Decimal.php create mode 100644 packages/Webkul/DataGrid/src/ColumnTypes/Integer.php create mode 100644 packages/Webkul/DataGrid/src/ColumnTypes/Text.php create mode 100644 packages/Webkul/DataGrid/src/Contracts/SavedFilter.php create mode 100644 packages/Webkul/DataGrid/src/DataGrid.php create mode 100644 packages/Webkul/DataGrid/src/Database/Migrations/2024_05_10_152848_create_saved_filters_table.php create mode 100644 packages/Webkul/DataGrid/src/Enums/ColumnTypeEnum.php create mode 100644 packages/Webkul/DataGrid/src/Enums/DateRangeOptionEnum.php create mode 100644 packages/Webkul/DataGrid/src/Enums/FilterTypeEnum.php create mode 100644 packages/Webkul/DataGrid/src/Exceptions/InvalidColumnException.php create mode 100644 packages/Webkul/DataGrid/src/Exceptions/InvalidColumnTypeException.php create mode 100644 packages/Webkul/DataGrid/src/Exceptions/InvalidDataGridException.php create mode 100644 packages/Webkul/DataGrid/src/Exports/DataGridExport.php create mode 100644 packages/Webkul/DataGrid/src/Http/helpers.php create mode 100644 packages/Webkul/DataGrid/src/MassAction.php create mode 100755 packages/Webkul/DataGrid/src/Models/SavedFilter.php create mode 100644 packages/Webkul/DataGrid/src/Models/SavedFilterProxy.php create mode 100644 packages/Webkul/DataGrid/src/Providers/DataGridServiceProvider.php create mode 100644 packages/Webkul/DataGrid/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/DataGrid/src/Repositories/SavedFilterRepository.php create mode 100644 packages/Webkul/DataTransfer/src/Config/importers.php create mode 100644 packages/Webkul/DataTransfer/src/Contracts/Import.php create mode 100644 packages/Webkul/DataTransfer/src/Contracts/ImportBatch.php create mode 100644 packages/Webkul/DataTransfer/src/Database/Migrations/2024_01_11_154640_create_imports_table.php create mode 100644 packages/Webkul/DataTransfer/src/Database/Migrations/2024_01_11_154741_create_import_batches_table.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Error.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Import.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Importers/AbstractImporter.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Importers/Leads/Importer.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Importers/Leads/Storage.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Importers/Persons/Importer.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Importers/Persons/Storage.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Importers/Products/Importer.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Importers/Products/SKUStorage.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Sources/AbstractSource.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Sources/CSV.php create mode 100644 packages/Webkul/DataTransfer/src/Helpers/Sources/Excel.php create mode 100644 packages/Webkul/DataTransfer/src/Jobs/Import/Completed.php create mode 100644 packages/Webkul/DataTransfer/src/Jobs/Import/ImportBatch.php create mode 100644 packages/Webkul/DataTransfer/src/Jobs/Import/IndexBatch.php create mode 100644 packages/Webkul/DataTransfer/src/Jobs/Import/Indexing.php create mode 100644 packages/Webkul/DataTransfer/src/Jobs/Import/LinkBatch.php create mode 100644 packages/Webkul/DataTransfer/src/Jobs/Import/Linking.php create mode 100755 packages/Webkul/DataTransfer/src/Models/Import.php create mode 100755 packages/Webkul/DataTransfer/src/Models/ImportBatch.php create mode 100644 packages/Webkul/DataTransfer/src/Models/ImportBatchProxy.php create mode 100644 packages/Webkul/DataTransfer/src/Models/ImportProxy.php create mode 100644 packages/Webkul/DataTransfer/src/Providers/DataTransferServiceProvider.php create mode 100644 packages/Webkul/DataTransfer/src/Providers/ModuleServiceProvider.php create mode 100755 packages/Webkul/DataTransfer/src/Repositories/ImportBatchRepository.php create mode 100755 packages/Webkul/DataTransfer/src/Repositories/ImportRepository.php create mode 100755 packages/Webkul/Email/composer.json create mode 100644 packages/Webkul/Email/src/Console/Commands/ProcessInboundEmails.php create mode 100644 packages/Webkul/Email/src/Contracts/Attachment.php create mode 100644 packages/Webkul/Email/src/Contracts/Email.php create mode 100644 packages/Webkul/Email/src/Database/Migrations/2021_05_24_075618_create_emails_table.php create mode 100644 packages/Webkul/Email/src/Database/Migrations/2021_05_25_072700_create_email_attachments_table.php create mode 100644 packages/Webkul/Email/src/Database/Migrations/2024_08_27_091619_create_email_tags_table.php create mode 100644 packages/Webkul/Email/src/Enums/SupportedFolderEnum.php create mode 100644 packages/Webkul/Email/src/Helpers/Attachment.php create mode 100644 packages/Webkul/Email/src/Helpers/Charset.php create mode 100644 packages/Webkul/Email/src/Helpers/Contracts/CharsetManager.php create mode 100644 packages/Webkul/Email/src/Helpers/HtmlFilter.php create mode 100644 packages/Webkul/Email/src/Helpers/Parser.php create mode 100644 packages/Webkul/Email/src/InboundEmailProcessor/Contracts/InboundEmailProcessor.php create mode 100644 packages/Webkul/Email/src/InboundEmailProcessor/SendgridEmailProcessor.php create mode 100644 packages/Webkul/Email/src/InboundEmailProcessor/WebklexImapEmailProcessor.php create mode 100644 packages/Webkul/Email/src/Mails/Email.php create mode 100644 packages/Webkul/Email/src/Models/Attachment.php create mode 100644 packages/Webkul/Email/src/Models/AttachmentProxy.php create mode 100644 packages/Webkul/Email/src/Models/Email.php create mode 100644 packages/Webkul/Email/src/Models/EmailProxy.php create mode 100644 packages/Webkul/Email/src/Providers/EmailServiceProvider.php create mode 100644 packages/Webkul/Email/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Email/src/Repositories/AttachmentRepository.php create mode 100644 packages/Webkul/Email/src/Repositories/EmailRepository.php create mode 100644 packages/Webkul/EmailTemplate/src/Contracts/EmailTemplate.php create mode 100644 packages/Webkul/EmailTemplate/src/Database/Migrations/2021_09_03_172713_create_email_templates_table.php create mode 100644 packages/Webkul/EmailTemplate/src/Database/Migrations/2025_07_09_133553_alter_email_templates_table.php create mode 100644 packages/Webkul/EmailTemplate/src/Models/EmailTemplate.php create mode 100644 packages/Webkul/EmailTemplate/src/Models/EmailTemplateProxy.php create mode 100644 packages/Webkul/EmailTemplate/src/Providers/EmailTemplateServiceProvider.php create mode 100644 packages/Webkul/EmailTemplate/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/EmailTemplate/src/Repositories/EmailTemplateRepository.php create mode 100644 packages/Webkul/GoogleContact/src/Config/acl.php create mode 100644 packages/Webkul/GoogleContact/src/Config/menu.php create mode 100644 packages/Webkul/GoogleContact/src/Contracts/ContactExportBatch.php create mode 100644 packages/Webkul/GoogleContact/src/Contracts/ContactExportBatchItem.php create mode 100644 packages/Webkul/GoogleContact/src/Contracts/GoogleContactAccount.php create mode 100644 packages/Webkul/GoogleContact/src/Database/Migrations/2026_07_30_000001_create_google_contact_accounts_table.php create mode 100644 packages/Webkul/GoogleContact/src/Database/Migrations/2026_07_31_000001_create_contact_export_batches_table.php create mode 100644 packages/Webkul/GoogleContact/src/Database/Migrations/2026_07_31_000002_create_contact_export_batch_items_table.php create mode 100644 packages/Webkul/GoogleContact/src/Jobs/Export/CompleteExportBatch.php create mode 100644 packages/Webkul/GoogleContact/src/Jobs/Export/ExportChunk.php create mode 100644 packages/Webkul/GoogleContact/src/Jobs/Export/PrepareExportBatch.php create mode 100644 packages/Webkul/GoogleContact/src/Models/ContactExportBatch.php create mode 100644 packages/Webkul/GoogleContact/src/Models/ContactExportBatchItem.php create mode 100644 packages/Webkul/GoogleContact/src/Models/ContactExportBatchItemProxy.php create mode 100644 packages/Webkul/GoogleContact/src/Models/ContactExportBatchProxy.php create mode 100644 packages/Webkul/GoogleContact/src/Models/GoogleContactAccount.php create mode 100644 packages/Webkul/GoogleContact/src/Models/GoogleContactAccountProxy.php create mode 100644 packages/Webkul/GoogleContact/src/Providers/GoogleContactServiceProvider.php create mode 100644 packages/Webkul/GoogleContact/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/GoogleContact/src/Repositories/ContactExportBatchItemRepository.php create mode 100644 packages/Webkul/GoogleContact/src/Repositories/ContactExportBatchRepository.php create mode 100644 packages/Webkul/GoogleContact/src/Repositories/GoogleContactAccountRepository.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/ar/app.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/en/app.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/es/app.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/fa/app.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/ja/app.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/ko/app.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/pt_BR/app.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/tr/app.php create mode 100644 packages/Webkul/GoogleContact/src/Resources/lang/vi/app.php create mode 100644 packages/Webkul/GoogleContact/src/Services/GoogleClientFactory.php create mode 100644 packages/Webkul/GoogleContact/src/Services/GoogleContactsService.php create mode 100644 packages/Webkul/GoogleContact/src/Services/TokenRefresher.php create mode 100644 packages/Webkul/Installer/.gitignore create mode 100644 packages/Webkul/Installer/composer.json create mode 100644 packages/Webkul/Installer/package.json create mode 100644 packages/Webkul/Installer/postcss.config.js create mode 100644 packages/Webkul/Installer/src/Console/Commands/Installer.php create mode 100644 packages/Webkul/Installer/src/Data/countries.json create mode 100644 packages/Webkul/Installer/src/Data/states.json create mode 100644 packages/Webkul/Installer/src/Database/Migrations/2024_06_24_174241_insert_warehouse_attributes_in_attributes_table.php create mode 100644 packages/Webkul/Installer/src/Database/Migrations/2024_07_31_093603_add_organization_sales_owner_attribute_in_attributes_table.php create mode 100644 packages/Webkul/Installer/src/Database/Migrations/2024_07_31_093605_add_person_job_title_attribute_in_attributes_table.php create mode 100644 packages/Webkul/Installer/src/Database/Migrations/2024_07_31_093605_add_person_sales_owner_attribute_in_attributes_table.php create mode 100644 packages/Webkul/Installer/src/Database/Migrations/2024_08_21_153011_add_leads_stage_and_pipeline_attributes.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Attribute/AttributeSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Attribute/DatabaseSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Core/CountriesSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Core/DatabaseSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Core/StatesSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/DatabaseSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/EmailTemplate/DatabaseSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/EmailTemplate/EmailTemplateSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Lead/DatabaseSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Lead/PipelineSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Lead/SourceSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Lead/TypeSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/User/DatabaseSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/User/RoleSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/User/UserSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Workflow/DatabaseSeeder.php create mode 100644 packages/Webkul/Installer/src/Database/Seeders/Workflow/WorkflowSeeder.php create mode 100644 packages/Webkul/Installer/src/Events/ComposerEvents.php create mode 100644 packages/Webkul/Installer/src/Helpers/DatabaseManager.php create mode 100644 packages/Webkul/Installer/src/Helpers/EnvironmentManager.php create mode 100644 packages/Webkul/Installer/src/Helpers/ServerRequirements.php create mode 100644 packages/Webkul/Installer/src/Http/Controllers/Controller.php create mode 100644 packages/Webkul/Installer/src/Http/Controllers/ImageCacheController.php create mode 100644 packages/Webkul/Installer/src/Http/Controllers/InstallerController.php create mode 100644 packages/Webkul/Installer/src/Http/Middleware/CanInstall.php create mode 100644 packages/Webkul/Installer/src/Http/Middleware/Locale.php create mode 100644 packages/Webkul/Installer/src/Listeners/Installer.php create mode 100644 packages/Webkul/Installer/src/Providers/InstallerServiceProvider.php create mode 100644 packages/Webkul/Installer/src/Resources/assets/css/app.css create mode 100644 packages/Webkul/Installer/src/Resources/assets/fonts/icomoon.eot create mode 100644 packages/Webkul/Installer/src/Resources/assets/fonts/icomoon.ttf create mode 100644 packages/Webkul/Installer/src/Resources/assets/fonts/icomoon.woff create mode 100644 packages/Webkul/Installer/src/Resources/assets/images/favicon.ico create mode 100644 packages/Webkul/Installer/src/Resources/assets/images/krayin-logo.svg create mode 100644 packages/Webkul/Installer/src/Resources/assets/images/spinner.svg create mode 100644 packages/Webkul/Installer/src/Resources/assets/js/app.js create mode 100644 packages/Webkul/Installer/src/Resources/assets/js/plugins/axios.js create mode 100644 packages/Webkul/Installer/src/Resources/lang/ar/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/en/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/es/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/fa/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/ja/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/ko/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/pt_BR/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/tr/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/vi/app.php create mode 100644 packages/Webkul/Installer/src/Resources/lang/zh_CN/app.php create mode 100644 packages/Webkul/Installer/src/Resources/views/components/button/index.blade.php create mode 100644 packages/Webkul/Installer/src/Resources/views/components/form/control-group/control.blade.php create mode 100644 packages/Webkul/Installer/src/Resources/views/components/form/control-group/error.blade.php create mode 100644 packages/Webkul/Installer/src/Resources/views/components/form/control-group/index.blade.php create mode 100644 packages/Webkul/Installer/src/Resources/views/components/form/control-group/label.blade.php create mode 100644 packages/Webkul/Installer/src/Resources/views/components/form/index.blade.php create mode 100644 packages/Webkul/Installer/src/Resources/views/installer/index.blade.php create mode 100644 packages/Webkul/Installer/src/Routes/web.php create mode 100644 packages/Webkul/Installer/src/Templates/on-boarding.php create mode 100644 packages/Webkul/Installer/tailwind.config.js create mode 100644 packages/Webkul/Installer/vite.config.js create mode 100755 packages/Webkul/Lead/composer.json create mode 100644 packages/Webkul/Lead/src/Contracts/Lead.php create mode 100644 packages/Webkul/Lead/src/Contracts/Pipeline.php create mode 100644 packages/Webkul/Lead/src/Contracts/Product.php create mode 100644 packages/Webkul/Lead/src/Contracts/Source.php create mode 100644 packages/Webkul/Lead/src/Contracts/Stage.php create mode 100644 packages/Webkul/Lead/src/Contracts/Type.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_04_21_172825_create_lead_sources_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_04_21_172847_create_lead_types_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_04_22_153258_create_lead_stages_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_04_22_155706_create_lead_pipelines_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_04_22_155838_create_lead_pipeline_stages_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_04_22_164215_create_leads_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_04_22_171805_create_lead_products_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_05_12_150329_create_lead_activities_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_05_20_141240_create_lead_tags_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_07_02_201822_create_lead_quotes_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_09_23_221138_add_column_expected_close_date_in_leads_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_09_30_135857_add_column_rotten_days_in_lead_pipelines_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_09_30_154222_alter_lead_pipeline_stages_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_09_30_161722_alter_leads_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_09_30_183825_change_user_id_to_nullable_in_leads_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2021_11_11_180804_change_lead_pipeline_stage_id_constraint_in_leads_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2024_11_29_120302_modify_foreign_keys_in_leads_table.php create mode 100644 packages/Webkul/Lead/src/Database/Migrations/2025_07_01_133612_alter_lead_pipelines_table.php create mode 100644 packages/Webkul/Lead/src/Helpers/MagicAI.php create mode 100644 packages/Webkul/Lead/src/Models/Lead.php create mode 100644 packages/Webkul/Lead/src/Models/LeadProxy.php create mode 100644 packages/Webkul/Lead/src/Models/Pipeline.php create mode 100644 packages/Webkul/Lead/src/Models/PipelineProxy.php create mode 100644 packages/Webkul/Lead/src/Models/Product.php create mode 100644 packages/Webkul/Lead/src/Models/ProductProxy.php create mode 100644 packages/Webkul/Lead/src/Models/Source.php create mode 100644 packages/Webkul/Lead/src/Models/SourceProxy.php create mode 100644 packages/Webkul/Lead/src/Models/Stage.php create mode 100644 packages/Webkul/Lead/src/Models/StageProxy.php create mode 100644 packages/Webkul/Lead/src/Models/Type.php create mode 100644 packages/Webkul/Lead/src/Models/TypeProxy.php create mode 100644 packages/Webkul/Lead/src/Providers/LeadServiceProvider.php create mode 100644 packages/Webkul/Lead/src/Providers/ModuleServiceProvider.php create mode 100755 packages/Webkul/Lead/src/Repositories/LeadRepository.php create mode 100755 packages/Webkul/Lead/src/Repositories/PipelineRepository.php create mode 100755 packages/Webkul/Lead/src/Repositories/ProductRepository.php create mode 100755 packages/Webkul/Lead/src/Repositories/SourceRepository.php create mode 100644 packages/Webkul/Lead/src/Repositories/StageRepository.php create mode 100755 packages/Webkul/Lead/src/Repositories/TypeRepository.php create mode 100644 packages/Webkul/Lead/src/Services/MagicAIService.php create mode 100644 packages/Webkul/Marketing/src/Console/Commands/CampaignCommand.php create mode 100644 packages/Webkul/Marketing/src/Contracts/Campaign.php create mode 100644 packages/Webkul/Marketing/src/Contracts/Event.php create mode 100644 packages/Webkul/Marketing/src/Database/Migrations/2024_10_29_044744_create_marketing_events_table.php create mode 100644 packages/Webkul/Marketing/src/Database/Migrations/2024_11_04_122500_create_marketing_campaigns_table.php create mode 100644 packages/Webkul/Marketing/src/Helpers/Campaign.php create mode 100644 packages/Webkul/Marketing/src/Mail/CampaignMail.php create mode 100644 packages/Webkul/Marketing/src/Models/Campaign.php create mode 100644 packages/Webkul/Marketing/src/Models/CampaignProxy.php create mode 100644 packages/Webkul/Marketing/src/Models/Event.php create mode 100644 packages/Webkul/Marketing/src/Models/EventProxy.php create mode 100644 packages/Webkul/Marketing/src/Providers/MarketingServiceProvider.php create mode 100644 packages/Webkul/Marketing/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Marketing/src/Repositories/CampaignRepository.php create mode 100644 packages/Webkul/Marketing/src/Repositories/EventRepository.php create mode 100755 packages/Webkul/Product/composer.json create mode 100644 packages/Webkul/Product/src/Contracts/Product.php create mode 100644 packages/Webkul/Product/src/Contracts/ProductInventory.php create mode 100644 packages/Webkul/Product/src/Database/Migrations/2021_04_09_065617_create_products_table.php create mode 100644 packages/Webkul/Product/src/Database/Migrations/2024_06_28_154009_create_product_inventories_table.php create mode 100644 packages/Webkul/Product/src/Database/Migrations/2024_08_10_150329_create_product_activities_table.php create mode 100644 packages/Webkul/Product/src/Database/Migrations/2024_08_10_150340_create_product_tags_table.php create mode 100644 packages/Webkul/Product/src/Database/Migrations/2024_09_06_065808_alter_product_inventories_table.php create mode 100644 packages/Webkul/Product/src/Models/Product.php create mode 100755 packages/Webkul/Product/src/Models/ProductInventory.php create mode 100644 packages/Webkul/Product/src/Models/ProductInventoryProxy.php create mode 100644 packages/Webkul/Product/src/Models/ProductProxy.php create mode 100644 packages/Webkul/Product/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Product/src/Providers/ProductServiceProvider.php create mode 100644 packages/Webkul/Product/src/Repositories/ProductInventoryRepository.php create mode 100644 packages/Webkul/Product/src/Repositories/ProductRepository.php create mode 100644 packages/Webkul/Quote/src/Contracts/Quote.php create mode 100644 packages/Webkul/Quote/src/Contracts/QuoteItem.php create mode 100644 packages/Webkul/Quote/src/Database/Migrations/2021_07_01_230345_create_quotes_table.php create mode 100644 packages/Webkul/Quote/src/Database/Migrations/2021_07_01_231317_create_quote_items_table.php create mode 100644 packages/Webkul/Quote/src/Database/Migrations/2026_05_29_000000_change_description_to_text_in_quotes_table.php create mode 100644 packages/Webkul/Quote/src/Models/Quote.php create mode 100644 packages/Webkul/Quote/src/Models/QuoteItem.php create mode 100644 packages/Webkul/Quote/src/Models/QuoteItemProxy.php create mode 100644 packages/Webkul/Quote/src/Models/QuoteProxy.php create mode 100644 packages/Webkul/Quote/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Quote/src/Providers/QuoteServiceProvider.php create mode 100644 packages/Webkul/Quote/src/Repositories/QuoteItemRepository.php create mode 100644 packages/Webkul/Quote/src/Repositories/QuoteRepository.php create mode 100755 packages/Webkul/Tag/composer.json create mode 100644 packages/Webkul/Tag/src/Contracts/Tag.php create mode 100644 packages/Webkul/Tag/src/Database/Migrations/2021_05_20_141230_create_tags_table.php create mode 100644 packages/Webkul/Tag/src/Models/Tag.php create mode 100644 packages/Webkul/Tag/src/Models/TagProxy.php create mode 100644 packages/Webkul/Tag/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Tag/src/Providers/TagServiceProvider.php create mode 100644 packages/Webkul/Tag/src/Repositories/TagRepository.php create mode 100755 packages/Webkul/User/.gitignore create mode 100755 packages/Webkul/User/composer.json create mode 100644 packages/Webkul/User/src/Contracts/Group.php create mode 100644 packages/Webkul/User/src/Contracts/Role.php create mode 100644 packages/Webkul/User/src/Contracts/User.php create mode 100755 packages/Webkul/User/src/Database/Migrations/.gitkeep create mode 100755 packages/Webkul/User/src/Database/Migrations/2021_03_12_074578_create_groups_table.php create mode 100755 packages/Webkul/User/src/Database/Migrations/2021_03_12_074597_create_roles_table.php create mode 100644 packages/Webkul/User/src/Database/Migrations/2021_03_12_074857_create_users_table.php create mode 100644 packages/Webkul/User/src/Database/Migrations/2021_03_12_074867_create_user_groups_table.php create mode 100755 packages/Webkul/User/src/Database/Migrations/2021_03_12_074957_create_user_password_resets_table.php create mode 100644 packages/Webkul/User/src/Database/Migrations/2021_09_22_194622_add_unique_index_to_name_in_groups_table.php create mode 100644 packages/Webkul/User/src/Database/Migrations/2021_11_12_171510_add_image_column_in_users_table.php create mode 100644 packages/Webkul/User/src/Database/Migrations/2026_08_06_000000_add_created_by_column_in_users_table.php create mode 100644 packages/Webkul/User/src/Database/Migrations/2026_08_06_000001_add_created_by_column_in_roles_table.php create mode 100755 packages/Webkul/User/src/Models/Group.php create mode 100644 packages/Webkul/User/src/Models/GroupProxy.php create mode 100755 packages/Webkul/User/src/Models/Role.php create mode 100644 packages/Webkul/User/src/Models/RoleProxy.php create mode 100755 packages/Webkul/User/src/Models/User.php create mode 100644 packages/Webkul/User/src/Models/UserProxy.php create mode 100644 packages/Webkul/User/src/Providers/ModuleServiceProvider.php create mode 100755 packages/Webkul/User/src/Providers/UserServiceProvider.php create mode 100755 packages/Webkul/User/src/Repositories/GroupRepository.php create mode 100755 packages/Webkul/User/src/Repositories/RoleRepository.php create mode 100755 packages/Webkul/User/src/Repositories/UserRepository.php create mode 100644 packages/Webkul/Warehouse/src/Contracts/Location.php create mode 100644 packages/Webkul/Warehouse/src/Contracts/Warehouse.php create mode 100644 packages/Webkul/Warehouse/src/Database/Migrations/2024_06_21_160707_create_warehouses_table.php create mode 100644 packages/Webkul/Warehouse/src/Database/Migrations/2024_06_21_160735_create_warehouse_locations_table.php create mode 100644 packages/Webkul/Warehouse/src/Database/Migrations/2024_08_10_100329_create_warehouse_activities_table.php create mode 100644 packages/Webkul/Warehouse/src/Database/Migrations/2024_08_10_100340_create_warehouse_tags_table.php create mode 100755 packages/Webkul/Warehouse/src/Models/Location.php create mode 100644 packages/Webkul/Warehouse/src/Models/LocationProxy.php create mode 100755 packages/Webkul/Warehouse/src/Models/Warehouse.php create mode 100644 packages/Webkul/Warehouse/src/Models/WarehouseProxy.php create mode 100644 packages/Webkul/Warehouse/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/Warehouse/src/Providers/WarehouseServiceProvider.php create mode 100755 packages/Webkul/Warehouse/src/Repositories/LocationRepository.php create mode 100755 packages/Webkul/Warehouse/src/Repositories/WarehouseRepository.php create mode 100755 packages/Webkul/WebForm/.gitignore create mode 100755 packages/Webkul/WebForm/composer.json create mode 100644 packages/Webkul/WebForm/package.json create mode 100644 packages/Webkul/WebForm/postcss.config.js create mode 100644 packages/Webkul/WebForm/src/Config/acl.php create mode 100644 packages/Webkul/WebForm/src/Config/menu.php create mode 100644 packages/Webkul/WebForm/src/Contracts/WebForm.php create mode 100644 packages/Webkul/WebForm/src/Contracts/WebFormAttribute.php create mode 100644 packages/Webkul/WebForm/src/DataGrids/WebFormDataGrid.php create mode 100644 packages/Webkul/WebForm/src/Database/Migrations/2021_12_14_213049_create_web_forms_table.php create mode 100644 packages/Webkul/WebForm/src/Database/Migrations/2021_12_14_214923_create_web_form_attributes_table.php create mode 100644 packages/Webkul/WebForm/src/Database/Migrations/2026_07_09_000000_add_lead_pipeline_id_to_web_forms_table.php create mode 100755 packages/Webkul/WebForm/src/Http/Controllers/Controller.php create mode 100644 packages/Webkul/WebForm/src/Http/Controllers/WebFormController.php create mode 100644 packages/Webkul/WebForm/src/Http/Requests/WebForm.php create mode 100644 packages/Webkul/WebForm/src/Models/WebForm.php create mode 100644 packages/Webkul/WebForm/src/Models/WebFormAttribute.php create mode 100644 packages/Webkul/WebForm/src/Models/WebFormAttributeProxy.php create mode 100644 packages/Webkul/WebForm/src/Models/WebFormProxy.php create mode 100644 packages/Webkul/WebForm/src/Providers/ModuleServiceProvider.php create mode 100644 packages/Webkul/WebForm/src/Providers/WebFormServiceProvider.php create mode 100644 packages/Webkul/WebForm/src/Repositories/WebFormAttributeRepository.php create mode 100644 packages/Webkul/WebForm/src/Repositories/WebFormRepository.php create mode 100644 packages/Webkul/WebForm/src/Resources/assets/css/app.css create mode 100644 packages/Webkul/WebForm/src/Resources/assets/fonts/icomoon.woff create mode 100644 packages/Webkul/WebForm/src/Resources/assets/js/app.js create mode 100644 packages/Webkul/WebForm/src/Resources/assets/js/plugins/axios.js create mode 100644 packages/Webkul/WebForm/src/Resources/assets/js/plugins/emitter.js create mode 100644 packages/Webkul/WebForm/src/Resources/assets/js/plugins/flatpickr.js create mode 100644 packages/Webkul/WebForm/src/Resources/assets/js/plugins/vee-validate.js create mode 100644 packages/Webkul/WebForm/src/Resources/assets/locales/hi_IN.json create mode 100644 packages/Webkul/WebForm/src/Resources/assets/locales/sin.json create mode 100644 packages/Webkul/WebForm/src/Resources/lang/ar/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/en/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/es/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/fa/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/ja/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/ko/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/pt_BR/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/tr/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/vi/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/lang/zh_CN/app.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/button/index.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/flash-group/index.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/flash-group/item.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/form/control-group/control.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/form/control-group/error.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/form/control-group/index.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/form/control-group/label.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/form/index.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/layouts/index.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/components/spinner/index.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/settings/web-forms/controls.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/settings/web-forms/embed.blade.php create mode 100644 packages/Webkul/WebForm/src/Resources/views/settings/web-forms/preview.blade.php create mode 100644 packages/Webkul/WebForm/src/Routes/routes.php create mode 100644 packages/Webkul/WebForm/src/Rules/PhoneNumber.php create mode 100644 packages/Webkul/WebForm/tailwind.config.js create mode 100644 packages/Webkul/WebForm/vite.config.js create mode 100644 phpunit.xml create mode 100644 pint.json create mode 100644 public/.htaccess create mode 100644 public/admin/build/assets/activities-DpB2XKqX.svg create mode 100644 public/admin/build/assets/app-CaTJYsX9.css create mode 100644 public/admin/build/assets/app-oZri8xg_.css create mode 100644 public/admin/build/assets/ar.es-bitOgb9K.js create mode 100644 public/admin/build/assets/bg.es-5tp0KQRo.js create mode 100644 public/admin/build/assets/bn.es-C_qR0y0-.js create mode 100644 public/admin/build/assets/bs.es-DduooXS8.js create mode 100644 public/admin/build/assets/ca.es-DYDNDJFJ.js create mode 100644 public/admin/build/assets/calls-BU_UfDax.svg create mode 100644 public/admin/build/assets/corner-clip-DsfWoKt7.svg create mode 100644 public/admin/build/assets/cs.es-CvnAvfoI.js create mode 100644 public/admin/build/assets/da.es-C_eJXDX3.js create mode 100644 public/admin/build/assets/dark-corner-clip-Deux0RQO.svg create mode 100644 public/admin/build/assets/dark-logo-Dnw3cpYj.svg create mode 100644 public/admin/build/assets/de.es-Cp-R9E16.js create mode 100644 public/admin/build/assets/default-DBulZ5O_.svg create mode 100644 public/admin/build/assets/description-BwifbkWG.svg create mode 100644 public/admin/build/assets/drag-and-drop.es-CbCm7IHa.js create mode 100644 public/admin/build/assets/el.es-BZ44a1Tb.js create mode 100644 public/admin/build/assets/emails-D_qEBj20.svg create mode 100644 public/admin/build/assets/en.es-DwVNOttH.js create mode 100644 public/admin/build/assets/error-ClRu8L1K.svg create mode 100644 public/admin/build/assets/es.es-DO8PaQ_q.js create mode 100644 public/admin/build/assets/et.es-DdRIcUrz.js create mode 100644 public/admin/build/assets/fa.es-XN0av8Tx.js create mode 100644 public/admin/build/assets/favicon-BtbgZBji.ico create mode 100644 public/admin/build/assets/fi.es-B0Iy6aas.js create mode 100644 public/admin/build/assets/files-Xm-jQ6Fi.svg create mode 100644 public/admin/build/assets/fr.es-B7WXlprl.js create mode 100644 public/admin/build/assets/he.es-BWEk5prp.js create mode 100644 public/admin/build/assets/hr.es-B_iqHQyr.js create mode 100644 public/admin/build/assets/hu.es-D_t4Ibxf.js create mode 100644 public/admin/build/assets/icomoon-BbY1GX-d.woff create mode 100644 public/admin/build/assets/id.es-CzHAK-XV.js create mode 100644 public/admin/build/assets/is.es-DChRpym4.js create mode 100644 public/admin/build/assets/it.es-CquQA5xx.js create mode 100644 public/admin/build/assets/ja.es-B19gsG6H.js create mode 100644 public/admin/build/assets/ka.es-CuOe65jc.js create mode 100644 public/admin/build/assets/ko.es-CsMl2USK.js create mode 100644 public/admin/build/assets/logo-Bjh7YAuF.svg create mode 100644 public/admin/build/assets/lt.es-vqgjGyMx.js create mode 100644 public/admin/build/assets/lunches-BHeSbGQ3.svg create mode 100644 public/admin/build/assets/meetings-Cwj897qL.svg create mode 100644 public/admin/build/assets/mn.es-BmdwFdas.js create mode 100644 public/admin/build/assets/mobile-dark-logo-COPgCide.svg create mode 100644 public/admin/build/assets/mobile-light-logo-CjoobCkl.svg create mode 100644 public/admin/build/assets/nl.es-CFxVMCWy.js create mode 100644 public/admin/build/assets/no.es-qRcuRMwl.js create mode 100644 public/admin/build/assets/notes-BdIILF0F.svg create mode 100644 public/admin/build/assets/organizations-aUdNng-O.svg create mode 100644 public/admin/build/assets/pipedrive-Dk_Mji0J.svg create mode 100644 public/admin/build/assets/pl.es-BQ3a0ltx.js create mode 100644 public/admin/build/assets/plans-DIcOmGwv.svg create mode 100644 public/admin/build/assets/products-zDh6UFtz.svg create mode 100644 public/admin/build/assets/pt-br.es-U9bVHsgs.js create mode 100644 public/admin/build/assets/pt-pt.es-U9bVHsgs.js create mode 100644 public/admin/build/assets/quotes-0H7ZCVr6.svg create mode 100644 public/admin/build/assets/ro.es-ldjMjt3l.js create mode 100644 public/admin/build/assets/ru.es-CS-QKsYL.js create mode 100644 public/admin/build/assets/sk.es-CAe8sEvL.js create mode 100644 public/admin/build/assets/sl.es-rdzOxu_X.js create mode 100644 public/admin/build/assets/sq.es-BY5E06HI.js create mode 100644 public/admin/build/assets/sr.es-QPseRVBy.js create mode 100644 public/admin/build/assets/sv.es-BfPUYcV1.js create mode 100644 public/admin/build/assets/tr.es-Cl6kz_IH.js create mode 100644 public/admin/build/assets/uk.es-DBEtNQnZ.js create mode 100644 public/admin/build/assets/users-6trD5il1.svg create mode 100644 public/admin/build/assets/vi.es-Bx2g_1jx.js create mode 100644 public/admin/build/assets/zh-cn.es-BWMmCH-R.js create mode 100644 public/admin/build/assets/zh-hk.es-DFZ0bKvi.js create mode 100644 public/admin/build/manifest.json create mode 100644 public/build/assets/app-DUr89oQr.js create mode 100644 public/build/assets/app-l0sNRNKZ.js create mode 100644 public/build/manifest.json create mode 100755 public/fonts/.gitignore create mode 100644 public/index.php create mode 100644 public/installer/build/assets/app-C9-BdERk.css create mode 100644 public/installer/build/assets/favicon-BtbgZBji.ico create mode 100644 public/installer/build/assets/icomoon-B5aogpno.woff create mode 100644 public/installer/build/assets/icomoon-BFKbaEOo.eot create mode 100644 public/installer/build/assets/icomoon-BgFe9HIr.ttf create mode 100644 public/installer/build/assets/krayin-logo-D5SO44Ys.svg create mode 100644 public/installer/build/assets/spinner-CWS6ej98.svg create mode 100644 public/installer/build/manifest.json create mode 100644 public/robots.txt create mode 100644 public/web.config create mode 100644 public/webform/build/assets/app-CWW-WGL4.css create mode 100644 public/webform/build/assets/app-DsP8OK1c.css create mode 100644 public/webform/build/assets/icomoon-CO37Y6g-.woff create mode 100644 public/webform/build/manifest.json create mode 100644 resources/css/app.css create mode 100644 resources/js/app.js create mode 100644 resources/js/bootstrap.js create mode 100644 resources/views/.gitignore create mode 100644 routes/api.php create mode 100644 routes/breadcrumbs.php create mode 100644 routes/channels.php create mode 100644 routes/console.php create mode 100644 routes/web.php create mode 100644 storage/.gitignore create mode 100644 storage/app/.gitignore create mode 100644 storage/app/public/.gitignore create mode 100644 storage/app/public/data-transfer/.gitignore create mode 100644 storage/app/public/data-transfer/samples/.gitignore create mode 100644 storage/app/public/data-transfer/samples/leads.csv create mode 100644 storage/app/public/data-transfer/samples/persons.csv create mode 100644 storage/app/public/data-transfer/samples/products.csv create mode 100644 storage/debugbar/.gitignore create mode 100644 storage/framework/.gitignore create mode 100644 storage/framework/cache/.gitignore create mode 100644 storage/framework/cache/data/.gitignore create mode 100644 storage/framework/sessions/.gitignore create mode 100644 storage/framework/testing/.gitignore create mode 100644 storage/framework/views/.gitignore create mode 100644 storage/logs/.gitignore create mode 100644 tests/Feature/AuthenticationTest.php create mode 100644 tests/Feature/HelpTest.php create mode 100644 tests/Feature/Security/AuthenticationHardeningTest.php create mode 100644 tests/Feature/Security/HtmlSanitizationTest.php create mode 100644 tests/Feature/Security/InstallerLockdownTest.php create mode 100644 tests/Feature/Security/PasswordResetStatusTest.php create mode 100644 tests/Feature/Security/SvgUploadSanitizationTest.php create mode 100644 tests/Pest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/BasicTest.php create mode 100644 vite.config.js diff --git a/.ai/skills b/.ai/skills new file mode 120000 index 0000000..3e73f3a --- /dev/null +++ b/.ai/skills @@ -0,0 +1 @@ +../.github/skills \ No newline at end of file diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..3e73f3a --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.github/skills \ No newline at end of file diff --git a/.codex/skills b/.codex/skills new file mode 120000 index 0000000..3e73f3a --- /dev/null +++ b/.codex/skills @@ -0,0 +1 @@ +../.github/skills \ No newline at end of file diff --git a/.cursor/skills b/.cursor/skills new file mode 120000 index 0000000..3e73f3a --- /dev/null +++ b/.cursor/skills @@ -0,0 +1 @@ +../.github/skills \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..37759de --- /dev/null +++ b/.env.example @@ -0,0 +1,71 @@ +APP_NAME='Krayin CRM' +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost +APP_TIMEZONE=Asia/Kolkata +APP_LOCALE=en +APP_CURRENCY=USD + +VITE_HOST= +VITE_PORT= + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +# Supported: mysql, mariadb +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel-crm +DB_USERNAME=root +DB_PASSWORD= +DB_PREFIX= + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailhog +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=laravel@krayincrm.com +MAIL_FROM_NAME="${APP_NAME}" +MAIL_DOMAIN=webkul.com + +MAIL_RECEIVER_DRIVER=sendgrid + +IMAP_HOST=imap.example.com +IMAP_PORT=993 +IMAP_ENCRYPTION=ssl +IMAP_VALIDATE_CERT=true +IMAP_USERNAME=your_username +IMAP_PASSWORD=your_password + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI= + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_APP_CLUSTER=mt1 + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..0d4af13 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +open_collective: bagisto \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md new file mode 100644 index 0000000..b7895b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -0,0 +1,36 @@ +--- +name: "🐛 Bug Report" +about: 'Report a general library issue.' +--- + +# Bug report + +### Title +**Just a quick sentence to brief your trouble with Krayin CRM or something associated with it.** +**Please be calm, short and emphasize on points.** + +### Issue Description +**Description helps the developers to understand the bug. It describes the problem encountered or some after effect of some kind.** + +### Preconditions +**Please provide as detailed information about your environment as possible.** + + 1. framework Version. + 2. Commit id. + +### Steps to reproduce +**It is important to provide a set of clear steps to reproduce this bug.If relevant please include code samples.** + + 1. step1 + 2. step2 + +### Expected result +**Tell us what should happen.** + +* [Screenshots, logs or description] + +### Actual result + +> **Tell us what happens instead.** + +* [points....] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md new file mode 100644 index 0000000..4c07928 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -0,0 +1,17 @@ +--- +name: "💡 Feature Request" +about: 'For ideas or feature requests, please make a pull request, or open an issue' +--- + +This repository is only for reporting bugs or issues. If you need support, please use +other channels: + +1. Write an email - mailto:support@krayincrm.com + +2. Create support ticket on https://krayincrm.uvdesk.com + +3. Visit forums to get support from our community (https://forums.krayincrm.com) + +Alternatively, you may use Facebook (https://www.facebook.com/groups/krayincrm/). + +We promise that more channels are coming soon!!! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md new file mode 100644 index 0000000..527514a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_Support_question.md @@ -0,0 +1,11 @@ +--- +name: "❔ Support Question" +about: 'This repository is only for reporting bugs or problems. If you need help, see: + https://github.com/krayin/laravel-crm#documentation' +--- + +This repository is only for reporting bugs or issues. If you need support, please use: + +1. Create support ticket on https://krayincrm.uvdesk.com + +Thanks! diff --git a/.github/ISSUE_TEMPLATE/4_Security_vulnerabilities.md b/.github/ISSUE_TEMPLATE/4_Security_vulnerabilities.md new file mode 100644 index 0000000..59d7343 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4_Security_vulnerabilities.md @@ -0,0 +1,9 @@ +--- +name: "🔒 Security Vulnerabilities" +about: 'For reporting security-related issues, see: https://github.com/krayin/laravel-crm#security-vulnerabilities' +--- + +PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW. + +If you have security vulnerability to address related to Krayin then please write a mail to us: +**support@krayincrm.com** \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9560133 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## Issue Reference + + +## Description + + +## How To Test This? + + +## Documentation +- [ ] My pull request requires an update on the documentation repository. + + +## Branch Selection + +- [ ] Target Branch: master + +## Tailwind Reordering + \ No newline at end of file diff --git a/.github/skills/README.md b/.github/skills/README.md new file mode 100644 index 0000000..1173bd3 --- /dev/null +++ b/.github/skills/README.md @@ -0,0 +1,51 @@ +# Skills Standard + +## Canonical Source + +All workspace skills are maintained in: + +- `.github/skills//SKILL.md` + +This is the single source of truth for every AI coding agent in this repository. + +## Agent Runtime Links + +The following paths are **symlinks** to `.github/skills` so each agent runtime +auto-discovers the same skills: + +| Path | Agent | +|------|-------| +| `.ai/skills` | Generic / `.ai` convention | +| `.claude/skills` | Claude Code | +| `.codex/skills` | OpenAI Codex | +| `.cursor/skills` | Cursor | +| `.kilocode/skills` | Kilo Code | + +> Symlinks are committed to git (mode `120000`). On a Windows checkout without +> symlink support they may materialize as plain files — clone with symlink +> support enabled (`git config core.symlinks true`). + +## Available Skills + +| Skill | Use when | +|-------|----------| +| `crm-package-development` | Creating a new Krayin CRM package/module or extending CRM functionality without touching core files. | +| `pest-testing` | Writing or debugging Krayin CRM unit/feature tests with Pest. | + +## Authoring Rules + +- Add or update skills **only** in `.github/skills` (never edit through a symlink target name as if it were a separate copy). +- Keep `name` and `description` in the YAML frontmatter of every `SKILL.md`. +- `name` must be lowercase, hyphen-separated, and **match the skill's directory name**. +- Use explicit "Use when..." trigger language in descriptions so agents match them reliably. +- Do not maintain duplicate `SKILL.md` files in agent-specific folders. + +## Validation + +Validate frontmatter and naming with the Agent Skills reference tool: + +```bash +skills-ref validate .github/skills/ +``` + +See the [Agent Skills specification](https://agentskills.io/specification) for the full format. diff --git a/.github/skills/crm-package-development/SKILL.md b/.github/skills/crm-package-development/SKILL.md new file mode 100644 index 0000000..e1cbeeb --- /dev/null +++ b/.github/skills/crm-package-development/SKILL.md @@ -0,0 +1,222 @@ +--- +name: crm-package-development +description: Use when creating a new Krayin CRM package or module, extending CRM functionality via a package, or adding custom business logic without modifying core files. Also use for any translation work — adding or moving lang files, adding a locale, or deciding whether a package gets its own Resources/lang directory — and whenever a fix or feature needs a CHANGELOG.md entry. Covers Laravel package structure, service providers, migrations, models, repositories, routes, controllers, views, localization, changelog entries, config, ACL, and menus. +--- + +# Skill: CRM Package Development (Krayin CRM) + +## Purpose +This skill guides the AI agent to design and develop a new CRM package +(module) for Krayin CRM in a clean, upgrade-safe, and maintainable way. + +--- + +## When to Activate +Activate this skill when the user wants to: +- Create a new CRM package/module +- Extend CRM functionality via a package +- Add custom business logic without modifying core files +- Add, move, or delete translation (lang) files, or add a new locale +- Fix a bug or ship a feature — the change needs a `CHANGELOG.md` entry + +--- + +## Project Context +- Framework: Laravel +- Product: Krayin CRM +- Krayin CRM is already installed and running +- The package must integrate seamlessly with existing CRM modules + +--- + +## Development Rules +- Follow Krayin CRM architecture and conventions +- Do NOT modify core Krayin files unless explicitly required +- Use Laravel package-based structure +- All database changes must be done using migrations +- Ensure backward compatibility and safe upgrades + +--- + +## Package Structure Guidelines + +A CRM package should follow this structure: + +```text +packages/ +└── Webkul/ + └── PackageName/ + ├── src/ + │ ├── Providers/ + │ │ └── PackageServiceProvider.php + │ ├── Models/ + │ ├── Contracts/ + │ ├── Repositories/ + │ ├── Http/ + │ │ ├── Controllers/ + │ │ └── Requests/ + │ ├── Routes/ + │ │ ├── admin.php + │ │ └── api.php + │ ├── Database/ + │ │ └── Migrations/ + │ ├── Resources/ + │ │ ├── views/ # only if the package renders its own Blade + │ │ └── lang/ # ONLY if views/ exists — see Localization + │ └── Config/ + │ ├── package.php + │ ├── menu.php + │ ├── core_config.php + │ └── acl.php + └── composer.json +``` + +--- + +## Localization (Translations) + +### The rule + +A package owns a `Resources/lang/` directory **only if it also owns +`Resources/views/`** — that is, only if it renders its own Blade templates. +Every other package ships **zero** translation files; its strings live in the +**Admin** package under `admin::app.*`. + +Before creating any lang file, ask: + +> Does this package have `src/Resources/views/` with Blade templates? + +- **Yes** → give it `Resources/lang//app.php` and register its own + namespace with `loadTranslationsFrom()` in the service provider. +- **No** → do **not** create a lang directory and do **not** call + `loadTranslationsFrom()`. Put the keys in the Admin package and reference them + as `admin::app.…`. + +Current state of the repo — preserve this invariant: + +| Owns views | Packages | Translations live in | +|---|---|---| +| Yes | `Admin`, `Installer`, `WebForm` | the package itself (`admin::`, `installer::`, `web_form::`) | +| No | everything else (`Core`, `DataTransfer`, `Lead`, `Contact`, `Quote`, `User`, …) | `Admin` (`admin::app.*`) | + +"Backend-only" strings are still translations and follow the same rule — +validation-rule messages, importer error maps, and `title` values referenced +from `Config/*.php` all belong in Admin when the package has no views. + +### Where keys go for a view-less package + +Nest them under the Admin key that already owns that domain so one feature's +strings stay together. Established precedent: + +| Before | After | +|---|---| +| `core::app.validations.code` | `admin::app.validations.message.code` | +| `data_transfer::app.importers.persons.title` | `admin::app.settings.data-transfer.importers.persons.title` | +| `data_transfer::app.validation.errors.system` | `admin::app.settings.data-transfer.validation.errors.system` | + +When you move keys out of a package, finish the job: rewrite every reference, +delete the package's `Resources/lang/`, and remove its `loadTranslationsFrom()` +call so the provider does not point at a path that no longer exists. + +### Working with Admin lang files + +- Admin ships one `app.php` per locale: `ar`, `en`, `es`, `fa`, `ko`, `pt_BR`, + `tr`, `vi`, `zh_CN`. **Add every new key to all of them in the same change** — + a key present only in `en` renders as the raw key string in other locales. +- If a real translation is unavailable, use the English text as the value. Never + omit the key, and never leave the key name as its own value. +- Preserve placeholders exactly: `:attribute`, `:days`, `:count` (Laravel) and + `%s` (importer messages formatted with `sprintf`). +- Pint enforces a **single space** around `=>` (`pint.json` → + `binary_operator_spaces`). Do not align `=>` into columns. +- Name the file `app.php` in every locale directory. A misnamed file (e.g. + `ar/ar.php`) is never loaded and the locale silently falls back to English. + +### Adding a new locale + +1. Create `/app.php` in each package that owns a lang directory + (`Admin`, `Installer`, `WebForm`), mirroring the `en` key structure exactly. +2. Register it in `config/app.php` → `available_locales`. This drives both the + admin locale dropdown (via `Core@locales`) and the web installer. +3. Add it to `$locales` in + `packages/Webkul/Installer/src/Console/Commands/Installer.php` so the CLI + installer offers it. +4. If the script is right-to-left, add it to the `['fa', 'ar']` checks in the + Admin layout Blade files. + +### Verifying + +Confirm parity with `en` — same key set, same placeholders — then check that +keys actually resolve: + +```bash +php artisan tinker --execute='app()->setLocale(""); echo trans("admin::app.");' +./vendor/bin/pint --test packages/Webkul/ +``` + +A key that resolves to its own name (`admin::app.foo.bar`) is missing. + +--- + +## Changelog + +### The rule + +**Every bug fix and every feature gets a `CHANGELOG.md` entry, in the same +change.** Do not leave it for someone else and do not treat it as optional +follow-up work — a fix that is not in the changelog is invisible to the release +notes. + +Add an entry when the change is user-visible or operator-visible: a bug fix, a +new feature, an enhancement, a security fix, a configuration option, or a +convention that affects how the product is built. Skip it only for changes with +no observable effect at all — a typo in a code comment, a pure test refactor. + +### Format + +Entries live at the top of [CHANGELOG.md](../../../CHANGELOG.md), newest version +first, and use exactly this shape: + +```markdown +## **v2.2.5 (Upcoming)** + +* #2601[fixed] Fixed lead value not saved when the currency is changed. + +* #2604[feature] Added Chinese (Simplified) translation. +``` + +- One blank line between every entry — the file is read as rendered markdown. +- No space between the issue number and the tag: `#2601[fixed]`, not `#2601 [fixed]`. +- Write the description as a complete sentence ending in a period, phrased from + the user's point of view, not the implementation's. + +Valid tags, and when each applies: + +| Tag | Use for | +|---|---| +| `[fixed]` | Something was broken and now works. | +| `[feature]` | New capability that did not exist before. | +| `[enhancement]` | Existing behaviour improved, refactored, or made faster. | +| `[security]` | Vulnerability fix. Reference the CVE when one exists. | + +### Which version section + +Add to the topmost section if it is still unreleased. If the top section is +already tagged `*Release*`, open a new one above it: + +```markdown +## **v (Upcoming)** +``` + +Replace `(Upcoming)` with the real date in the release commit — the existing +released headings show the expected style: `## **v2.2.4 (20th of July 2026)** *Release*`. + +### Two things not to do + +- **Never invent an issue or PR number.** They point at real GitHub PRs, so a + guessed number creates a false reference. If the PR does not exist yet, omit + the `#NNNN` prefix and leave the tag, then fill the number in once it is + opened. +- **Do not bump the version constant as part of a feature or fix.** + `KRAYIN_VERSION` in `packages/Webkul/Core/src/Core.php` is incremented in the + release commit, together with dating the changelog heading — not per change. diff --git a/.github/skills/pest-testing/SKILL.md b/.github/skills/pest-testing/SKILL.md new file mode 100644 index 0000000..3e7d6bb --- /dev/null +++ b/.github/skills/pest-testing/SKILL.md @@ -0,0 +1,144 @@ +--- +name: pest-testing +description: Use when writing or updating Krayin CRM tests (unit or feature), debugging test failures, using datasets, mocks or custom expectations, writing architecture tests, or when the user mentions test, spec, TDD, expects, assertion, or coverage. Uses the Pest PHP framework (v3) with Laravel 12. +license: MIT +metadata: + author: webkul +--- + +# Pest Testing in Krayin CRM + +## When to Apply + +Activate this skill when: +- Creating or updating tests (unit or feature) +- Debugging test failures +- Using datasets, mocks, or custom expectations +- Writing architecture or convention tests +- The user mentions test, spec, TDD, expects, assertion, coverage, or verifying behavior + +## Krayin Testing Structure + +### Test Locations + +Krayin tests are stored in the root `tests/` directory: + +``` +/tests +├── Feature/ +├── Unit/ +├── Pest.php +├── TestCase.php +└── CreatesApplication.php +``` + +### Available Test Suites + +Defined in `phpunit.xml`: + +| Test Suite | Location | Command | +|------------|----------|---------| +| Unit | `tests/Unit` | `php artisan test --testsuite="Unit"` | +| Feature | `tests/Feature` | `php artisan test --testsuite="Feature"` | + +## Pest.php Configuration + +`tests/Pest.php` binds the base test case for feature tests: + +```php +uses(\Tests\TestCase::class)->in('Feature'); +``` + +It also defines helper functions you can reuse, such as: +- `getDefaultAdmin()` +- `actingAsSanctumAuthenticatedAdmin()` +- `getFirstName($fullName)` + +## Running Tests + +### Run All Tests + +```bash +php artisan test --compact +``` + +### Run a Specific Test Suite + +```bash +php artisan test --testsuite="Feature" +php artisan test --testsuite="Unit" +``` + +### Run a Specific Test File + +```bash +php artisan test --compact tests/Feature/ExampleTest.php +``` + +### Run with Filter + +```bash +php artisan test --compact --filter=testName +``` + +## Creating New Tests + +### Create Feature Test + +```bash +php artisan make:test --pest ExampleFeatureTest +``` + +### Create Unit Test + +```bash +php artisan make:test --pest --unit ExampleUnitTest +``` + +## Basic Test Structure + +```php +it('passes basic assertion', function () { + expect(true)->toBeTrue(); +}); + +it('returns successful response', function () { + $response = $this->get('/'); + + $response->assertSuccessful(); +}); +``` + +## Assertions + +Prefer specific response helpers: + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Datasets + +```php +it('has valid emails', function (string $email) { + expect($email)->toContain('@'); +})->with([ + 'james' => 'james@krayin.com', + 'john' => 'john@krayin.com', +]); +``` + +## Common Pitfalls + +- Forgetting to use `assertSuccessful()` and other specific helpers +- Skipping `--pest` when creating tests +- Ignoring helper utilities already available in `tests/Pest.php` + +## Testing Best Practices + +- Cover happy paths, failure paths, and edge cases +- Keep tests isolated and focused +- Use factories where available +- Follow existing test patterns in the repository diff --git a/.github/workflows/admin_playwright_tests.yml b/.github/workflows/admin_playwright_tests.yml new file mode 100644 index 0000000..a55b402 --- /dev/null +++ b/.github/workflows/admin_playwright_tests.yml @@ -0,0 +1,122 @@ +name: Admin | Playwright Tests + +on: [push, pull_request] + +permissions: + contents: read + +env: + FORCE_COLOR: 1 + +jobs: + admin_playwright_test: + runs-on: ${{ matrix.operating-systems }} + + strategy: + fail-fast: false + matrix: + operating-systems: [ubuntu-latest] + php-versions: ['8.3'] + node-version: ['22.13.1'] + shard-index: [1,2,3,4,5,6] + shard-total: [6] + + name: Admin | Playwright Tests | Shard ${{ matrix.shard-index }} Of ${{ matrix.shard-total }} + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: krayin + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: curl, fileinfo, gd, intl, mbstring, openssl, pdo, pdo_mysql, tokenizer, zip + ini-values: error_reporting=E_ALL + tools: composer:v2 + + - name: Set Up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Node.js Dependencies + run: npm install + working-directory: packages/Webkul/Admin + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + working-directory: packages/Webkul/Admin + + - name: Setting Environment + run: | + cp .env.example .env + sed -i "s|^\(DB_HOST=\s*\).*$|\1127.0.0.1|" .env + sed -i "s|^\(DB_PORT=\s*\).*$|\1${{ job.services.mysql.ports['3306'] }}|" .env + sed -i "s|^\(DB_DATABASE=\s*\).*$|\1krayin|" .env + sed -i "s|^\(DB_USERNAME=\s*\).*$|\1root|" .env + sed -i "s|^\(DB_PASSWORD=\s*\).*$|\1root|" .env + sed -i "s|^\(APP_DEBUG=\s*\).*$|\1true|" .env + sed -i "s|^\(APP_URL=\s*\).*$|\1http://127.0.0.1:8000|" .env + cat .env + + - name: Install Composer Dependencies + run: composer install + + - name: Running Krayin Installer + run: php artisan krayin-crm:install --skip-env-check --skip-admin-creation + + - name: Clear Laravel Caches + run: php artisan optimize:clear + + - name: Verify Activity Binding + run: | + php artisan tinker --execute=" + // 1) Laravel container binding (needed by repositories) + if (! app()->bound(Webkul\\Activity\\Contracts\\Activity::class)) { + throw new RuntimeException('Activity contract is NOT bound in Laravel container'); + } + + // 2) Concord model-proxy registry (needed by ActivityProxy::modelClass()) + \$concord = app('concord'); + if (! \$concord->model(Webkul\\Activity\\Contracts\\Activity::class)) { + throw new RuntimeException('Activity contract is NOT registered in Concord model registry'); + } + + dump(app()->make(Webkul\\Activity\\Contracts\\Activity::class)::class); + dump(\$concord->model(Webkul\\Activity\\Contracts\\Activity::class)); + " + + # - name: Seed Product Table + # run: php artisan db:seed --class="Webkul\\Installer\\Database\\Seeders\\ProductTableSeeder" + + - name: Start Laravel server + run: | + php artisan serve --host=0.0.0.0 --port=8000 > server.log 2>&1 & + echo "Waiting for server to start..." + timeout 30 bash -c 'until curl -s http://127.0.0.1:8000 > /dev/null; do sleep 1; done' + + - name: Run All Playwright Tests + env: + BASE_URL: 'http://127.0.0.1:8000' + run: | + npx playwright test --reporter=list --config=tests/e2e-pw/playwright.config.ts --shard=${{ matrix.shard-index }}/${{ matrix.shard-total }} + working-directory: packages/Webkul/Admin + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-${{ matrix.shard-index }}-${{ github.run_id }}-${{ github.run_number }} + path: packages/Webkul/Admin/tests/e2e-pw/test-results + retention-days: 1 diff --git a/.github/workflows/auto_commits.yml b/.github/workflows/auto_commits.yml new file mode 100644 index 0000000..315d103 --- /dev/null +++ b/.github/workflows/auto_commits.yml @@ -0,0 +1,40 @@ +name: Linting Tests + +on: [push, pull_request] + +permissions: + contents: write + +jobs: + linting_tests: + runs-on: ${{ matrix.operating-systems }} + + strategy: + matrix: + operating-systems: [ubuntu-latest] + php-versions: ['8.3'] + + name: PHP ${{ matrix.php-versions }} test on ${{ matrix.operating-systems }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: composer:v2 + + - name: Install Pint + run: composer global require laravel/pint + env: + COMPOSER_HOME: ${{ runner.temp }}/composer + + - name: Run Pint + run: ${{ runner.temp }}/composer/vendor/bin/pint + + - name: Commit Linted Files + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: applied pint changes" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d9eb063 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: [push, pull_request] + +jobs: + tests: + runs-on: ${{ matrix.operating-system }} + + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ["8.3"] + + name: PHP ${{ matrix.php-versions }} test on ${{ matrix.operating-system }} + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: krayin + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: curl, gd, intl, mbstring, openssl, pdo, pdo_mysql, tokenizer, zip + ini-values: error_reporting=E_ALL + tools: composer:v2 + + - name: Running Composer Install + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Set Testing Environment + run: | + cp .env.example .env + sed -i "s|^\(APP_ENV=\s*\).*$|\1testing|" .env + sed -i "s|^\(DB_HOST=\s*\).*$|\1127.0.0.1|" .env + sed -i "s|^\(DB_PORT=\s*\).*$|\1${{ job.services.mysql.ports['3306'] }}|" .env + sed -i "s|^\(DB_DATABASE=\s*\).*$|\1krayin|" .env + sed -i "s|^\(DB_USERNAME=\s*\).*$|\1root|" .env + sed -i "s|^\(DB_PASSWORD=\s*\).*$|\1root|" .env + + - name: Running Krayin Installer + run: php artisan krayin-crm:install --skip-env-check --skip-admin-creation + + - name: Running Pest Test + run: vendor/bin/pest --parallel --colors=always diff --git a/.github/workflows/skills-consistency.yml b/.github/workflows/skills-consistency.yml new file mode 100644 index 0000000..fffd127 --- /dev/null +++ b/.github/workflows/skills-consistency.yml @@ -0,0 +1,41 @@ +name: Skills Consistency + +permissions: + contents: read + +on: + pull_request: + paths: + - '.github/skills/**' + - '.ai/**' + - '.claude/**' + - '.codex/**' + - '.cursor/**' + - '.kilocode/**' + - 'bin/validate-skills.sh' + - '.github/workflows/skills-consistency.yml' + push: + branches: + - '2.*' + - master + - main + paths: + - '.github/skills/**' + - '.ai/**' + - '.claude/**' + - '.codex/**' + - '.cursor/**' + - '.kilocode/**' + - 'bin/validate-skills.sh' + - '.github/workflows/skills-consistency.yml' + +jobs: + validate-skills: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate skill source-of-truth + run: bash bin/validate-skills.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df2925a --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +.env +.env.testing +.idea +.php_cs.cache +.phpunit.result.cache +.vscode +.vagrant +*.hot +/stubs +/data +/docker-compose-collection +Homestead.json +Homestead.yaml +/ignorables/* +/node_modules +npm-debug.log +package-lock.json +/docs +/playwright-report +/public/css +/public/js +/public/hot +/public/storage +/public/vendor +/lang/vendor +/storage/*.key +/storage/dcc-data/ +/vendor +yarn.lock +yarn-error.log + +# Playwright +node_modules/ +/test-results/ +/blob-report/ +/playwright/.cache/ diff --git a/.kilocode/skills b/.kilocode/skills new file mode 120000 index 0000000..3e73f3a --- /dev/null +++ b/.kilocode/skills @@ -0,0 +1 @@ +../.github/skills \ No newline at end of file diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..9daadf1 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,9 @@ +php: + preset: laravel + disabled: + - no_unused_imports + finder: + not-name: + - index.php +js: true +css: true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2cb0532 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# Krayin CRM: Agent Instructions + +This repository ships reusable **agent skills** for developing Krayin CRM. These +instructions apply to all AI coding agents (Claude Code, Codex, Copilot, Cursor, +Kilo Code, etc.). + +--- + +## Framework Context + +- **Product:** Krayin CRM (Webkul) — an open-source Laravel CRM. +- **Stack:** Laravel `^12.0`, PHP `^8.3`, Pest `^3.0`, Laravel Pint `^1.18`. +- **Follow Krayin's own conventions** — use the patterns already present in this + repository's modules; do not import patterns from other Laravel products. +- **Module location:** `packages/Webkul/{ModuleName}/src/` +- **Core modules (reference implementations):** `Lead`, `Contact`, `Activity`, + `Quote`, `Product`, `User`, `Attribute`, `Core`, `Admin`, `DataGrid`. + +--- + +## Skills + +Workspace skills live in `.github/skills/` (canonical source) and are symlinked +into every agent runtime (`.ai`, `.claude`, `.codex`, `.cursor`, `.kilocode`). +See [.github/skills/README.md](.github/skills/README.md) for the standard. + +| Skill | Use when | +|-------|----------| +| `crm-package-development` | Creating a new CRM package/module or extending CRM functionality without touching core. | +| `pest-testing` | Writing or debugging unit/feature tests with Pest. | + +Read the relevant `SKILL.md` before starting work in its domain. Do not restate +skill content here — keep details in the skills. + +--- + +## Critical Conventions (Never Deviate) + +- **Never modify core Krayin files** unless explicitly required. Extend behavior + through a package under `packages/Webkul/`. +- **All schema changes go through migrations** — never edit the database directly. +- **Follow the existing module layout** (Providers, Models, Contracts, + Repositories, Http/Controllers, Routes, Database/Migrations, Resources/views, + Config). The `crm-package-development` skill documents the full structure. +- **Repository pattern:** models are accessed through repositories bound via + contracts in the service provider — match the surrounding modules. +- **Translations follow the views:** a package gets its own `Resources/lang/` + **only if** it also has `Resources/views/`. Every view-less package keeps its + strings in the Admin package under `admin::app.*` — no lang directory, no + `loadTranslationsFrom()`. See the `crm-package-development` skill. +- **Every fix and feature updates `CHANGELOG.md`** in the same change, under the + topmost unreleased version heading. Never invent a `#PR` number and never bump + `KRAYIN_VERSION` outside a release commit. See the `crm-package-development` + skill for the entry format and tags. +- **Preserve backward compatibility** so the CRM stays upgrade-safe. + +--- + +## Development Cycle + +```bash +composer install # install PHP dependencies +php artisan migrate # run migrations +php artisan test --compact # run the Pest test suite +./vendor/bin/pint # format code (PSR-12 via Laravel Pint) +``` + +- Add tests for new behavior; follow the `pest-testing` skill. +- Run `./vendor/bin/pint` before committing — CI applies Pint formatting. +- Validate the skills setup with `bash bin/validate-skills.sh`. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 0000000..22f2c69 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,237 @@ +# CHANGELOG for 2.2 + +This changelog consists of the bug & security fixes and new features being included in the releases listed below. + +## **v2.2.6 (10th of Sept 2026)** + +* [feature] Added MariaDB support. + +* [feature] Added customizable lead card information. Datagrid columns and Kanban lead card fields can now be chosen per user through new column and card settings components. + +* [feature] Added PDF export for dashboard reports, available from the dashboard alongside the existing views. + +* [feature] Added Japanese (`ja`) translation for the Admin, Installer, GoogleContact and WebForm packages. + +* [feature] Added an associated group column to the users grid in Settings > Users. + +* [fixed] Fixed duplicated activity handling across the lead, person, product and warehouse activity controllers by consolidating the shared logic, and added the missing Japanese activity translations. + +* [fixed] Fixed the data transfer import queue processing inconsistently, and corrected the form control group rendering used by the import screen. + +* [fixed] Fixed the Google Contact settings screen erroring when no account was connected, and added the corresponding translations. + +* [fixed] Fixed the mail ACL mapping so email actions are checked against the correct permission. + +* [fixed] Fixed people created from a lead being saved with a null `user_id`, which hid them from the person listing for users restricted to group or individual data scope. They are now assigned to the lead owner, falling back to the acting user. + +* [fixed] Fixed the stage API resource omitting `lead_pipeline_id`, so stages could not be matched to their pipeline. + +* [fixed] Fixed email attachment downloads being blocked by the URL sanitizer middleware. + +* [fixed] Added the missing Chinese translations for the users grid's associated group column. + +* [fixed] Fixed flaky admin end-to-end tests around organization owner lookup, lead creation and rich-text comment fields. + +* [security] Fixed user and role listings not being scoped by the acting user's data scope, which allowed users to see records outside their own group. Roles now track their creator via a new `created_by` column. + +* [security] Hardened the admin ACL middleware to fail closed. An administrative route with no ACL mapping is now denied rather than allowed, inheriting the permission of its nearest mapped ancestor, with a narrow allow-list for authentication, self-service account management and generic UI helpers. + +* [security] Removed SVG from the allowed upload types for the admin logo and favicon configuration fields. + +* [security] Fixed a security issue configuration file uploads. + +* [security] Fixed a security issue activity notes and the TinyMCE editor component. + +* [security] Fixed a security issue in the web form embed view. + +* [security] Fixed a security issue in attribute downloads. + +* [security] Fixed a security issue in mail links. + +* [security] Fixed broken tag handling in the tag settings controller. + +* [security] Fixed SVG sanitization bypasses in media and configuration file uploads. + +* [security] Secured installer APIs. + +## **v2.2.5 (4th of Aug 2026)** + +* #2631[fixed] Fixed the persons CSV import creating duplicate records and dropping select attribute values on re-import. Existing people are now updated by matched email regardless of a changed phone or organization (so the reported count is accurate), and select/multiselect option labels (or ids) are resolved to their option ids instead of being stored as `0`. + +* #2630[fixed] Fixed date attributes in the persons CSV import silently saving as `0000-00-00`. Spreadsheet serial numbers and regional formats such as `DD/MM/YYYY` are now normalised to a valid date, and a value that cannot be parsed is reported as a row error instead of being stored as a zero date. + +* [feature] Added Chinese (Simplified) `zh_CN` translation for the Admin, Installer, DataTransfer, WebForm and Core packages. + +* [feature] Added a configurable default dashboard date range — 1 month, 3 months, 9 months, 1 year, 2 years or a custom number of days — under Configuration > General > Settings > Dashboard Configurations. + +* [fixed] Fixed menu item names set in Configuration not applying to section pages, breadcrumbs and the mobile sidebar. Previously only the desktop sidebar reflected a rename. + +* [fixed] Fixed renaming the "Mail" and "Contacts" menu items having no effect anywhere, as their configuration fields did not match the actual menu keys. + +* [fixed] Fixed the dashboard date range label omitting the year on ranges spanning more than one calendar year, which rendered as "30 Jul - 30 Jul". + +* [fixed] Fixed Arabic DataTransfer translations never loading, as the file was named `ar/ar.php` instead of `ar/app.php`. + +* [fixed] Fixed the missing Korean translation for the "None" input validation option on the create and edit attribute forms. + +* [enhancement] Moved the Core and DataTransfer package translations into the Admin package. Only packages that ship their own Blade views now carry a `Resources/lang` directory. + +* [enhancement] Reduced database queries on every admin page by loading the configured menu names in a single query instead of one per menu item. + +* [enhancement] Documented the localization convention in the `crm-package-development` agent skill and in AGENTS.md. +* [feature] Added a collapse/expand toggle to the admin sidebar, matching the Bagisto admin. The choice is remembered across page loads, and page content now reflows to the sidebar width instead of being overlaid by it. The sidebar no longer expands on hover; it is controlled by the toggle only. +* #2614[security] Fixed unauthenticated installer access and executable email attachment upload vulnerabilities. + +* #2612[feature] Added import and export support for custom attributes for Leads and Persons. + +* #2609[feature] Added Google Contacts export for Persons with Google account connection, duplicate detection, queued export progress, and result summary. + +* #2608[fixed] Added missing "none" key to the Korean locale for attribute validation. + +* #2606[feature] Added a collapse/expand toggle to the admin sidebar, matching the Bagisto admin. The choice is remembered across page loads, and page content now reflows to the sidebar width instead of being overlaid by it. The sidebar no longer expands on hover; it is controlled by the toggle only. + +* #2606[feature] Added an option to show or hide the "Powered by" bar under Configuration > General > Settings > Powered by Section Configurations. + +* #2603[feature] Added Korean translations for the Installer, DataTransfer, WebForm, and Core packages. + +* #2602[feature] Added Korean translation support for the Admin package. + +* #2600[fixed] Fixed invalid activity calendar .ics date-times by emitting UTC RFC 5545 values. + +* #2592[security] Fixed webhook validation to reject internal endpoint URLs. + +* #2580[enhancement] Added a "None" option to input validation for text attributes. + +## **v2.2.4 (20th of July 2026)** *Release* + +* #2590[fixed] Fixed page does not refresh after creating a record via Quick Add. + +* #2589[fixed] Fixed Quick Add not working for users with group and individual permissions. + +* #2582[fixed] Fixed pipeline field visible on public webform. + +* #2581[enhancement] Fixed responsive UI issues when page is zoomed. + +* #2579[feature] Allow group selection for individual view permission users. + +* #2575[enhancement] Added previous month's sales update in Kanban view. + +* #2573[enhancement] Added dashboard support for multiple pipelines. + +* #2572[enhancement] Added filter by tag option in Contacts > Persons. + +* #2571[fixed] Fixed issue with lead creation. + +* #2570[fixed] Fixed auto-fill lead email issue. + +* #2567[fixed] Fixed IDOR agent record access control vulnerability. + +* #2563[fixed] Fixed Kanban infinite scroll duplicates issue. + +* #2583[fixed] Fixed SQL injection in rotten lead filter. + +* #2585[security] Fixed unrestricted file upload vulnerability (CVE-2026-38526). + +* #2559[fixed] Fixed agent record access control issue. + +* #2556[fixed] Fixed installation config save issue. + +* #2550[fixed] Fixed Kanban infinite scroll duplicates. + +* #2549[enhancement] Added support tab feature. + +* #2548[enhancement] Allow search by phone and email when creating a lead. + +* #2546[feature] Quick Attribute now available at lead form. + +* #2545[feature] Added agent skills functionality. + +* #2544[enhancement] Added validate skills. + +* #2543[enhancement] Added Agents Skills folder. + +* #2542[fixed] Fixed stored XSS vulnerability in notes field. + +* #2541[fixed] Fixed quote description truncation issue. + +* #2539[fixed] Fixed lost revenue arrow UI issue. + +* #2538[fixed] Fixed missing translations. + +* #2501[fixed] Fixed sales owner not saved in organization. + +* #2500[fixed] Fixed activities date filter range issue. + +* #2479[fixed] Fixed textarea field not rendered in WebForm. + +* #2471[fixed] Fixed missing translations for lead won/lost modal. + +* #2420[fixed] Added missing mega search translations for settings and configurations. + +* #2533[fixed] Fixed GUI installation issue. + +* #2419[security] Fixed stored XSS vulnerability in notes field. + +* #2454[fixed] Fixed quote description truncation. + +* #2407[fixed] Fixed missing translations. + +* #2157[fixed] Fixed auto-fill lead email when creating a lead. + +* #2258[fixed] Fixed issue with same-as-billing-address field. + +## **v2.2.3 (1st of May 2026)** *Release* + +* [fixed] Pipline critical issue resolved. + +## **v2.2.2 (1st of May 2026)** *Release* + +* [fixed] Update Change Log and version. + +## **v2.2.1 (1st of May 2026)** *Release* + +* [fixed] Quote fields now auto-fill correctly when a quote is linked to a lead. + +* [fixed] Fixed price formatting issue. + +* [fixed] Fixed Lead Kanban list ordering. + +* [fixed] Fixed header block position at the top. + +* [fixed] Updated Activity UI. + +* [fixed] Admins can now view and share quote details to a person from the quote list. + +* [fixed] Fixed submission issue on the web form. + +* [fixed] Fixed activity display issue in the Calendar view. + +* [fixed] Logo update issue resolved. + +* [enhancement] Drag-and-drop support added to Activity. Admins can now change date and time directly from the Calendar view. + +* [feature] Quick App feature added for faster access to key CRM actions. + +* [feature] Admins can now add or update a person directly from the lead view page. + +* [security] Resolved an authentication bypass vulnerability caused by improper access control in the installer. + +## **v2.2.0 (17th of March 2026)** *Release* + +* **[Laravel 12 Upgrade]** Upgraded framework to Laravel 12 + +* #2480[enhancement] Codebase updates and refinements. + +* #2478[enhancement] Improved class instantiation handling. + +* #2472[enhancement] Upgrade to Laravel 12. + +* #2470[enhancement] Updated auto_commits.yml configuration. + +* #2469[enhancement] General enhancements and optimizations. + +* #2468[enhancement] Documentation updates (MD files). + +* #2450[fixed] Added ACL support for warehouses. + +* #2444[fixed] Improved global search functionality for organizations. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..882f374 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at support@krayincrm.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa358e8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright 2010-2025, Webkul Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..49e1b98 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +

+ + + + + Krayin CRM + + +

+ +

+Total Downloads +Latest Stable Version +License +

+ + +![enter image description here](https://raw.githubusercontent.com/krayin/temp-media/master/dashboard.png) + +## Topics + +1. [Introduction](#introduction) +2. [Documentation](#documentation) +3. [Requirements](#requirements) +4. [Installation & Configuration](#installation-and-configuration) +4. [Docker Installation](https://devdocs.krayincrm.com/2.0/introduction/docker.html) +5. [Krayin Cloud System](#krayin-cloud-hosting) +6. [License](#license) +7. [Security Vulnerabilities](#security-vulnerabilities) + +### Introduction + +[Krayin CRM](https://krayincrm.com) is a hand tailored CRM framework built on some of the hottest opensource technologies +such as [Laravel](https://laravel.com) (a [PHP](https://secure.php.net/) framework) and [Vue.js](https://vuejs.org) +a progressive Javascript framework. + +**Free & Opensource Laravel CRM solution for SMEs and Enterprises for complete customer lifecycle management.** + +**Read our documentation: [Krayin CRM Docs](https://devdocs.krayincrm.com/)** + +**We also have a forum for any type of concerns, feature requests, or discussions. Please visit: [Krayin CRM Forums](https://forums.krayincrm.com/)** + +# Visit our live [Demo](https://demo.krayincrm.com) + + + Chinese + + +It packs in lots of features that will allow your E-Commerce business to scale in no time: + +- Descriptive and Simple Admin Panel. +- Admin Dashboard. +- Custom Attributes. +- Built on Modular Approach. +- Email parsing via Sendgrid. +- Check out [these features and more](https://krayincrm.com/features/). + +**For Developers**: +Take advantage of two of the hottest frameworks used in this project -- Laravel and Vue.js -- both of which have been used in Krayin CRM. + +### Documentation + +#### Krayin Documentation [https://devdocs.krayincrm.com](https://devdocs.krayincrm.com) + +### Requirements + +- **SERVER**: Apache 2 or NGINX. +- **RAM**: 3 GB or higher. +- **PHP**: 8.3 or higher +- **Composer**: 2.5 or higher +- **For MySQL users**: 8.0.32 or higher. +- **For MariaDB users**: 11.4 LTS or higher (11.8 LTS recommended). + +### Installation and Configuration + +##### Execute these commands below, in order + +``` +composer create-project +``` + +- Find **.env** file in root directory and change the **APP_URL** param to your **domain**. + +- Also, Configure the **Mail** and **Database** parameters inside **.env** file. + +``` +php artisan krayin-crm:install +``` + +**To execute Krayin**: + +##### On server: + +Warning: Before going into production mode we recommend you uninstall developer dependencies. +In order to do that, run the command below: + +> composer install --no-dev + +``` +Open the specified entry point in your hosts file in your browser or make an entry in hosts file if not done. +``` + +##### On local: + +``` +php artisan route:clear +php artisan serve +``` + + +**How to log in as admin:** + +> _http(s)://example.com/admin/login_ + +``` +email:admin@example.com +password:admin123 +``` + +### Krayin Cloud Hosting + +[Krayin CRM Cloud Hosting](https://krayincrm.com/crm-cloud-hosting) is a fully managed hosting solution where our team sets up, secures, and configures your Krayin CRM on reliable infrastructure. + +Get a ready-to-use CRM on your own domain, without manual installation or infrastructure complexity, and focus on growing your business while we handle the technology. + +![Krayin CRM Cloud Hosting](https://raw.githubusercontent.com/krayin/temp-media/master/cloud_hosting.png) + +### Krayin CRM Multi Tenant SaaS + +[Krayin CRM Multi Tenant SaaS](https://krayincrm.com/extensions/krayin-crm-multi-tenant-saas-extension/) Krayin Multitenant SaaS is a Laravel-based CRM solution that allows multiple businesses (tenants) to use a single application instance while keeping their data isolated and secure. + +![enter image description here](https://raw.githubusercontent.com/krayin/temp-media/master/krayin-saas.png) + +### WhatsApp CRM Integration + +[Krayin CRM WhatsApp](https://krayincrm.com/extensions/krayin-crm-whatsapp-extension/) Extension enables the store administrator to generate leads via their WhatsApp number. + +![enter image description here](https://raw.githubusercontent.com/krayin/temp-media/master/krayin-crm-whatsapp-integration.png) + +### VoIP CRM Integration + +[Krayin CRM VoIP](https://krayincrm.com/extensions/krayin-crm-voip/) extension allows the user to make Trunk calls over a broadband Internet connection and the user can also perform Inbound routes. + +![enter image description here](https://raw.githubusercontent.com/krayin/temp-media/master/krayin-voip.png) + +### License + +Krayin CRM is a fully open-source CRM framework which will always be free under the [MIT License](https://github.com/krayin/laravel-crm/blob/2.1/LICENSE). + +### Security Vulnerabilities + +Please don't disclose security vulnerabilities publicly. If you find any security vulnerability in Krayin CRM then please email us: sales@krayincrm.com. diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..958b600 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,42 @@ +# UPGRADE Guide + +- [Upgrading To v2.2 From v2.1](#upgrading-to-v22-from-v21) + +## High Impact Changes + +- [Laravel 12 Upgrade](#laravel-12-upgrade) + +## Upgrading To v2.2 From v2.1 + +> [!NOTE] +> We strive to document every potential breaking change. However, as some of these alterations occur in lesser-known sections of Krayin, only a fraction of them may impact your application. + +### Updating Dependencies + +**Impact Probability: High** + +#### PHP 8.3 Required + +Krayin CRM v2.2 now requires PHP 8.3 or greater. + +### Laravel 12 Upgrade + +**Impact Probability: High** + +Krayin CRM v2.2 has been upgraded to Laravel 12, which introduces stricter type checking and modernized date/time handling. + +#### Key Changes + +- **Bootstrap**: `bootstrap/app.php` now uses the new `Application::configure()` builder pattern. Service providers are listed in `bootstrap/providers.php`. + +- **Kernels Removed**: `app/Http/Kernel.php` and `app/Console/Kernel.php` are removed. Middleware and scheduling are configured in `bootstrap/app.php` and `routes/console.php` respectively. + +- **Exception Handler Removed**: `app/Exceptions/Handler.php` is removed. Exception handling is configured in `bootstrap/app.php`. + +- **Middleware Classes Removed**: Built-in middleware wrappers (EncryptCookies, VerifyCsrfToken, TrimStrings, TrustProxies, etc.) are removed. Customizations are now done via `bootstrap/app.php`. + +- **Service Providers Simplified**: `AuthServiceProvider`, `EventServiceProvider`, `RouteServiceProvider`, and `BroadcastServiceProvider` are removed. Their logic is handled in `bootstrap/app.php` or `AppServiceProvider`. + +- **Config**: `config/app.php` no longer contains `providers` or `aliases` arrays. + +- **doctrine/dbal** dependency has been removed (native column modification in Laravel 12). \ No newline at end of file diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ + */ + use HasFactory, Notifiable; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..452e6b6 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,24 @@ +handleCommand(new ArgvInput); + +exit($status); diff --git a/bin/validate-skills.sh b/bin/validate-skills.sh new file mode 100755 index 0000000..755221f --- /dev/null +++ b/bin/validate-skills.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +canonical_dir=".github/skills" + +echo "[skills] validating canonical directory" +if [[ ! -d "$canonical_dir" ]]; then + echo "ERROR: $canonical_dir does not exist" + exit 1 +fi + +skill_count=$(find "$canonical_dir" -mindepth 2 -maxdepth 2 -type f -name SKILL.md | wc -l | tr -d ' ') +if [[ "$skill_count" -eq 0 ]]; then + echo "ERROR: no SKILL.md files found under $canonical_dir" + exit 1 +fi + +echo "[skills] validating agent link targets" +while IFS='|' read -r link_path expected_target; do + [[ -z "$link_path" ]] && continue + + if [[ ! -L "$link_path" ]]; then + echo "ERROR: $link_path must be a symlink to $expected_target" + exit 1 + fi + + actual_target=$(readlink "$link_path") + if [[ "$actual_target" != "$expected_target" ]]; then + echo "ERROR: $link_path points to $actual_target (expected $expected_target)" + exit 1 + fi +done <<'LINKS' +.ai/skills|../.github/skills +.claude/skills|../.github/skills +.codex/skills|../.github/skills +.cursor/skills|../.github/skills +.kilocode/skills|../.github/skills +LINKS + +echo "[skills] checking for SKILL.md outside canonical source" +mapfile -t outside_skills < <( + find . \ + -path './.git' -prune -o \ + -path './vendor' -prune -o \ + -path './node_modules' -prune -o \ + -type f -name SKILL.md -print \ + | grep -v '^./.github/skills/' || true +) + +if (( ${#outside_skills[@]} > 0 )); then + echo "ERROR: found SKILL.md files outside $canonical_dir" + printf ' - %s\n' "${outside_skills[@]}" + exit 1 +fi + +echo "[skills] validating frontmatter keys and name/directory match" +while IFS= read -r skill_file; do + if ! grep -qE '^name:' "$skill_file"; then + echo "ERROR: missing name in $skill_file" + exit 1 + fi + + if ! grep -qE '^description:' "$skill_file"; then + echo "ERROR: missing description in $skill_file" + exit 1 + fi + + dir_name=$(basename "$(dirname "$skill_file")") + name_value=$(grep -E '^name:' "$skill_file" | head -1 | sed -E 's/^name:[[:space:]]*//; s/[[:space:]]*$//') + if [[ "$name_value" != "$dir_name" ]]; then + echo "ERROR: name '$name_value' must match directory '$dir_name' in $skill_file" + exit 1 + fi +done < <(find "$canonical_dir" -mindepth 2 -maxdepth 2 -type f -name SKILL.md | sort) + +echo "[skills] validation passed ($skill_count skills)" diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100755 index 0000000..251badb --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,36 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + channels: __DIR__.'/../routes/channels.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + $middleware->append(CanInstall::class); + + $middleware->encryptCookies(except: [ + 'dark_mode', + 'sidebar_collapsed', + ]); + + $middleware->validateCsrfTokens(except: [ + 'admin/mail/inbound-parse', + 'admin/web-forms/forms/*', + ]); + + $middleware->api(prepend: [ + EnsureFrontendRequestsAreStateful::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..2a85923 --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,64 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Admin URL + |-------------------------------------------------------------------------- + | + | This URL suffix is used to define the admin url for example + | admin/ or backend/ + | + */ + + 'admin_path' => env('APP_ADMIN_PATH', 'admin'), + + 'asset_url' => env('ASSET_URL'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => env('APP_TIMEZONE', 'Asia/Kolkata'), + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Available Locales Configuration + |-------------------------------------------------------------------------- + | + | The application available locale determines the supported locales + | by application + | + */ + + 'available_locales' => [ + 'ar' => 'Arabic', + 'en' => 'English', + 'es' => 'Español', + 'fa' => 'Persian', + 'ja' => '日本語', + 'ko' => '한국어', + 'pt_BR' => 'Portuguese', + 'tr' => 'Türkçe', + 'vi' => 'Vietnamese', + 'zh_CN' => '简体中文', + ], + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => 'en_US', + + /* + |-------------------------------------------------------------------------- + | Base Currency Code + |-------------------------------------------------------------------------- + | + | Here you may specify the base currency code for your application. + | + */ + + 'currency' => env('APP_CURRENCY', 'USD'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..fdc71f9 --- /dev/null +++ b/config/auth.php @@ -0,0 +1,107 @@ + [ + 'guard' => 'user', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | here which uses session storage and the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session", "token" + | + */ + + 'guards' => [ + 'user' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => User::class, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | The expire time is the number of minutes that the reset token should be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => 'user_password_resets', + 'expire' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | times out and the user is prompted to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => 10800, + +]; diff --git a/config/breadcrumbs.php b/config/breadcrumbs.php new file mode 100644 index 0000000..bd15c4e --- /dev/null +++ b/config/breadcrumbs.php @@ -0,0 +1,78 @@ + 'breadcrumbs::bootstrap5', + + /* + |-------------------------------------------------------------------------- + | Breadcrumbs File(s) + |-------------------------------------------------------------------------- + | + | The file(s) where breadcrumbs are defined. e.g. + | + | - base_path('routes/breadcrumbs.php') + | - glob(base_path('breadcrumbs/*.php')) + | + */ + + 'files' => base_path('routes/breadcrumbs.php'), + + /* + |-------------------------------------------------------------------------- + | Exceptions + |-------------------------------------------------------------------------- + | + | Determine when to throw an exception. + | + */ + + // When route-bound breadcrumbs are used but the current route doesn't have a name (UnnamedRouteException) + 'unnamed-route-exception' => true, + + // When route-bound breadcrumbs are used and the matching breadcrumb doesn't exist (InvalidBreadcrumbException) + 'missing-route-bound-breadcrumb-exception' => true, + + // When a named breadcrumb is used but doesn't exist (InvalidBreadcrumbException) + 'invalid-named-breadcrumb-exception' => true, + + /* + |-------------------------------------------------------------------------- + | Classes + |-------------------------------------------------------------------------- + | + | Subclass the default classes for more advanced customisations. + | + */ + + // Manager + 'manager-class' => Manager::class, + + // Generator + 'generator-class' => Generator::class, + +]; diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 0000000..2d52982 --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,64 @@ + env('BROADCAST_DRIVER', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'useTLS' => true, + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..e32a2fd --- /dev/null +++ b/config/cache.php @@ -0,0 +1,106 @@ + env('CACHE_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "apc", "array", "database", "file", + | "memcached", "redis", "dynamodb", "null" + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, + 'lock_connection' => null, + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'cache', + 'lock_connection' => 'default', + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing a RAM based store such as APC or Memcached, there might + | be other applications utilizing the same cache. So, we'll specify a + | value to get prefixed to all our keys so we can avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), + +]; diff --git a/config/concord.php b/config/concord.php new file mode 100644 index 0000000..e8db60f --- /dev/null +++ b/config/concord.php @@ -0,0 +1,41 @@ + [ + DataTransferModuleServiceProvider::class, + AdminModuleServiceProvider::class, + AttributeModuleServiceProvider::class, + AutomationModuleServiceProvider::class, + ContactModuleServiceProvider::class, + CoreModuleServiceProvider::class, + DataGridModuleServiceProvider::class, + EmailTemplateModuleServiceProvider::class, + EmailModuleServiceProvider::class, + LeadModuleServiceProvider::class, + ProductModuleServiceProvider::class, + QuoteModuleServiceProvider::class, + TagModuleServiceProvider::class, + UserModuleServiceProvider::class, + WarehouseModuleServiceProvider::class, + WebFormModuleServiceProvider::class, + ], + + 'register_route_models' => true, +]; diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..4222c11 --- /dev/null +++ b/config/cors.php @@ -0,0 +1,62 @@ + [ + 'admin/web-forms/forms/*', + ], + + /* + * Matches the request method. `['*']` allows all methods. + */ + 'allowed_methods' => ['*'], + + /* + * Matches the request origin. `['*']` allows all origins. Wildcards can be used, eg `*.mydomain.com` + */ + 'allowed_origins' => ['*'], + + /* + * Patterns that can be used with `preg_match` to match the origin. + */ + 'allowed_origins_patterns' => [], + + /* + * Sets the Access-Control-Allow-Headers response header. `['*']` allows all headers. + */ + 'allowed_headers' => ['*'], + + /* + * Sets the Access-Control-Expose-Headers response header with these headers. + */ + 'exposed_headers' => [], + + /* + * Sets the Access-Control-Max-Age response header when > 0. + */ + 'max_age' => 0, + + /* + * Sets the Access-Control-Allow-Credentials header. + */ + 'supports_credentials' => false, +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..8a16066 --- /dev/null +++ b/config/database.php @@ -0,0 +1,166 @@ + env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => env('DB_PREFIX', ''), + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => env('DB_PREFIX', ''), + 'prefix_indexes' => true, + 'strict' => false, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => env('DB_PREFIX', ''), + 'prefix_indexes' => true, + 'strict' => false, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => env('DB_PREFIX', ''), + 'prefix_indexes' => true, + 'schema' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => env('DB_PREFIX', ''), + 'prefix_indexes' => true, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..acff539 --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,72 @@ + env('FILESYSTEM_DISK', 'public'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been setup for each driver as an example of the required options. + | + | Supported Drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/hashing.php b/config/hashing.php new file mode 100644 index 0000000..8425770 --- /dev/null +++ b/config/hashing.php @@ -0,0 +1,52 @@ + 'bcrypt', + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + +]; diff --git a/config/imap.php b/config/imap.php new file mode 100644 index 0000000..3ea7ed4 --- /dev/null +++ b/config/imap.php @@ -0,0 +1,207 @@ + env('IMAP_DEFAULT_ACCOUNT', 'default'), + + /* + |-------------------------------------------------------------------------- + | Default date format + |-------------------------------------------------------------------------- + | + | The default date format is used to convert any given Carbon::class object into a valid date string. + | These are currently known working formats: "d-M-Y", "d-M-y", "d M y" + | + */ + 'date_format' => 'd-M-Y', + + /* + |-------------------------------------------------------------------------- + | Available IMAP accounts + |-------------------------------------------------------------------------- + | + | Please list all IMAP accounts which you are planning to use within the + | array below. + | + */ + 'accounts' => [ + 'default' => [ + 'host' => env('IMAP_HOST', 'localhost'), + 'port' => env('IMAP_PORT', 993), + 'protocol' => env('IMAP_PROTOCOL', 'imap'), // might also use imap, [pop3 or nntp (untested)] + 'encryption' => env('IMAP_ENCRYPTION', 'ssl'), // Supported: false, 'ssl', 'tls', 'notls', 'starttls' + 'validate_cert' => env('IMAP_VALIDATE_CERT', true), + 'username' => env('IMAP_USERNAME', 'root@example.com'), + 'password' => env('IMAP_PASSWORD', ''), + 'authentication' => env('IMAP_AUTHENTICATION', null), + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + 'timeout' => 30, + 'extensions' => [], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Available IMAP options + |-------------------------------------------------------------------------- + | + | Available php imap config parameters are listed below + | -Delimiter (optional): + | This option is only used when calling $oClient-> + | You can use any supported char such as ".", "/", (...) + | -Fetch option: + | IMAP::FT_UID - Message marked as read by fetching the body message + | IMAP::FT_PEEK - Fetch the message without setting the "seen" flag + | -Fetch sequence id: + | IMAP::ST_UID - Fetch message components using the message uid + | IMAP::ST_MSGN - Fetch message components using the message number + | -Body download option + | Default TRUE + | -Flag download option + | Default TRUE + | -Soft fail + | Default FALSE - Set to TRUE if you want to ignore certain exception while fetching bulk messages + | -RFC822 + | Default TRUE - Set to FALSE to prevent the usage of \imap_rfc822_parse_headers(). + | See https://github.com/Webklex/php-imap/issues/115 for more information. + | -Debug enable to trace communication traffic + | -UID cache enable the UID cache + | -Fallback date is used if the given message date could not be parsed + | -Boundary regex used to detect message boundaries. If you are having problems with empty messages, missing + | attachments or anything like this. Be advised that it likes to break which causes new problems.. + | -Message key identifier option + | You can choose between the following: + | 'id' - Use the MessageID as array key (default, might cause hickups with yahoo mail) + | 'number' - Use the message number as array key (isn't always unique and can cause some interesting behavior) + | 'list' - Use the message list number as array key (incrementing integer (does not always start at 0 or 1) + | 'uid' - Use the message uid as array key (isn't always unique and can cause some interesting behavior) + | -Fetch order + | 'asc' - Order all messages ascending (probably results in oldest first) + | 'desc' - Order all messages descending (probably results in newest first) + | -Disposition types potentially considered an attachment + | Default ['attachment', 'inline'] + | -Common folders + | Default folder locations and paths assumed if none is provided + | -Open IMAP options: + | DISABLE_AUTHENTICATOR - Disable authentication properties. + | Use 'GSSAPI' if you encounter the following + | error: "Kerberos error: No credentials cache + | file found (try running kinit) (...)" + | or ['GSSAPI','PLAIN'] if you are using outlook mail + | -Decoder options (currently only the message subject and attachment name decoder can be set) + | 'utf-8' - Uses imap_utf8($string) to decode a string + | 'mimeheader' - Uses mb_decode_mimeheader($string) to decode a string + | + */ + 'options' => [ + 'delimiter' => '/', + 'fetch' => IMAP::FT_PEEK, + 'sequence' => IMAP::ST_UID, + 'fetch_body' => true, + 'fetch_flags' => true, + 'soft_fail' => false, + 'rfc822' => true, + 'debug' => false, + 'uid_cache' => true, + // 'fallback_date' => "01.01.1970 00:00:00", + 'boundary' => '/boundary=(.*?(?=;)|(.*))/i', + 'message_key' => 'list', + 'fetch_order' => 'asc', + 'dispositions' => ['attachment', 'inline'], + 'common_folders' => [ + 'root' => 'INBOX', + 'junk' => 'INBOX/Junk', + 'draft' => 'INBOX/Drafts', + 'sent' => 'INBOX/Sent', + 'trash' => 'INBOX/Trash', + ], + 'decoder' => [ + 'message' => 'utf-8', // mimeheader + 'attachment' => 'utf-8', // mimeheader + ], + 'open' => [ + // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' + ], + ], + + /* + |-------------------------------------------------------------------------- + | Available flags + |-------------------------------------------------------------------------- + | + | List all available / supported flags. Set to null to accept all given flags. + */ + 'flags' => ['recent', 'flagged', 'answered', 'deleted', 'seen', 'draft'], + + /* + |-------------------------------------------------------------------------- + | Available events + |-------------------------------------------------------------------------- + | + */ + 'events' => [ + 'message' => [ + 'new' => MessageNewEvent::class, + 'moved' => MessageMovedEvent::class, + 'copied' => MessageCopiedEvent::class, + 'deleted' => MessageDeletedEvent::class, + 'restored' => MessageRestoredEvent::class, + ], + 'folder' => [ + 'new' => FolderNewEvent::class, + 'moved' => FolderMovedEvent::class, + 'deleted' => FolderDeletedEvent::class, + ], + 'flag' => [ + 'new' => FlagNewEvent::class, + 'deleted' => FlagDeletedEvent::class, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Available masking options + |-------------------------------------------------------------------------- + | + | By using your own custom masks you can implement your own methods for + | a better and faster access and less code to write. + | + | Checkout the two examples custom_attachment_mask and custom_message_mask + | for a quick start. + | + | The provided masks below are used as the default masks. + */ + 'masks' => [ + 'message' => MessageMask::class, + 'attachment' => AttachmentMask::class, + ], +]; diff --git a/config/krayin-vite.php b/config/krayin-vite.php new file mode 100755 index 0000000..8ed51cf --- /dev/null +++ b/config/krayin-vite.php @@ -0,0 +1,32 @@ + [ + 'admin' => [ + 'hot_file' => 'admin-vite.hot', + 'build_directory' => 'admin/build', + 'package_assets_directory' => 'src/Resources/assets', + ], + + 'installer' => [ + 'hot_file' => 'installer-vite.hot', + 'build_directory' => 'installer/build', + 'package_assets_directory' => 'src/Resources/assets', + ], + + 'webform' => [ + 'hot_file' => 'webform-vite.hot', + 'build_directory' => 'webform/build', + 'package_assets_directory' => 'src/Resources/assets', + ], + ], +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..6aa77fe --- /dev/null +++ b/config/logging.php @@ -0,0 +1,104 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['single'], + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => 14, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + 'emoji' => ':boom:', + 'level' => env('LOG_LEVEL', 'critical'), + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => SyslogUdpHandler::class, + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + ], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + ], + +]; diff --git a/config/mail-receiver.php b/config/mail-receiver.php new file mode 100644 index 0000000..32f1573 --- /dev/null +++ b/config/mail-receiver.php @@ -0,0 +1,17 @@ + env('MAIL_RECEIVER_DRIVER', 'sendgrid'), +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..57699f0 --- /dev/null +++ b/config/mail.php @@ -0,0 +1,130 @@ + env('MAIL_MAILER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", + | "postmark", "log", "array", "failover" + | + */ + + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'verify_peer' => false, + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'mailgun' => [ + 'transport' => 'mailgun', + ], + + 'postmark' => [ + 'transport' => 'postmark', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails sent by your application to be sent from + | the same address. Here, you may specify a name and address that is + | used globally for all e-mails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS'), + 'name' => env('MAIL_FROM_NAME'), + ], + + /* + |-------------------------------------------------------------------------- + | Default Mailer Domain + |-------------------------------------------------------------------------- + | + | This option controls the domain for email message_id that is used to send email + | messages sent by your application. + | + */ + + 'domain' => env('MAIL_DOMAIN', 'webkul.com'), + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => 'default', + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..1222296 --- /dev/null +++ b/config/queue.php @@ -0,0 +1,89 @@ + env('QUEUE_CONNECTION', 'sync'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'retry_after' => 90, + 'block_for' => 0, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'your-queue-name'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => 90, + 'block_for' => null, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/repository.php b/config/repository.php new file mode 100644 index 0000000..50ee5fe --- /dev/null +++ b/config/repository.php @@ -0,0 +1,249 @@ + [ + 'limit' => 15, + ], + + /* + |-------------------------------------------------------------------------- + | Fractal Presenter Config + |-------------------------------------------------------------------------- + | + + Available serializers: + ArraySerializer + DataArraySerializer + JsonApiSerializer + + */ + 'fractal' => [ + 'params' => [ + 'include' => 'include', + ], + 'serializer' => DataArraySerializer::class, + ], + + /* + |-------------------------------------------------------------------------- + | Cache Config + |-------------------------------------------------------------------------- + | + */ + 'cache' => [ + /* + |-------------------------------------------------------------------------- + | Cache Status + |-------------------------------------------------------------------------- + | + | Enable or disable cache + | + */ + 'enabled' => false, + + /* + |-------------------------------------------------------------------------- + | Cache Minutes + |-------------------------------------------------------------------------- + | + | Time of expiration cache + | + */ + 'minutes' => 30, + + /* + |-------------------------------------------------------------------------- + | Cache Repository + |-------------------------------------------------------------------------- + | + | Instance of Illuminate\Contracts\Cache\Repository + | + */ + 'repository' => 'cache', + + /* + |-------------------------------------------------------------------------- + | Cache Clean Listener + |-------------------------------------------------------------------------- + | + | + | + */ + 'clean' => [ + + /* + |-------------------------------------------------------------------------- + | Enable clear cache on repository changes + |-------------------------------------------------------------------------- + | + */ + 'enabled' => true, + + /* + |-------------------------------------------------------------------------- + | Actions in Repository + |-------------------------------------------------------------------------- + | + | create : Clear Cache on create Entry in repository + | update : Clear Cache on update Entry in repository + | delete : Clear Cache on delete Entry in repository + | + */ + 'on' => [ + 'create' => true, + 'update' => true, + 'delete' => true, + ], + ], + + 'params' => [ + /* + |-------------------------------------------------------------------------- + | Skip Cache Params + |-------------------------------------------------------------------------- + | + | + | Ex: http://prettus.local/?search=lorem&skipCache=true + | + */ + 'skipCache' => 'skipCache', + ], + + /* + |-------------------------------------------------------------------------- + | Methods Allowed + |-------------------------------------------------------------------------- + | + | methods cacheable : all, paginate, find, findByField, findWhere, getByCriteria + | + | Ex: + | + | 'only' =>['all','paginate'], + | + | or + | + | 'except' =>['find'], + */ + 'allowed' => [ + 'only' => null, + 'except' => null, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Criteria Config + |-------------------------------------------------------------------------- + | + | Settings of request parameters names that will be used by Criteria + | + */ + 'criteria' => [ + /* + |-------------------------------------------------------------------------- + | Accepted Conditions + |-------------------------------------------------------------------------- + | + | Conditions accepted in consultations where the Criteria + | + | Ex: + | + | 'acceptedConditions'=>['=','like'] + | + | $query->where('foo','=','bar') + | $query->where('foo','like','bar') + | + */ + 'acceptedConditions' => [ + '=', + 'like', + 'in', + ], + /* + |-------------------------------------------------------------------------- + | Request Params + |-------------------------------------------------------------------------- + | + | Request parameters that will be used to filter the query in the repository + | + | Params : + | + | - search : Searched value + | Ex: http://prettus.local/?search=lorem + | + | - searchFields : Fields in which research should be carried out + | Ex: + | http://prettus.local/?search=lorem&searchFields=name;email + | http://prettus.local/?search=lorem&searchFields=name:like;email + | http://prettus.local/?search=lorem&searchFields=name:like + | + | - filter : Fields that must be returned to the response object + | Ex: + | http://prettus.local/?search=lorem&filter=id,name + | + | - orderBy : Order By + | Ex: + | http://prettus.local/?search=lorem&orderBy=id + | + | - sortedBy : Sort + | Ex: + | http://prettus.local/?search=lorem&orderBy=id&sortedBy=asc + | http://prettus.local/?search=lorem&orderBy=id&sortedBy=desc + | + | - searchJoin: Specifies the search method (AND / OR), by default the + | application searches each parameter with OR + | EX: + | http://prettus.local/?search=lorem&searchJoin=and + | http://prettus.local/?search=lorem&searchJoin=or + | + */ + 'params' => [ + 'search' => 'search', + 'searchFields' => 'searchFields', + 'filter' => 'filter', + 'orderBy' => 'orderBy', + 'sortedBy' => 'sortedBy', + 'with' => 'with', + 'searchJoin' => 'searchJoin', + 'withCount' => 'withCount', + ], + ], + /* + |-------------------------------------------------------------------------- + | Generator Config + |-------------------------------------------------------------------------- + | + */ + 'generator' => [ + 'basePath' => app()->path(), + 'rootNamespace' => 'App\\', + 'stubsOverridePath' => app()->path(), + 'paths' => [ + 'models' => 'Entities', + 'repositories' => 'Repositories', + 'interfaces' => 'Repositories', + 'transformers' => 'Transformers', + 'presenters' => 'Presenters', + 'validators' => 'Validators', + 'controllers' => 'Http/Controllers', + 'provider' => 'RepositoryServiceProvider', + 'criteria' => 'Criteria', + ], + ], +]; diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..f801168 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,68 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '' + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['user'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. If this value is null, personal access tokens do + | not expire. This won't tweak the lifetime of first-party sessions. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'verify_csrf_token' => VerifyCsrfToken::class, + 'encrypt_cookies' => EncryptCookies::class, + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..9bff862 --- /dev/null +++ b/config/services.php @@ -0,0 +1,39 @@ + [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + ], + + 'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'google' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => env('GOOGLE_REDIRECT_URI'), + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..4e0f66c --- /dev/null +++ b/config/session.php @@ -0,0 +1,201 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION', null), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | While using one of the framework's cache driven session backends you may + | list a cache store that should be used for these sessions. This value + | must match with one of the application's configured cache "stores". + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE', null), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application but you are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you if it can not be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. You are free to modify this option if needed. + | + */ + + 'http_only' => true, + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" since this is a secure default value. + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => 'lax', + +]; diff --git a/config/tinker.php b/config/tinker.php new file mode 100644 index 0000000..c187942 --- /dev/null +++ b/config/tinker.php @@ -0,0 +1,50 @@ + [ + // App\Console\Commands\ExampleCommand::class, + ], + + /* + |-------------------------------------------------------------------------- + | Auto Aliased Classes + |-------------------------------------------------------------------------- + | + | Tinker will not automatically alias classes in your vendor namespaces + | but you may explicitly allow a subset of classes to get aliased by + | adding the names of each of those classes to the following list. + | + */ + + 'alias' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Classes That Should Not Be Aliased + |-------------------------------------------------------------------------- + | + | Typically, Tinker automatically aliases classes as you require them in + | Tinker. However, you may wish to never alias certain classes, which + | you may accomplish by listing the classes in the following array. + | + */ + + 'dont_alias' => [ + 'App\Nova', + ], + +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..22b8a18 --- /dev/null +++ b/config/view.php @@ -0,0 +1,36 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')) + ), + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..97fc976 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1,2 @@ +*.sqlite +*.sqlite-journal diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..acc91ba --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,47 @@ + $this->faker->name, + 'email' => $this->faker->unique()->safeEmail, + 'email_verified_at' => now(), + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + * + * @return Factory + */ + public function unverified() + { + return $this->state(function (array $attributes) { + return [ + 'email_verified_at' => null, + ]; + }); + } +} diff --git a/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/database/migrations/2019_08_19_000000_create_failed_jobs_table.php new file mode 100644 index 0000000..1719198 --- /dev/null +++ b/database/migrations/2019_08_19_000000_create_failed_jobs_table.php @@ -0,0 +1,36 @@ +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. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php new file mode 100644 index 0000000..e828ad8 --- /dev/null +++ b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/migrations/2024_09_09_094040_create_job_batches_table.php b/database/migrations/2024_09_09_094040_create_job_batches_table.php new file mode 100644 index 0000000..226e640 --- /dev/null +++ b/database/migrations/2024_09_09_094040_create_job_batches_table.php @@ -0,0 +1,39 @@ +string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->text('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('job_batches'); + } +}; diff --git a/database/migrations/2024_09_09_094042_create_jobs_table.php b/database/migrations/2024_09_09_094042_create_jobs_table.php new file mode 100644 index 0000000..a786a89 --- /dev/null +++ b/database/migrations/2024_09_09_094042_create_jobs_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('jobs'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..4b55b51 --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,19 @@ +call(KrayinDatabaseSeeder::class); + } +} diff --git a/example.txt b/example.txt new file mode 100644 index 0000000..fa379d3 --- /dev/null +++ b/example.txt @@ -0,0 +1 @@ +Hello, this is a generated file! \ No newline at end of file diff --git a/lang/en/auth.php b/lang/en/auth.php new file mode 100644 index 0000000..6598e2c --- /dev/null +++ b/lang/en/auth.php @@ -0,0 +1,20 @@ + 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +]; diff --git a/lang/en/pagination.php b/lang/en/pagination.php new file mode 100644 index 0000000..d481411 --- /dev/null +++ b/lang/en/pagination.php @@ -0,0 +1,19 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/lang/en/passwords.php b/lang/en/passwords.php new file mode 100644 index 0000000..2345a56 --- /dev/null +++ b/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Your password has been reset!', + 'sent' => 'We have emailed your password reset link!', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => "We can't find a user with that email address.", + +]; diff --git a/lang/en/validation.php b/lang/en/validation.php new file mode 100644 index 0000000..c77e41c --- /dev/null +++ b/lang/en/validation.php @@ -0,0 +1,152 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'multiple_of' => 'The :attribute must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + +]; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5763ce2 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.6.4", + "laravel-vite-plugin": "^1.0.0", + "vite": "^6.4.3" + } +} diff --git a/packages/Webkul/Activity/composer.json b/packages/Webkul/Activity/composer.json new file mode 100755 index 0000000..0cf8329 --- /dev/null +++ b/packages/Webkul/Activity/composer.json @@ -0,0 +1,28 @@ +{ + "name": "krayin/laravel-activity", + "license": "MIT", + "authors": [ + { + "name": "Jitendra Singh", + "email": "jitendra@webkul.com" + } + ], + "require": { + "krayin/laravel-core": "^1.0", + "krayin/laravel-user": "^1.0" + }, + "autoload": { + "psr-4": { + "Webkul\\Activity\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Webkul\\Activity\\Providers\\ActivityServiceProvider" + ], + "aliases": {} + } + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/packages/Webkul/Activity/src/Contracts/Activity.php b/packages/Webkul/Activity/src/Contracts/Activity.php new file mode 100644 index 0000000..9a88b87 --- /dev/null +++ b/packages/Webkul/Activity/src/Contracts/Activity.php @@ -0,0 +1,5 @@ +increments('id'); + $table->string('title')->nullable(); + $table->string('type'); + $table->text('comment')->nullable(); + $table->json('additional')->nullable(); + $table->datetime('schedule_from')->nullable(); + $table->datetime('schedule_to')->nullable(); + $table->boolean('is_done')->default(0); + + $table->integer('user_id')->unsigned(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('activities'); + } +}; diff --git a/packages/Webkul/Activity/src/Database/Migrations/2021_05_15_151855_create_activity_files_table.php b/packages/Webkul/Activity/src/Database/Migrations/2021_05_15_151855_create_activity_files_table.php new file mode 100644 index 0000000..30d516c --- /dev/null +++ b/packages/Webkul/Activity/src/Database/Migrations/2021_05_15_151855_create_activity_files_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->string('name'); + $table->string('path'); + + $table->integer('activity_id')->unsigned(); + $table->foreign('activity_id')->references('id')->on('activities')->onDelete('cascade'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('activity_files'); + } +}; diff --git a/packages/Webkul/Activity/src/Database/Migrations/2021_07_28_142453_create_activity_participants_table.php b/packages/Webkul/Activity/src/Database/Migrations/2021_07_28_142453_create_activity_participants_table.php new file mode 100644 index 0000000..ba72faa --- /dev/null +++ b/packages/Webkul/Activity/src/Database/Migrations/2021_07_28_142453_create_activity_participants_table.php @@ -0,0 +1,39 @@ +increments('id'); + + $table->integer('activity_id')->unsigned(); + $table->foreign('activity_id')->references('id')->on('activities')->onDelete('cascade'); + + $table->integer('user_id')->nullable()->unsigned(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + + $table->integer('person_id')->nullable()->unsigned(); + $table->foreign('person_id')->references('id')->on('persons')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('activity_participants'); + } +}; diff --git a/packages/Webkul/Activity/src/Database/Migrations/2021_11_17_190943_add_location_column_in_activities_table.php b/packages/Webkul/Activity/src/Database/Migrations/2021_11_17_190943_add_location_column_in_activities_table.php new file mode 100644 index 0000000..53727e1 --- /dev/null +++ b/packages/Webkul/Activity/src/Database/Migrations/2021_11_17_190943_add_location_column_in_activities_table.php @@ -0,0 +1,32 @@ +string('location')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('activities', function (Blueprint $table) { + $table->dropColumn('location'); + }); + } +}; diff --git a/packages/Webkul/Activity/src/Database/Migrations/2025_01_17_151632_alter_activities_table.php b/packages/Webkul/Activity/src/Database/Migrations/2025_01_17_151632_alter_activities_table.php new file mode 100644 index 0000000..f36b8a1 --- /dev/null +++ b/packages/Webkul/Activity/src/Database/Migrations/2025_01_17_151632_alter_activities_table.php @@ -0,0 +1,52 @@ +dropForeign(['user_id']); + + $table->unsignedInteger('user_id')->nullable()->change(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('activities', function (Blueprint $table) { + $tablePrefix = DB::getTablePrefix(); + + // Disable foreign key checks temporarily. + DB::statement('SET FOREIGN_KEY_CHECKS=0'); + + // Drop the foreign key constraint using raw SQL. + DB::statement('ALTER TABLE '.$tablePrefix.'activities DROP FOREIGN KEY activities_user_id_foreign'); + + // Drop the index. + DB::statement('ALTER TABLE '.$tablePrefix.'activities DROP INDEX activities_user_id_foreign'); + + // Change the column to be non-nullable. + $table->unsignedInteger('user_id')->nullable(false)->change(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + + // Re-enable foreign key checks. + DB::statement('SET FOREIGN_KEY_CHECKS=1'); + }); + } +}; diff --git a/packages/Webkul/Activity/src/Models/Activity.php b/packages/Webkul/Activity/src/Models/Activity.php new file mode 100644 index 0000000..1b1262a --- /dev/null +++ b/packages/Webkul/Activity/src/Models/Activity.php @@ -0,0 +1,111 @@ + 'datetime', + 'schedule_to' => 'datetime', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'title', + 'type', + 'location', + 'comment', + 'additional', + 'schedule_from', + 'schedule_to', + 'is_done', + 'user_id', + ]; + + /** + * Get the user that owns the activity. + */ + public function user() + { + return $this->belongsTo(UserProxy::modelClass()); + } + + /** + * The participants that belong to the activity. + */ + public function participants() + { + return $this->hasMany(ParticipantProxy::modelClass()); + } + + /** + * Get the file associated with the activity. + */ + public function files() + { + return $this->hasMany(FileProxy::modelClass(), 'activity_id'); + } + + /** + * The leads that belong to the activity. + */ + public function leads() + { + return $this->belongsToMany(LeadProxy::modelClass(), 'lead_activities'); + } + + /** + * The Person that belong to the activity. + */ + public function persons() + { + return $this->belongsToMany(PersonProxy::modelClass(), 'person_activities'); + } + + /** + * The leads that belong to the activity. + */ + public function products() + { + return $this->belongsToMany(ProductProxy::modelClass(), 'product_activities'); + } + + /** + * The Warehouse that belong to the activity. + */ + public function warehouses() + { + return $this->belongsToMany(WarehouseProxy::modelClass(), 'warehouse_activities'); + } +} diff --git a/packages/Webkul/Activity/src/Models/ActivityProxy.php b/packages/Webkul/Activity/src/Models/ActivityProxy.php new file mode 100644 index 0000000..d748e53 --- /dev/null +++ b/packages/Webkul/Activity/src/Models/ActivityProxy.php @@ -0,0 +1,7 @@ +path); + } + + /** + * Get image url for the product image. + */ + public function getUrlAttribute() + { + return $this->url(); + } + + /** + * Get the activity that owns the file. + */ + public function activity() + { + return $this->belongsTo(ActivityProxy::modelClass()); + } + + /** + * @return array + */ + public function toArray() + { + $array = parent::toArray(); + + $array['url'] = $this->url; + + return $array; + } +} diff --git a/packages/Webkul/Activity/src/Models/FileProxy.php b/packages/Webkul/Activity/src/Models/FileProxy.php new file mode 100644 index 0000000..09ff947 --- /dev/null +++ b/packages/Webkul/Activity/src/Models/FileProxy.php @@ -0,0 +1,7 @@ +belongsTo(ActivityProxy::modelClass()); + } + + /** + * Get the user that owns the participant. + */ + public function user() + { + return $this->belongsTo(UserProxy::modelClass()); + } + + /** + * Get the person that owns the participant. + */ + public function person() + { + return $this->belongsTo(PersonProxy::modelClass()); + } +} diff --git a/packages/Webkul/Activity/src/Models/ParticipantProxy.php b/packages/Webkul/Activity/src/Models/ParticipantProxy.php new file mode 100644 index 0000000..c4433af --- /dev/null +++ b/packages/Webkul/Activity/src/Models/ParticipantProxy.php @@ -0,0 +1,7 @@ +app->bindIf(ActivityContract::class, Activity::class); + $this->app->bindIf(FileContract::class, File::class); + $this->app->bindIf(ParticipantContract::class, Participant::class); + } + + /** + * Bootstrap services. + * + * @return void + */ + public function boot(Router $router) + { + $this->loadMigrationsFrom(__DIR__.'/../Database/Migrations'); + + // Guarantee that Concord's model-proxy registry contains the Activity + // bindings after all providers have booted. ModelProxy::modelClass() + // looks up $concord->model($contract) — a separate store from the + // Laravel container — so this registration is strictly necessary for + // proxies like ActivityProxy used in Eloquent relationships. + $this->app->booted(function () { + $concord = app('concord'); + + if (! $concord->model(ActivityContract::class)) { + $concord->registerModel(ActivityContract::class, Activity::class); + } + + if (! $concord->model(FileContract::class)) { + $concord->registerModel(FileContract::class, File::class); + } + + if (! $concord->model(ParticipantContract::class)) { + $concord->registerModel(ParticipantContract::class, Participant::class); + } + }); + } +} diff --git a/packages/Webkul/Activity/src/Providers/ModuleServiceProvider.php b/packages/Webkul/Activity/src/Providers/ModuleServiceProvider.php new file mode 100644 index 0000000..7af3136 --- /dev/null +++ b/packages/Webkul/Activity/src/Providers/ModuleServiceProvider.php @@ -0,0 +1,17 @@ +create([ + 'name' => $data['name'] ?? $data['file']->getClientOriginalName(), + 'path' => $data['file']->store('activities/'.$activity->id), + 'activity_id' => $activity->id, + ]); + } + + if (! isset($data['participants'])) { + return $activity; + } + + foreach ($data['participants']['users'] ?? [] as $userId) { + $activity->participants()->create([ + 'user_id' => $userId, + ]); + } + + foreach ($data['participants']['persons'] ?? [] as $personId) { + $activity->participants()->create([ + 'person_id' => $personId, + ]); + } + + return $activity; + } + + /** + * Update pipeline. + * + * @param int $id + * @param string $attribute + * @return Activity + */ + public function update(array $data, $id, $attribute = 'id') + { + $activity = parent::update($data, $id); + + if (isset($data['participants'])) { + $activity->participants()->delete(); + + foreach ($data['participants']['users'] ?? [] as $userId) { + /** + * In some cases, the component exists in an HTML form, and traditional HTML does not send an empty array. + * Therefore, we need to check for an empty string. + * This scenario occurs only when all participants are removed. + */ + if (empty($userId)) { + break; + } + + $activity->participants()->create([ + 'user_id' => $userId, + ]); + } + + foreach ($data['participants']['persons'] ?? [] as $personId) { + /** + * In some cases, the component exists in an HTML form, and traditional HTML does not send an empty array. + * Therefore, we need to check for an empty string. + * This scenario occurs only when all participants are removed. + */ + if (empty($personId)) { + break; + } + + $activity->participants()->create([ + 'person_id' => $personId, + ]); + } + } + + return $activity; + } + + /** + * @param string $dateRange + * @return mixed + */ + public function getActivities($dateRange) + { + $tablePrefix = \DB::getTablePrefix(); + + return $this->select( + 'activities.id', + 'activities.created_at', + 'activities.title', + 'activities.schedule_from as start', + 'activities.schedule_to as end', + 'users.name as user_name', + ) + ->addSelect(\DB::raw('IF('.$tablePrefix.'activities.is_done, "done", "") as class')) + ->leftJoin('activity_participants', 'activities.id', '=', 'activity_participants.activity_id') + ->leftJoin('users', 'activities.user_id', '=', 'users.id') + ->whereIn('type', ['call', 'meeting', 'lunch']) + ->whereBetween('activities.schedule_from', $dateRange) + ->where(function ($query) { + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $query->whereIn('activities.user_id', $userIds) + ->orWhereIn('activity_participants.user_id', $userIds); + } + }) + ->distinct() + ->get(); + } + + /** + * @param string $startFrom + * @param string $endFrom + * @param array $participants + * @param int $id + * @return bool + */ + public function isDurationOverlapping($startFrom, $endFrom, $participants, $id) + { + $queryBuilder = $this->leftJoin('activity_participants', 'activities.id', '=', 'activity_participants.activity_id') + ->where(function ($query) use ($startFrom, $endFrom) { + $query->where([ + ['activities.schedule_from', '<=', $startFrom], + ['activities.schedule_to', '>=', $startFrom], + ])->orWhere([ + ['activities.schedule_from', '>=', $startFrom], + ['activities.schedule_from', '<=', $endFrom], + ]); + }) + ->where(function ($query) use ($participants) { + if (is_null($participants)) { + return; + } + + if (isset($participants['users'])) { + $query->orWhereIn('activity_participants.user_id', $participants['users']); + } + + if (isset($participants['persons'])) { + $query->orWhereIn('activity_participants.person_id', $participants['persons']); + } + }) + ->groupBy('activities.id'); + + if (! is_null($id)) { + $queryBuilder->where('activities.id', '!=', $id); + } + + return $queryBuilder->count() ? true : false; + } +} diff --git a/packages/Webkul/Activity/src/Repositories/FileRepository.php b/packages/Webkul/Activity/src/Repositories/FileRepository.php new file mode 100755 index 0000000..9e53693 --- /dev/null +++ b/packages/Webkul/Activity/src/Repositories/FileRepository.php @@ -0,0 +1,19 @@ +entity ?? $model, 'activities')) { + return; + } + + if (! $model instanceof AttributeValue) { + $activity = app(ActivityRepository::class)->create([ + 'type' => 'system', + 'title' => trans('admin::app.activities.created'), + 'is_done' => 1, + 'user_id' => auth()->check() + ? auth()->id() + : null, + ]); + + $model->activities()->attach($activity->id); + + return; + } + + static::logActivity($model); + }); + + static::updated(function ($model) { + if (! method_exists($model->entity ?? $model, 'activities')) { + return; + } + + static::logActivity($model); + }); + + static::deleting(function ($model) { + if (! method_exists($model->entity ?? $model, 'activities')) { + return; + } + + $model->activities()->delete(); + }); + } + + /** + * Create activity. + */ + protected static function logActivity($model) + { + $customAttributes = []; + + if (method_exists($model, 'getCustomAttributes')) { + $customAttributes = $model->getCustomAttributes()->pluck('code')->toArray(); + } + + $updatedAttributes = static::getUpdatedAttributes($model); + + foreach ($updatedAttributes as $attributeCode => $attributeData) { + if (in_array($attributeCode, $customAttributes)) { + continue; + } + + $attributeCode = $model->attribute?->name ?: $attributeCode; + + $activity = app(ActivityRepository::class)->create([ + 'type' => 'system', + 'title' => trans('admin::app.activities.updated', ['attribute' => $attributeCode]), + 'is_done' => 1, + 'additional' => json_encode([ + 'attribute' => $attributeCode, + 'new' => [ + 'value' => $attributeData['new'], + 'label' => static::getAttributeLabel($attributeData['new'], $model->attribute), + ], + 'old' => [ + 'value' => $attributeData['old'], + 'label' => static::getAttributeLabel($attributeData['old'], $model->attribute), + ], + ]), + 'user_id' => auth()->id(), + ]); + + if ($model instanceof AttributeValue) { + $model->entity->activities()->attach($activity->id); + } else { + $model->activities()->attach($activity->id); + } + } + } + + /** + * Get attribute label. + */ + protected static function getAttributeLabel($value, $attribute) + { + return app(AttributeValueRepository::class)->getAttributeLabel($value, $attribute); + } + + /** + * Create activity. + */ + protected static function getUpdatedAttributes($model) + { + $updatedAttributes = []; + + foreach ($model->getDirty() as $key => $value) { + if (in_array($key, [ + 'id', + 'attribute_id', + 'entity_id', + 'entity_type', + 'updated_at', + ])) { + continue; + } + + $newValue = static::decodeValueIfJson($value); + + $oldValue = static::decodeValueIfJson($model->getOriginal($key)); + + if ($newValue != $oldValue) { + $updatedAttributes[$key] = [ + 'new' => $newValue, + 'old' => $oldValue, + ]; + } + } + + return $updatedAttributes; + } + + /** + * Convert value if json. + */ + protected static function decodeValueIfJson($value) + { + if ( + ! is_array($value) + && json_decode($value, true) + ) { + $value = json_decode($value, true); + } + + if (! is_array($value)) { + return $value; + } + + static::ksortRecursive($value); + + return $value; + } + + /** + * Sort array recursively. + */ + protected static function ksortRecursive(&$array) + { + if (! is_array($array)) { + return; + } + + ksort($array); + + foreach ($array as &$value) { + if (! is_array($value)) { + continue; + } + + static::ksortRecursive($value); + } + } +} diff --git a/packages/Webkul/Admin/.gitignore b/packages/Webkul/Admin/.gitignore new file mode 100755 index 0000000..4d32a59 --- /dev/null +++ b/packages/Webkul/Admin/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/package-lock.json +npm-debug.log +/playwright-report +/test-results \ No newline at end of file diff --git a/packages/Webkul/Admin/composer.json b/packages/Webkul/Admin/composer.json new file mode 100755 index 0000000..78f413c --- /dev/null +++ b/packages/Webkul/Admin/composer.json @@ -0,0 +1,35 @@ +{ + "name": "krayin/laravel-admin", + "license": "MIT", + "authors": [ + { + "name": "Jitendra Singh", + "email": "jitendra@webkul.com" + } + ], + "require": { + "krayin/laravel-attribute": "^1.0", + "krayin/laravel-contact": "^1.0", + "krayin/laravel-core": "^1.0", + "krayin/laravel-email": "^1.0", + "krayin/laravel-lead": "^1.0", + "krayin/laravel-product": "^1.0", + "krayin/laravel-tag": "^1.0", + "krayin/laravel-ui": "^1.0", + "krayin/laravel-user": "^1.0" + }, + "autoload": { + "psr-4": { + "Webkul\\Admin\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Webkul\\Admin\\Providers\\AdminServiceProvider" + ], + "aliases": {} + } + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/packages/Webkul/Admin/example.txt b/packages/Webkul/Admin/example.txt new file mode 100644 index 0000000..fa379d3 --- /dev/null +++ b/packages/Webkul/Admin/example.txt @@ -0,0 +1 @@ +Hello, this is a generated file! \ No newline at end of file diff --git a/packages/Webkul/Admin/package.json b/packages/Webkul/Admin/package.json new file mode 100644 index 0000000..f492e2c --- /dev/null +++ b/packages/Webkul/Admin/package.json @@ -0,0 +1,36 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@types/node": "^22.7.8", + "autoprefixer": "^10.4.16", + "axios": "^1.7.4", + "laravel-vite-plugin": "^1.0", + "postcss": "^8.4.23", + "tailwindcss": "^3.3.2", + "vite": "^5.4.12", + "vue": "^3.4.21" + }, + "dependencies": { + "@vee-validate/i18n": "^4.9.1", + "@vee-validate/rules": "^4.9.1", + "@vitejs/plugin-vue": "^4.2.3", + "chartjs-chart-funnel": "^4.2.1", + "dompurify": "^3.1.7", + "dotenv": "^16.4.7", + "fast-csv": "^5.0.5", + "flatpickr": "^4.6.13", + "mitt": "^3.0.1", + "playwright": "^1.48.1", + "readline-sync": "^1.4.10", + "vee-validate": "^4.9.1", + "vue-cal": "^4.9.0", + "vue-flatpickr": "^2.3.0", + "vuedraggable": "^4.1.0" + } +} diff --git a/packages/Webkul/Admin/postcss.config.cjs b/packages/Webkul/Admin/postcss.config.cjs new file mode 100644 index 0000000..818c656 --- /dev/null +++ b/packages/Webkul/Admin/postcss.config.cjs @@ -0,0 +1,3 @@ +module.exports = ({ env }) => ({ + plugins: [require("tailwindcss")(), require("autoprefixer")()], +}); diff --git a/packages/Webkul/Admin/src/Bouncer.php b/packages/Webkul/Admin/src/Bouncer.php new file mode 100755 index 0000000..b53610b --- /dev/null +++ b/packages/Webkul/Admin/src/Bouncer.php @@ -0,0 +1,60 @@ +guard('user')->check() && auth()->guard('user')->user()->role->permission_type == 'all') { + return true; + } else { + if (! auth()->guard('user')->check() || ! auth()->guard('user')->user()->hasPermission($permission)) { + return false; + } + } + + return true; + } + + /** + * Checks if user allowed or not for certain action + * + * @param string $permission + * @return void + */ + public static function allow($permission) + { + if (! auth()->guard('user')->check() || ! auth()->guard('user')->user()->hasPermission($permission)) { + abort(401, 'This action is unauthorized'); + } + } + + /** + * This function will return user ids of current user's groups + * + * @return array|null + */ + public function getAuthorizedUserIds() + { + $user = auth()->guard('user')->user(); + + if ($user->view_permission == 'global') { + return null; + } + + if ($user->view_permission == 'group') { + return app(UserRepository::class)->getCurrentUserGroupsUserIds(); + } else { + return [$user->id]; + } + } +} diff --git a/packages/Webkul/Admin/src/Config/acl.php b/packages/Webkul/Admin/src/Config/acl.php new file mode 100644 index 0000000..60cefe9 --- /dev/null +++ b/packages/Webkul/Admin/src/Config/acl.php @@ -0,0 +1,590 @@ + 'dashboard', + 'name' => 'admin::app.layouts.dashboard', + 'route' => 'admin.dashboard.index', + 'sort' => 1, + ], [ + 'key' => 'leads', + 'name' => 'admin::app.acl.leads', + 'route' => 'admin.leads.index', + 'sort' => 2, + ], [ + 'key' => 'leads.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.leads.create', 'admin.leads.store'], + 'sort' => 1, + ], [ + 'key' => 'leads.create.quick-create', + 'name' => 'admin::app.acl.quick_add', + 'route' => ['admin.leads.create', 'admin.leads.store'], + 'sort' => 1, + ], [ + 'key' => 'leads.view', + 'name' => 'admin::app.acl.view', + 'route' => 'admin.leads.view', + 'sort' => 2, + ], [ + 'key' => 'leads.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.leads.edit', 'admin.leads.update', 'admin.leads.mass_update'], + 'sort' => 3, + ], [ + 'key' => 'leads.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.leads.delete', 'admin.leads.mass_delete'], + 'sort' => 4, + ], [ + 'key' => 'quotes', + 'name' => 'admin::app.acl.quotes', + 'route' => 'admin.quotes.index', + 'sort' => 3, + ], [ + 'key' => 'quotes.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.quotes.create', 'admin.quotes.store'], + 'sort' => 1, + ], [ + 'key' => 'quotes.mail', + 'name' => 'admin::app.acl.mail', + 'route' => ['admin.quotes.mail', 'admin.leads.quotes.mail'], + 'sort' => 2, + ], [ + 'key' => 'quotes.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.quotes.edit', 'admin.quotes.update'], + 'sort' => 2, + ], [ + 'key' => 'quotes.print', + 'name' => 'admin::app.acl.print', + 'route' => 'admin.quotes.print', + 'sort' => 3, + ], [ + 'key' => 'quotes.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.quotes.delete', 'admin.quotes.mass_delete'], + 'sort' => 4, + ], [ + 'key' => 'mail', + 'name' => 'admin::app.acl.mail', + 'route' => 'admin.mail.index', + 'sort' => 4, + ], [ + 'key' => 'mail.inbox', + 'name' => 'admin::app.acl.inbox', + 'route' => 'admin.mail.index', + 'sort' => 1, + ], [ + 'key' => 'mail.draft', + 'name' => 'admin::app.acl.draft', + 'route' => 'admin.mail.index', + 'sort' => 2, + ], [ + 'key' => 'mail.outbox', + 'name' => 'admin::app.acl.outbox', + 'route' => 'admin.mail.index', + 'sort' => 3, + ], [ + 'key' => 'mail.sent', + 'name' => 'admin::app.acl.sent', + 'route' => 'admin.mail.index', + 'sort' => 4, + ], [ + 'key' => 'mail.trash', + 'name' => 'admin::app.acl.trash', + 'route' => 'admin.mail.index', + 'sort' => 5, + ], [ + 'key' => 'mail.compose', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.mail.store'], + 'sort' => 6, + ], [ + 'key' => 'mail.compose.quick-create', + 'name' => 'admin::app.acl.quick_add', + 'route' => ['admin.mail.store'], + 'sort' => 6, + ], [ + 'key' => 'mail.view', + 'name' => 'admin::app.acl.view', + 'route' => ['admin.mail.view', 'admin.mail.attachment_download'], + 'sort' => 7, + ], [ + 'key' => 'mail.edit', + 'name' => 'admin::app.acl.edit', + 'route' => 'admin.mail.update', + 'sort' => 8, + ], [ + 'key' => 'mail.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.mail.delete', 'admin.mail.mass_delete'], + 'sort' => 9, + ], [ + 'key' => 'activities', + 'name' => 'admin::app.acl.activities', + 'route' => 'admin.activities.index', + 'sort' => 5, + ], [ + 'key' => 'activities.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.activities.create', 'admin.activities.store'], + 'sort' => 1, + ], [ + 'key' => 'activities.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.activities.edit', 'admin.activities.update', 'admin.activities.mass_update'], + 'sort' => 2, + ], [ + 'key' => 'activities.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.activities.delete', 'admin.activities.mass_delete'], + 'sort' => 3, + ], [ + 'key' => 'contacts', + 'name' => 'admin::app.acl.contacts', + 'route' => 'admin.contacts.users.index', + 'sort' => 6, + ], [ + 'key' => 'contacts.persons', + 'name' => 'admin::app.acl.persons', + 'route' => 'admin.contacts.persons.index', + 'sort' => 1, + ], [ + 'key' => 'contacts.persons.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.contacts.persons.create', 'admin.contacts.persons.store'], + 'sort' => 2, + ], [ + 'key' => 'contacts.persons.create.quick-create', + 'name' => 'admin::app.acl.quick_add', + 'route' => ['admin.contacts.persons.create', 'admin.contacts.persons.store'], + 'sort' => 2, + ], [ + 'key' => 'contacts.persons.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.contacts.persons.edit', 'admin.contacts.persons.update'], + 'sort' => 3, + ], [ + 'key' => 'contacts.persons.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.contacts.persons.delete', 'admin.contacts.persons.mass_delete'], + 'sort' => 4, + ], [ + 'key' => 'contacts.persons.export_google', + 'name' => 'admin::app.acl.export', + 'route' => ['admin.contacts.persons.google_export.store', 'admin.contacts.persons.google_export.stats'], + 'sort' => 6, + ], [ + 'key' => 'contacts.persons.view', + 'name' => 'admin::app.acl.view', + 'route' => 'admin.contacts.persons.view', + 'sort' => 5, + ], [ + 'key' => 'contacts.organizations', + 'name' => 'admin::app.acl.organizations', + 'route' => 'admin.contacts.organizations.index', + 'sort' => 2, + ], [ + 'key' => 'contacts.organizations.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.contacts.organizations.create', 'admin.contacts.organizations.store'], + 'sort' => 1, + ], [ + 'key' => 'contacts.organizations.create.quick-create', + 'name' => 'admin::app.acl.quick_add', + 'route' => ['admin.contacts.organizations.create', 'admin.contacts.organizations.store'], + 'sort' => 1, + ], [ + 'key' => 'contacts.organizations.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.contacts.organizations.edit', 'admin.contacts.organizations.update'], + 'sort' => 2, + ], [ + 'key' => 'contacts.organizations.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.contacts.organizations.delete', 'admin.contacts.organizations.mass_delete'], + 'sort' => 3, + ], [ + 'key' => 'products', + 'name' => 'admin::app.acl.products', + 'route' => 'admin.products.index', + 'sort' => 7, + ], [ + 'key' => 'products.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.products.create', 'admin.products.store'], + 'sort' => 1, + ], [ + 'key' => 'products.create.quick-create', + 'name' => 'admin::app.acl.quick_add', + 'route' => ['admin.products.create', 'admin.products.store'], + 'sort' => 1, + ], [ + 'key' => 'products.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.products.edit', 'admin.products.update'], + 'sort' => 2, + ], [ + 'key' => 'products.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.products.delete', 'admin.products.mass_delete'], + 'sort' => 3, + ], [ + 'key' => 'products.view', + 'name' => 'admin::app.acl.view', + 'route' => 'admin.products.view', + 'sort' => 3, + ], [ + 'key' => 'settings', + 'name' => 'admin::app.acl.settings', + 'route' => 'admin.settings.index', + 'sort' => 8, + ], [ + 'key' => 'settings.user', + 'name' => 'admin::app.acl.user', + 'route' => ['admin.settings.groups.index', 'admin.settings.roles.index', 'admin.settings.users.index'], + 'sort' => 1, + ], [ + 'key' => 'settings.user.groups', + 'name' => 'admin::app.acl.groups', + 'route' => 'admin.settings.groups.index', + 'sort' => 1, + ], [ + 'key' => 'settings.user.groups.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.groups.create', 'admin.settings.groups.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.user.groups.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.groups.edit', 'admin.settings.groups.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.user.groups.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.groups.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.user.roles', + 'name' => 'admin::app.acl.roles', + 'route' => 'admin.settings.roles.index', + 'sort' => 2, + ], [ + 'key' => 'settings.user.roles.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.roles.create', 'admin.settings.roles.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.user.roles.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.roles.edit', 'admin.settings.roles.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.user.roles.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.roles.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.user.users', + 'name' => 'admin::app.acl.users', + 'route' => 'admin.settings.users.index', + 'sort' => 3, + ], [ + 'key' => 'settings.user.users.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.users.create', 'admin.settings.users.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.user.users.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.users.edit', 'admin.settings.users.update', 'admin.settings.users.mass_update'], + 'sort' => 2, + ], [ + 'key' => 'settings.user.users.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.settings.users.delete', 'admin.settings.users.mass_delete'], + 'sort' => 3, + ], [ + 'key' => 'settings.lead', + 'name' => 'admin::app.acl.lead', + 'route' => ['admin.settings.pipelines.index', 'admin.settings.sources.index', 'admin.settings.types.index'], + 'sort' => 2, + ], [ + 'key' => 'settings.lead.pipelines', + 'name' => 'admin::app.acl.pipelines', + 'route' => 'admin.settings.pipelines.index', + 'sort' => 1, + ], [ + 'key' => 'settings.lead.pipelines.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.pipelines.create', 'admin.settings.pipelines.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.lead.pipelines.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.pipelines.edit', 'admin.settings.pipelines.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.lead.pipelines.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.pipelines.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.lead.sources', + 'name' => 'admin::app.acl.sources', + 'route' => 'admin.settings.sources.index', + 'sort' => 2, + ], [ + 'key' => 'settings.lead.sources.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.sources.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.lead.sources.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.sources.edit', 'admin.settings.sources.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.lead.sources.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.sources.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.lead.types', + 'name' => 'admin::app.acl.types', + 'route' => 'admin.settings.types.index', + 'sort' => 3, + ], [ + 'key' => 'settings.lead.types.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.types.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.lead.types.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.types.edit', 'admin.settings.types.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.lead.types.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.types.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.inventory', + 'name' => 'admin::app.acl.inventory', + 'route' => ['admin.settings.warehouses.index'], + 'sort' => 3, + ], [ + 'key' => 'settings.inventory.warehouse', + 'name' => 'admin::app.acl.warehouses', + 'route' => ['admin.settings.warehouses.index'], + 'sort' => 1, + ], [ + 'key' => 'settings.inventory.warehouse.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.warehouses.create', 'admin.settings.warehouses.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.inventory.warehouse.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.warehouses.edit', 'admin.settings.warehouses.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.inventory.warehouse.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.warehouses.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.automation', + 'name' => 'admin::app.acl.automation', + 'route' => ['admin.settings.attributes.index', 'admin.settings.email_templates.index', 'admin.settings.workflows.index'], + 'sort' => 4, + ], [ + 'key' => 'settings.automation.attributes', + 'name' => 'admin::app.acl.attributes', + 'route' => 'admin.settings.attributes.index', + 'sort' => 1, + ], [ + 'key' => 'settings.automation.attributes.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.attributes.create', 'admin.settings.attributes.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.automation.attributes.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.attributes.edit', 'admin.settings.attributes.update', 'admin.settings.attributes.mass_update'], + 'sort' => 2, + ], [ + 'key' => 'settings.automation.attributes.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.attributes.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.automation.email_templates', + 'name' => 'admin::app.acl.email-templates', + 'route' => 'admin.settings.email_templates.index', + 'sort' => 2, + ], [ + 'key' => 'settings.automation.email_templates.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.email_templates.create', 'admin.settings.email_templates.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.automation.email_templates.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.email_templates.edit', 'admin.settings.email_templates.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.automation.email_templates.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.email_templates.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.automation.workflows', + 'name' => 'admin::app.acl.workflows', + 'route' => 'admin.settings.workflows.index', + 'sort' => 3, + ], [ + 'key' => 'settings.automation.workflows.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.workflows.create', 'admin.settings.workflows.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.automation.workflows.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.workflows.edit', 'admin.settings.workflows.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.automation.workflows.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.workflows.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.automation.events', + 'name' => 'admin::app.acl.event', + 'route' => 'admin.settings.marketing.events.index', + 'sort' => 4, + ], [ + 'key' => 'settings.automation.events.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.marketing.events.create', 'admin.settings.marketing.events.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.automation.events.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.marketing.events.edit', 'admin.settings.marketing.events.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.automation.events.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.settings.marketing.events.delete', 'admin.settings.marketing.events.mass_delete'], + 'sort' => 3, + ], [ + 'key' => 'settings.automation.campaigns', + 'name' => 'admin::app.acl.campaigns', + 'route' => 'admin.settings.marketing.campaigns.index', + 'sort' => 5, + ], [ + 'key' => 'settings.automation.campaigns.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.marketing.campaigns.create', 'admin.settings.marketing.campaigns.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.automation.campaigns.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.marketing.campaigns.edit', 'admin.settings.marketing.campaigns.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.automation.campaigns.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.settings.marketing.campaigns.delete', 'admin.settings.marketing.campaigns.mass_delete'], + 'sort' => 3, + ], [ + 'key' => 'settings.automation.webhooks', + 'name' => 'admin::app.acl.webhook', + 'route' => 'admin.settings.webhooks.index', + 'sort' => 6, + ], [ + 'key' => 'settings.automation.webhooks.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.webhooks.create', 'admin.settings.webhooks.store'], + 'sort' => 1, + ], [ + 'key' => 'settings.automation.webhooks.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.webhooks.edit', 'admin.settings.webhooks.update'], + 'sort' => 2, + ], [ + 'key' => 'settings.automation.webhooks.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.webhooks.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.automation.data_transfer', + 'name' => 'admin::app.acl.data-transfer', + 'route' => 'admin.settings.data_transfer.imports.index', + 'sort' => 7, + ], [ + 'key' => 'settings.automation.data_transfer.imports', + 'name' => 'admin::app.acl.imports', + 'route' => 'admin.settings.data_transfer.imports.index', + 'sort' => 1, + ], [ + 'key' => 'settings.automation.data_transfer.imports.create', + 'name' => 'admin::app.acl.create', + 'route' => 'admin.settings.data_transfer.imports.create', + 'sort' => 1, + ], [ + 'key' => 'settings.automation.data_transfer.imports.edit', + 'name' => 'admin::app.acl.edit', + 'route' => 'admin.settings.data_transfer.imports.edit', + 'sort' => 2, + ], [ + 'key' => 'settings.automation.data_transfer.imports.delete', + 'name' => 'admin::app.acl.delete', + 'route' => 'admin.settings.data_transfer.imports.delete', + 'sort' => 3, + ], [ + 'key' => 'settings.automation.data_transfer.imports.import', + 'name' => 'admin::app.acl.import', + 'route' => 'admin.settings.data_transfer.imports.imports', + 'sort' => 4, + ], [ + 'key' => 'settings.other_settings', + 'name' => 'admin::app.acl.other-settings', + 'route' => 'admin.settings.tags.index', + 'sort' => 5, + ], [ + 'key' => 'settings.other_settings.tags', + 'name' => 'admin::app.acl.tags', + 'route' => 'admin.settings.tags.index', + 'sort' => 1, + ], [ + 'key' => 'settings.other_settings.tags.create', + 'name' => 'admin::app.acl.create', + 'route' => ['admin.settings.tags.create', 'admin.settings.tags.store', 'admin.leads.tags.attach'], + 'sort' => 1, + ], [ + 'key' => 'settings.other_settings.tags.edit', + 'name' => 'admin::app.acl.edit', + 'route' => ['admin.settings.tags.edit', 'admin.settings.tags.update'], + 'sort' => 1, + ], [ + 'key' => 'settings.other_settings.tags.delete', + 'name' => 'admin::app.acl.delete', + 'route' => ['admin.settings.tags.delete', 'admin.settings.tags.mass_delete', 'admin.leads.tags.detach'], + 'sort' => 2, + ], [ + 'key' => 'configuration', + 'name' => 'admin::app.acl.configuration', + 'route' => 'admin.configuration.index', + 'sort' => 9, + ], [ + 'key' => 'help', + 'name' => 'admin::app.acl.help', + 'route' => 'admin.help.index', + 'sort' => 10, + ], +]; diff --git a/packages/Webkul/Admin/src/Config/attribute_entity_types.php b/packages/Webkul/Admin/src/Config/attribute_entity_types.php new file mode 100644 index 0000000..0b178bb --- /dev/null +++ b/packages/Webkul/Admin/src/Config/attribute_entity_types.php @@ -0,0 +1,33 @@ + [ + 'name' => 'admin::app.leads.index.title', + 'repository' => 'Webkul\Lead\Repositories\LeadRepository', + ], + + 'persons' => [ + 'name' => 'admin::app.contacts.persons.index.title', + 'repository' => 'Webkul\Contact\Repositories\PersonRepository', + ], + + 'organizations' => [ + 'name' => 'admin::app.contacts.organizations.index.title', + 'repository' => 'Webkul\Contact\Repositories\OrganizationRepository', + ], + + 'products' => [ + 'name' => 'admin::app.products.index.title', + 'repository' => 'Webkul\Product\Repositories\ProductRepository', + ], + + 'quotes' => [ + 'name' => 'admin::app.quotes.index.title', + 'repository' => 'Webkul\Quote\Repositories\QuoteRepository', + ], + + 'warehouses' => [ + 'name' => 'admin::app.settings.warehouses.index.title', + 'repository' => 'Webkul\Warehouse\Repositories\WarehouseRepository', + ], +]; diff --git a/packages/Webkul/Admin/src/Config/attribute_lookups.php b/packages/Webkul/Admin/src/Config/attribute_lookups.php new file mode 100644 index 0000000..338948b --- /dev/null +++ b/packages/Webkul/Admin/src/Config/attribute_lookups.php @@ -0,0 +1,54 @@ + [ + 'name' => 'Leads', + 'repository' => 'Webkul\Lead\Repositories\LeadRepository', + 'label_column' => 'title', + ], + + 'lead_sources' => [ + 'name' => 'Lead Sources', + 'repository' => 'Webkul\Lead\Repositories\SourceRepository', + ], + + 'lead_types' => [ + 'name' => 'Lead Types', + 'repository' => 'Webkul\Lead\Repositories\TypeRepository', + ], + + 'lead_pipelines' => [ + 'name' => 'Lead Pipelines', + 'repository' => 'Webkul\Lead\Repositories\PipelineRepository', + ], + + 'lead_pipeline_stages' => [ + 'name' => 'Lead Pipeline Stages', + 'repository' => 'Webkul\Lead\Repositories\StageRepository', + ], + + 'users' => [ + 'name' => 'Sales Owners', + 'repository' => 'Webkul\User\Repositories\UserRepository', + ], + + 'organizations' => [ + 'name' => 'Organizations', + 'repository' => 'Webkul\Contact\Repositories\OrganizationRepository', + ], + + 'persons' => [ + 'name' => 'Persons', + 'repository' => 'Webkul\Contact\Repositories\PersonRepository', + ], + + 'warehouses' => [ + 'name' => 'Warehouses', + 'repository' => 'Webkul\Warehouse\Repositories\WarehouseRepository', + ], + + 'locations' => [ + 'name' => 'Locations', + 'repository' => 'Webkul\Warehouse\Repositories\LocationRepository', + ], +]; diff --git a/packages/Webkul/Admin/src/Config/core_config.php b/packages/Webkul/Admin/src/Config/core_config.php new file mode 100644 index 0000000..eda8abd --- /dev/null +++ b/packages/Webkul/Admin/src/Config/core_config.php @@ -0,0 +1,353 @@ + 'general', + 'name' => 'admin::app.configuration.index.general.title', + 'info' => 'admin::app.configuration.index.general.info', + 'sort' => 1, + ], [ + 'key' => 'general.general', + 'name' => 'admin::app.configuration.index.general.general.title', + 'info' => 'admin::app.configuration.index.general.general.info', + 'icon' => 'icon-setting', + 'sort' => 1, + ], [ + 'key' => 'general.general.locale_settings', + 'name' => 'admin::app.configuration.index.general.general.locale-settings.title', + 'info' => 'admin::app.configuration.index.general.general.locale-settings.title-info', + 'sort' => 1, + 'fields' => [ + [ + 'name' => 'locale', + 'title' => 'admin::app.configuration.index.general.general.locale-settings.title', + 'type' => 'select', + 'default' => 'en', + 'options' => 'Webkul\Core\Core@locales', + ], + ], + ], [ + 'key' => 'general.general.admin_logo', + 'name' => 'admin::app.configuration.index.general.general.admin-logo.title', + 'info' => 'admin::app.configuration.index.general.general.admin-logo.title-info', + 'sort' => 2, + 'fields' => [ + [ + 'name' => 'logo_image', + 'title' => 'admin::app.configuration.index.general.general.admin-logo.logo-image', + 'type' => 'image', + 'validation' => 'mimes:bmp,jpeg,jpg,png,webp', + ], [ + 'name' => 'favicon_image', + 'title' => 'admin::app.configuration.index.general.general.admin-logo.favicon-image', + 'type' => 'image', + 'validation' => 'mimes:ico,jpeg,jpg,png,webp', + ], + ], + ], [ + 'key' => 'general.settings', + 'name' => 'admin::app.configuration.index.general.settings.title', + 'info' => 'admin::app.configuration.index.general.settings.info', + 'icon' => 'icon-configuration', + 'sort' => 2, + ], [ + 'key' => 'general.settings.footer', + 'name' => 'admin::app.configuration.index.general.settings.footer.title', + 'info' => 'admin::app.configuration.index.general.settings.footer.info', + 'sort' => 1, + 'fields' => [ + [ + 'name' => 'show', + 'title' => 'admin::app.configuration.index.general.settings.footer.show', + 'info' => 'admin::app.configuration.index.general.settings.footer.show-info', + 'type' => 'boolean', + 'default' => 1, + ], [ + 'name' => 'label', + 'title' => 'admin::app.configuration.index.general.settings.footer.powered-by', + 'type' => 'editor', + 'depends' => 'show:1', + 'default' => 'Powered by Krayin, an open-source project by Webkul.', + 'tinymce' => true, + ], + ], + ], [ + 'key' => 'general.settings.menu', + 'name' => 'admin::app.configuration.index.general.settings.menu.title', + 'info' => 'admin::app.configuration.index.general.settings.menu.info', + 'sort' => 2, + 'fields' => [ + [ + 'name' => 'dashboard', + 'title' => 'admin::app.configuration.index.general.settings.menu.dashboard', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'leads', + 'title' => 'admin::app.configuration.index.general.settings.menu.leads', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'quotes', + 'title' => 'admin::app.configuration.index.general.settings.menu.quotes', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'mail', + 'title' => 'admin::app.configuration.index.general.settings.menu.mail', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'mail.inbox', + 'title' => 'admin::app.configuration.index.general.settings.menu.inbox', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'mail.draft', + 'title' => 'admin::app.configuration.index.general.settings.menu.draft', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'mail.outbox', + 'title' => 'admin::app.configuration.index.general.settings.menu.outbox', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'mail.sent', + 'title' => 'admin::app.configuration.index.general.settings.menu.sent', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'mail.trash', + 'title' => 'admin::app.configuration.index.general.settings.menu.trash', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'activities', + 'title' => 'admin::app.configuration.index.general.settings.menu.activities', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'contacts', + 'title' => 'admin::app.configuration.index.general.settings.menu.contacts', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'contacts.persons', + 'title' => 'admin::app.configuration.index.general.settings.menu.persons', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'contacts.organizations', + 'title' => 'admin::app.configuration.index.general.settings.menu.organizations', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'products', + 'title' => 'admin::app.configuration.index.general.settings.menu.products', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'settings', + 'title' => 'admin::app.configuration.index.general.settings.menu.settings', + 'type' => 'text', + 'validation' => 'max:20', + ], [ + 'name' => 'configuration', + 'title' => 'admin::app.configuration.index.general.settings.menu.configuration', + 'type' => 'text', + 'validation' => 'max:20', + ], + ], + ], [ + 'key' => 'general.settings.menu_color', + 'name' => 'admin::app.configuration.index.general.settings.menu-color.title', + 'info' => 'admin::app.configuration.index.general.settings.menu-color.info', + 'sort' => 3, + 'fields' => [ + [ + 'name' => 'brand_color', + 'title' => 'admin::app.configuration.index.general.settings.menu-color.brand-color', + 'type' => 'color', + 'default' => '#0E90D9', + ], + ], + ], [ + 'key' => 'general.settings.dashboard', + 'name' => 'admin::app.configuration.index.general.settings.dashboard.title', + 'info' => 'admin::app.configuration.index.general.settings.dashboard.info', + 'sort' => 4, + 'fields' => [ + [ + 'name' => 'date_range', + 'title' => 'admin::app.configuration.index.general.settings.dashboard.date-range', + 'info' => 'admin::app.configuration.index.general.settings.dashboard.date-range-info', + 'type' => 'select', + 'default' => '1_month', + 'options' => [ + [ + 'title' => 'admin::app.configuration.index.general.settings.dashboard.ranges.1-month', + 'value' => '1_month', + ], [ + 'title' => 'admin::app.configuration.index.general.settings.dashboard.ranges.3-months', + 'value' => '3_months', + ], [ + 'title' => 'admin::app.configuration.index.general.settings.dashboard.ranges.9-months', + 'value' => '9_months', + ], [ + 'title' => 'admin::app.configuration.index.general.settings.dashboard.ranges.1-year', + 'value' => '1_year', + ], [ + 'title' => 'admin::app.configuration.index.general.settings.dashboard.ranges.2-years', + 'value' => '2_years', + ], [ + 'title' => 'admin::app.configuration.index.general.settings.dashboard.ranges.custom', + 'value' => 'custom', + ], + ], + ], [ + 'name' => 'custom_days', + 'title' => 'admin::app.configuration.index.general.settings.dashboard.custom-days', + 'info' => 'admin::app.configuration.index.general.settings.dashboard.custom-days-info', + 'type' => 'number', + 'default' => 30, + 'depends' => 'date_range:custom', + 'validation' => 'required_if:date_range,custom', + ], + ], + ], [ + 'key' => 'general.magic_ai', + 'name' => 'admin::app.configuration.index.magic-ai.title', + 'info' => 'admin::app.configuration.index.magic-ai.info', + 'icon' => 'icon-setting', + 'sort' => 3, + ], [ + 'key' => 'general.magic_ai.settings', + 'name' => 'admin::app.configuration.index.magic-ai.settings.title', + 'info' => 'admin::app.configuration.index.magic-ai.settings.info', + 'sort' => 1, + 'fields' => [ + [ + 'name' => 'enable', + 'title' => 'admin::app.configuration.index.magic-ai.settings.enable', + 'type' => 'boolean', + 'channel_based' => true, + ], [ + 'name' => 'api_key', + 'title' => 'admin::app.configuration.index.magic-ai.settings.api-key', + 'type' => 'password', + 'depends' => 'enable:1', + 'validation' => 'required_if:enable,1', + 'info' => 'admin::app.configuration.index.magic-ai.settings.api-key-info', + ], [ + 'name' => 'model', + 'title' => 'admin::app.configuration.index.magic-ai.settings.models.title', + 'type' => 'select', + 'channel_based' => true, + 'depends' => 'enable:1', + 'options' => [ + [ + 'title' => 'admin::app.configuration.index.magic-ai.settings.models.gpt-4o', + 'value' => 'openai/chatgpt-4o-latest', + ], [ + 'title' => 'admin::app.configuration.index.magic-ai.settings.models.gpt-4o-mini', + 'value' => 'openai/gpt-4o-mini', + ], [ + 'title' => 'admin::app.configuration.index.magic-ai.settings.models.gemini-2-0-flash-001', + 'value' => 'google/gemini-2.0-flash-001', + ], [ + 'title' => 'admin::app.configuration.index.magic-ai.settings.models.deepseek-r1', + 'value' => 'deepseek/deepseek-r1-distill-llama-8b', + ], [ + 'title' => 'admin::app.configuration.index.magic-ai.settings.models.llama-3-2-3b-instruct', + 'value' => 'meta-llama/llama-3.2-3b-instruct', + ], [ + 'title' => 'admin::app.configuration.index.magic-ai.settings.models.grok-2-1212', + 'value' => 'x-ai/grok-2-1212', + ], + ], + ], [ + 'name' => 'other_model', + 'title' => 'admin::app.configuration.index.magic-ai.settings.other', + 'type' => 'text', + 'info' => 'admin::app.configuration.index.magic-ai.settings.other-model', + 'default' => null, + 'depends' => 'enable:1', + ], + ], + ], [ + 'key' => 'general.magic_ai.doc_generation', + 'name' => 'admin::app.configuration.index.magic-ai.settings.doc-generation', + 'info' => 'admin::app.configuration.index.magic-ai.settings.doc-generation-info', + 'sort' => 2, + 'fields' => [ + [ + 'name' => 'enabled', + 'title' => 'admin::app.configuration.index.magic-ai.settings.enable', + 'type' => 'boolean', + ], + ], + ], + + /** + * Email. + */ + [ + 'key' => 'email', + 'name' => 'admin::app.configuration.index.email.title', + 'info' => 'admin::app.configuration.index.email.info', + 'sort' => 2, + ], [ + 'key' => 'email.imap', + 'name' => 'admin::app.configuration.index.email.imap.title', + 'info' => 'admin::app.configuration.index.email.imap.info', + 'icon' => 'icon-setting', + 'sort' => 1, + ], [ + 'key' => 'email.imap.account', + 'name' => 'admin::app.configuration.index.email.imap.account.title', + 'info' => 'admin::app.configuration.index.email.imap.account.title-info', + 'sort' => 1, + 'fields' => [ + [ + 'name' => 'host', + 'title' => 'admin::app.configuration.index.email.imap.account.host', + 'type' => 'text', + 'default' => config('imap.accounts.default.host'), + ], + [ + 'name' => 'port', + 'title' => 'admin::app.configuration.index.email.imap.account.port', + 'type' => 'text', + 'default' => config('imap.accounts.default.port'), + ], + [ + 'name' => 'encryption', + 'title' => 'admin::app.configuration.index.email.imap.account.encryption', + 'type' => 'text', + 'default' => config('imap.accounts.default.encryption'), + ], + [ + 'name' => 'validate_cert', + 'title' => 'admin::app.configuration.index.email.imap.account.validate-cert', + 'type' => 'boolean', + 'default' => config('imap.accounts.default.validate_cert'), + ], + [ + 'name' => 'username', + 'title' => 'admin::app.configuration.index.email.imap.account.username', + 'type' => 'text', + 'default' => config('imap.accounts.default.username'), + ], + [ + 'name' => 'password', + 'title' => 'admin::app.configuration.index.email.imap.account.password', + 'type' => 'password', + 'default' => config('imap.accounts.default.password'), + ], + ], + ], +]; diff --git a/packages/Webkul/Admin/src/Config/menu.php b/packages/Webkul/Admin/src/Config/menu.php new file mode 100644 index 0000000..be552b9 --- /dev/null +++ b/packages/Webkul/Admin/src/Config/menu.php @@ -0,0 +1,301 @@ + 'dashboard', + 'name' => 'admin::app.layouts.dashboard', + 'route' => 'admin.dashboard.index', + 'sort' => 1, + 'icon-class' => 'icon-dashboard', + ], + + /** + * Leads. + */ + [ + 'key' => 'leads', + 'name' => 'admin::app.layouts.leads', + 'route' => 'admin.leads.index', + 'sort' => 2, + 'icon-class' => 'icon-leads', + ], + + /** + * Quotes. + */ + [ + 'key' => 'quotes', + 'name' => 'admin::app.layouts.quotes', + 'route' => 'admin.quotes.index', + 'sort' => 3, + 'icon-class' => 'icon-quote', + ], + + /** + * Emails. + */ + [ + 'key' => 'mail', + 'name' => 'admin::app.layouts.mail.title', + 'route' => 'admin.mail.index', + 'params' => ['route' => 'inbox'], + 'sort' => 4, + 'icon-class' => 'icon-mail', + ], [ + 'key' => 'mail.inbox', + 'name' => 'admin::app.layouts.mail.inbox', + 'route' => 'admin.mail.index', + 'params' => ['route' => 'inbox'], + 'sort' => 2, + 'icon-class' => '', + ], [ + 'key' => 'mail.draft', + 'name' => 'admin::app.layouts.mail.draft', + 'route' => 'admin.mail.index', + 'params' => ['route' => 'draft'], + 'sort' => 3, + 'icon-class' => '', + ], [ + 'key' => 'mail.outbox', + 'name' => 'admin::app.layouts.mail.outbox', + 'route' => 'admin.mail.index', + 'params' => ['route' => 'outbox'], + 'sort' => 4, + 'icon-class' => '', + ], [ + 'key' => 'mail.sent', + 'name' => 'admin::app.layouts.mail.sent', + 'route' => 'admin.mail.index', + 'params' => ['route' => 'sent'], + 'sort' => 4, + 'icon-class' => '', + ], [ + 'key' => 'mail.trash', + 'name' => 'admin::app.layouts.mail.trash', + 'route' => 'admin.mail.index', + 'params' => ['route' => 'trash'], + 'sort' => 5, + 'icon-class' => '', + ], + + /** + * Activities. + */ + [ + 'key' => 'activities', + 'name' => 'admin::app.layouts.activities', + 'route' => 'admin.activities.index', + 'sort' => 5, + 'icon-class' => 'icon-activity', + ], + + /** + * Contacts. + */ + [ + 'key' => 'contacts', + 'name' => 'admin::app.layouts.contacts', + 'route' => 'admin.contacts.persons.index', + 'sort' => 6, + 'icon-class' => 'icon-contact', + ], [ + 'key' => 'contacts.persons', + 'name' => 'admin::app.layouts.persons', + 'route' => 'admin.contacts.persons.index', + 'sort' => 1, + 'icon-class' => '', + ], [ + 'key' => 'contacts.organizations', + 'name' => 'admin::app.layouts.organizations', + 'route' => 'admin.contacts.organizations.index', + 'sort' => 2, + 'icon-class' => '', + ], + + /** + * Products. + */ + [ + 'key' => 'products', + 'name' => 'admin::app.layouts.products', + 'route' => 'admin.products.index', + 'sort' => 7, + 'icon-class' => 'icon-product', + ], + + /** + * Settings. + */ + [ + 'key' => 'settings', + 'name' => 'admin::app.layouts.settings', + 'route' => 'admin.settings.index', + 'sort' => 8, + 'icon-class' => 'icon-setting', + ], [ + 'key' => 'settings.user', + 'name' => 'admin::app.layouts.user', + 'route' => 'admin.settings.groups.index', + 'info' => 'admin::app.layouts.user-info', + 'sort' => 1, + 'icon-class' => 'icon-settings-group', + ], [ + 'key' => 'settings.user.groups', + 'name' => 'admin::app.layouts.groups', + 'info' => 'admin::app.layouts.groups-info', + 'route' => 'admin.settings.groups.index', + 'sort' => 1, + 'icon-class' => 'icon-settings-group', + ], [ + 'key' => 'settings.user.roles', + 'name' => 'admin::app.layouts.roles', + 'info' => 'admin::app.layouts.roles-info', + 'route' => 'admin.settings.roles.index', + 'sort' => 2, + 'icon-class' => 'icon-role', + ], [ + 'key' => 'settings.user.users', + 'name' => 'admin::app.layouts.users', + 'info' => 'admin::app.layouts.users-info', + 'route' => 'admin.settings.users.index', + 'sort' => 3, + 'icon-class' => 'icon-user', + ], [ + 'key' => 'settings.lead', + 'name' => 'admin::app.layouts.lead', + 'info' => 'admin::app.layouts.lead-info', + 'route' => 'admin.settings.pipelines.index', + 'sort' => 2, + 'icon-class' => '', + ], [ + 'key' => 'settings.lead.pipelines', + 'name' => 'admin::app.layouts.pipelines', + 'info' => 'admin::app.layouts.pipelines-info', + 'route' => 'admin.settings.pipelines.index', + 'sort' => 1, + 'icon-class' => 'icon-settings-pipeline', + ], [ + 'key' => 'settings.lead.sources', + 'name' => 'admin::app.layouts.sources', + 'info' => 'admin::app.layouts.sources-info', + 'route' => 'admin.settings.sources.index', + 'sort' => 2, + 'icon-class' => 'icon-settings-sources', + ], [ + 'key' => 'settings.lead.types', + 'name' => 'admin::app.layouts.types', + 'info' => 'admin::app.layouts.types-info', + 'route' => 'admin.settings.types.index', + 'sort' => 3, + 'icon-class' => 'icon-settings-type', + ], [ + 'key' => 'settings.inventory', + 'name' => 'admin::app.layouts.inventory', + 'info' => 'admin::app.layouts.inventory-info', + 'route' => 'admin.settings.pipelines.index', + 'icon-class' => '', + 'sort' => 2, + ], [ + 'key' => 'settings.inventory.warehouse', + 'name' => 'admin::app.layouts.warehouses', + 'info' => 'admin::app.layouts.warehouses-info', + 'route' => 'admin.settings.warehouses.index', + 'sort' => 1, + 'icon-class' => 'icon-settings-warehouse', + ], [ + 'key' => 'settings.automation', + 'name' => 'admin::app.layouts.automation', + 'info' => 'admin::app.layouts.automation-info', + 'route' => 'admin.settings.attributes.index', + 'sort' => 3, + 'icon-class' => '', + ], [ + 'key' => 'settings.automation.attributes', + 'name' => 'admin::app.layouts.attributes', + 'info' => 'admin::app.layouts.attributes-info', + 'route' => 'admin.settings.attributes.index', + 'sort' => 1, + 'icon-class' => 'icon-attribute', + ], [ + 'key' => 'settings.automation.email_templates', + 'name' => 'admin::app.layouts.email-templates', + 'info' => 'admin::app.layouts.email-templates-info', + 'route' => 'admin.settings.email_templates.index', + 'sort' => 2, + 'icon-class' => 'icon-settings-mail', + ], [ + 'key' => 'settings.automation.events', + 'name' => 'admin::app.layouts.events', + 'info' => 'admin::app.layouts.events-info', + 'route' => 'admin.settings.marketing.events.index', + 'sort' => 2, + 'icon-class' => 'icon-calendar', + ], [ + 'key' => 'settings.automation.campaigns', + 'name' => 'admin::app.layouts.campaigns', + 'info' => 'admin::app.layouts.campaigns-info', + 'route' => 'admin.settings.marketing.campaigns.index', + 'sort' => 2, + 'icon-class' => 'icon-note', + ], [ + 'key' => 'settings.automation.webhooks', + 'name' => 'admin::app.layouts.webhooks', + 'info' => 'admin::app.layouts.webhooks-info', + 'route' => 'admin.settings.webhooks.index', + 'sort' => 2, + 'icon-class' => 'icon-settings-webhooks', + ], [ + 'key' => 'settings.automation.workflows', + 'name' => 'admin::app.layouts.workflows', + 'info' => 'admin::app.layouts.workflows-info', + 'route' => 'admin.settings.workflows.index', + 'sort' => 3, + 'icon-class' => 'icon-settings-flow', + ], [ + 'key' => 'settings.automation.data_transfer', + 'name' => 'admin::app.layouts.data_transfer', + 'info' => 'admin::app.layouts.data_transfer_info', + 'route' => 'admin.settings.data_transfer.imports.index', + 'sort' => 4, + 'icon-class' => 'icon-download', + ], [ + 'key' => 'settings.other_settings', + 'name' => 'admin::app.layouts.other-settings', + 'info' => 'admin::app.layouts.other-settings-info', + 'route' => 'admin.settings.tags.index', + 'sort' => 4, + 'icon-class' => 'icon-settings', + ], [ + 'key' => 'settings.other_settings.tags', + 'name' => 'admin::app.layouts.tags', + 'info' => 'admin::app.layouts.tags-info', + 'route' => 'admin.settings.tags.index', + 'sort' => 1, + 'icon-class' => 'icon-settings-tag', + ], + + /** + * Configuration. + */ + [ + 'key' => 'configuration', + 'name' => 'admin::app.layouts.configuration', + 'route' => 'admin.configuration.index', + 'sort' => 9, + 'icon-class' => 'icon-configuration', + ], + + /** + * Help. + */ + [ + 'key' => 'help', + 'name' => 'admin::app.layouts.help', + 'route' => 'admin.help.index', + 'sort' => 10, + 'icon-class' => 'icon-help', + ], +]; diff --git a/packages/Webkul/Admin/src/Config/support.php b/packages/Webkul/Admin/src/Config/support.php new file mode 100644 index 0000000..23dfc04 --- /dev/null +++ b/packages/Webkul/Admin/src/Config/support.php @@ -0,0 +1,70 @@ + [ + [ + 'icon' => 'cloud', + 'title' => 'admin::app.help.index.services.cloud-hosting.title', + 'description' => 'admin::app.help.index.services.cloud-hosting.description', + 'url' => 'https://krayincrm.com/crm-cloud-hosting', + 'url_label' => 'krayincrm.com/cloud-hosting', + ], + [ + 'icon' => 'support', + 'title' => 'admin::app.help.index.services.support.title', + 'description' => 'admin::app.help.index.services.support.description', + 'url' => 'https://krayincrm.com/support', + 'url_label' => 'krayincrm.com/support', + ], + [ + 'icon' => 'extensions', + 'title' => 'admin::app.help.index.services.extensions.title', + 'description' => 'admin::app.help.index.services.extensions.description', + 'url' => 'https://krayincrm.com/extensions/', + 'url_label' => 'krayincrm.com/extensions', + ], + ], + + 'resources' => [ + [ + 'icon' => 'docs', + 'title' => 'admin::app.help.index.resources.user-docs.title', + 'description' => 'admin::app.help.index.resources.user-docs.description', + 'url' => 'https://docs.krayincrm.com/', + 'url_label' => 'docs.krayincrm.com', + ], + [ + 'icon' => 'docs', + 'title' => 'admin::app.help.index.resources.dev-docs.title', + 'description' => 'admin::app.help.index.resources.dev-docs.description', + 'url' => 'https://devdocs.krayincrm.com/', + 'url_label' => 'devdocs.krayincrm.com', + ], + [ + 'icon' => 'api', + 'title' => 'admin::app.help.index.resources.api-docs.title', + 'description' => 'admin::app.help.index.resources.api-docs.description', + 'url' => 'https://devdocs.krayincrm.com/2.2/api/', + 'url_label' => 'devdocs.krayincrm.com/api', + ], + ], + + 'community' => [ + [ + 'icon' => 'community', + 'title' => 'admin::app.help.index.community.forums.title', + 'description' => 'admin::app.help.index.community.forums.description', + 'url' => 'https://forums.krayincrm.com', + 'link_label' => 'admin::app.help.index.community.forums.link', + ], + [ + 'icon' => 'video', + 'title' => 'admin::app.help.index.community.tutorials.title', + 'description' => 'admin::app.help.index.community.tutorials.description', + 'url' => 'https://www.youtube.com/@krayincrm', + 'link_label' => 'admin::app.help.index.community.tutorials.link', + ], + ], + + 'contact_url' => 'https://krayincrm.com/contact', +]; diff --git a/packages/Webkul/Admin/src/DataGrids/Activity/ActivityDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Activity/ActivityDataGrid.php new file mode 100644 index 0000000..904d401 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Activity/ActivityDataGrid.php @@ -0,0 +1,237 @@ +distinct() + ->select( + 'activities.*', + 'leads.id as lead_id', + 'leads.title as lead_title', + 'leads.lead_pipeline_id', + 'users.id as created_by_id', + 'users.name as created_by', + ) + ->leftJoin('activity_participants', 'activities.id', '=', 'activity_participants.activity_id') + ->leftJoin('lead_activities', 'activities.id', '=', 'lead_activities.activity_id') + ->leftJoin('leads', 'lead_activities.lead_id', '=', 'leads.id') + ->leftJoin('users', 'activities.user_id', '=', 'users.id') + ->whereIn('type', ['call', 'meeting', 'lunch']) + ->where(function ($query) { + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $query->whereIn('activities.user_id', $userIds) + ->orWhereIn('activity_participants.user_id', $userIds); + } + })->groupBy('activities.id', 'leads.id', 'users.id'); + + $this->addFilter('id', 'activities.id'); + $this->addFilter('title', 'activities.title'); + $this->addFilter('schedule_from', 'activities.schedule_from'); + $this->addFilter('created_by', 'users.name'); + $this->addFilter('created_by_id', 'users.name'); + $this->addFilter('created_at', 'activities.created_at'); + $this->addFilter('lead_title', 'leads.title'); + + return $queryBuilder; + } + + /** + * Prepare columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.activities.index.datagrid.id'), + 'type' => 'integer', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'is_done', + 'label' => trans('admin::app.activities.index.datagrid.is_done'), + 'type' => 'string', + 'dropdown_options' => $this->getBooleanDropdownOptions('yes_no'), + 'searchable' => false, + 'closure' => fn ($row) => view('admin::activities.datagrid.is-done', compact('row'))->render(), + ]); + + $this->addColumn([ + 'index' => 'title', + 'label' => trans('admin::app.activities.index.datagrid.title'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'created_by_id', + 'label' => trans('admin::app.activities.index.datagrid.created_by'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => UserRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + 'closure' => function ($row) { + $route = urldecode(route('admin.settings.users.index', ['id[eq]' => $row->created_by_id])); + + return "".$row->created_by.''; + }, + ]); + + $this->addColumn([ + 'index' => 'comment', + 'label' => trans('admin::app.activities.index.datagrid.comment'), + 'type' => 'string', + ]); + + $this->addColumn([ + 'index' => 'lead_title', + 'label' => trans('admin::app.activities.index.datagrid.lead'), + 'type' => 'string', + 'searchable' => true, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => LeadRepository::class, + 'column' => [ + 'label' => 'title', + 'value' => 'title', + ], + ], + 'closure' => function ($row) { + if ($row->lead_title == null) { + return "N/A"; + } + + $route = urldecode(route('admin.leads.view', $row->lead_id)); + + return "".$row->lead_title.''; + }, + ]); + + $this->addColumn([ + 'index' => 'type', + 'label' => trans('admin::app.activities.index.datagrid.type'), + 'type' => 'string', + 'searchable' => false, + 'filterable' => false, + 'sortable' => true, + 'closure' => fn ($row) => trans('admin::app.activities.index.datagrid.'.$row->type), + ]); + + $this->addColumn([ + 'index' => 'schedule_from', + 'label' => trans('admin::app.activities.index.datagrid.schedule_from'), + 'type' => 'date', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + 'closure' => fn ($row) => core()->formatDate($row->schedule_from), + ]); + + $this->addColumn([ + 'index' => 'schedule_to', + 'label' => trans('admin::app.activities.index.datagrid.schedule_to'), + 'type' => 'date', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + 'closure' => fn ($row) => core()->formatDate($row->schedule_to), + ]); + + $this->addColumn([ + 'index' => 'created_at', + 'label' => trans('admin::app.activities.index.datagrid.created_at'), + 'type' => 'date', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'closure' => fn ($row) => core()->formatDate($row->created_at), + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('activities.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.activities.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.activities.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('activities.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.activities.index.datagrid.update'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.activities.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.activities.index.datagrid.mass-delete'), + 'method' => 'POST', + 'url' => route('admin.activities.mass_delete'), + ]); + + $this->addMassAction([ + 'title' => trans('admin::app.activities.index.datagrid.mass-update'), + 'url' => route('admin.activities.mass_update'), + 'method' => 'POST', + 'options' => [ + [ + 'label' => trans('admin::app.activities.index.datagrid.done'), + 'value' => 1, + ], [ + 'label' => trans('admin::app.activities.index.datagrid.not-done'), + 'value' => 0, + ], + ], + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Contact/OrganizationDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Contact/OrganizationDataGrid.php new file mode 100644 index 0000000..1113b76 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Contact/OrganizationDataGrid.php @@ -0,0 +1,120 @@ +addSelect( + 'organizations.id', + 'organizations.name', + 'organizations.address', + 'organizations.created_at' + ) + ->selectSub( + DB::table('persons') + ->selectRaw('count(*)') + ->whereColumn('persons.organization_id', 'organizations.id'), + 'persons_count' + ); + + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $queryBuilder->whereIn('organizations.user_id', $userIds); + } + + $this->addFilter('id', 'organizations.id'); + + $this->addFilter('organization', 'organizations.name'); + + return $queryBuilder; + } + + /** + * Add columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.contacts.organizations.index.datagrid.id'), + 'type' => 'integer', + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.contacts.organizations.index.datagrid.name'), + 'type' => 'string', + 'searchable' => true, + 'sortable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'persons_count', + 'label' => trans('admin::app.contacts.organizations.index.datagrid.persons-count'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => false, + 'filterable' => false, + ]); + + $this->addColumn([ + 'index' => 'created_at', + 'label' => trans('admin::app.settings.tags.index.datagrid.created-at'), + 'type' => 'date', + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + 'sortable' => true, + 'closure' => fn ($row) => core()->formatDate($row->created_at), + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('contacts.organizations.edit')) { + $this->addAction([ + 'icon' => 'icon-edit', + 'title' => trans('admin::app.contacts.organizations.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.contacts.organizations.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('contacts.organizations.delete')) { + $this->addAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.contacts.organizations.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.contacts.organizations.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.contacts.organizations.index.datagrid.delete'), + 'method' => 'PUT', + 'url' => route('admin.contacts.organizations.mass_delete'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Contact/PersonDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Contact/PersonDataGrid.php new file mode 100644 index 0000000..a6a8c4e --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Contact/PersonDataGrid.php @@ -0,0 +1,240 @@ +addSelect( + 'persons.id', + 'persons.name as person_name', + 'persons.emails', + 'persons.contact_numbers', + 'organizations.name as organization', + 'organizations.id as organization_id', + 'tags.name as tag_name' + ) + ->leftJoin('organizations', 'persons.organization_id', '=', 'organizations.id') + ->leftJoin('person_tags', 'persons.id', '=', 'person_tags.person_id') + ->leftJoin('tags', 'tags.id', '=', 'person_tags.tag_id') + ->groupBy('persons.id'); + + /** + * Custom (user defined) attribute values live in the EAV `attribute_values` table, so they + * are pulled in as correlated sub-selects only when exporting. They are hidden from the + * grid display (see prepareColumns) but included in the exported file. + */ + if (request()->boolean('export')) { + $tablePrefix = DB::getTablePrefix(); + + foreach ($this->getCustomAttributes() as $attribute) { + $valueColumn = AttributeValue::$attributeTypeFields[$attribute->type] ?? 'text_value'; + + $queryBuilder->addSelect(DB::raw( + '(SELECT '.$tablePrefix.'attribute_values.'.$valueColumn. + ' FROM '.$tablePrefix.'attribute_values'. + ' WHERE '.$tablePrefix.'attribute_values.entity_id = '.$tablePrefix.'persons.id'. + ' AND '.$tablePrefix.'attribute_values.attribute_id = '.(int) $attribute->id. + ' AND '.$tablePrefix."attribute_values.entity_type = 'persons'". + ' LIMIT 1) as '.$attribute->code + )); + } + } + + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $queryBuilder->whereIn('persons.user_id', $userIds); + } + + $this->addFilter('id', 'persons.id'); + $this->addFilter('person_name', 'persons.name'); + $this->addFilter('organization', 'organizations.name'); + $this->addFilter('tag_name', 'tags.name'); + + return $queryBuilder; + } + + /** + * Add columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.contacts.persons.index.datagrid.id'), + 'type' => 'integer', + 'filterable' => true, + 'sortable' => true, + 'searchable' => true, + ]); + + $this->addColumn([ + 'index' => 'person_name', + 'label' => trans('admin::app.contacts.persons.index.datagrid.name'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => true, + 'searchable' => true, + ]); + + $this->addColumn([ + 'index' => 'emails', + 'label' => trans('admin::app.contacts.persons.index.datagrid.emails'), + 'type' => 'string', + 'sortable' => false, + 'filterable' => true, + 'searchable' => true, + 'closure' => fn ($row) => collect(json_decode($row->emails, true) ?? [])->pluck('value')->join(', '), + ]); + + $this->addColumn([ + 'index' => 'contact_numbers', + 'label' => trans('admin::app.contacts.persons.index.datagrid.contact-numbers'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => true, + 'searchable' => true, + 'closure' => fn ($row) => collect(json_decode($row->contact_numbers, true) ?? [])->pluck('value')->join(', '), + ]); + + $this->addColumn([ + 'index' => 'organization', + 'label' => trans('admin::app.contacts.persons.index.datagrid.organization-name'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => OrganizationRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + ]); + + $this->addColumn([ + 'index' => 'tag_name', + 'label' => trans('admin::app.contacts.persons.index.datagrid.tag-name'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'closure' => fn ($row) => $row->tag_name ?? '--', + 'filterable_options' => [ + 'repository' => TagRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + ]); + + /** + * User defined attributes are hidden from the grid but included in the export so the data + * entered into custom fields can be exported alongside the built-in columns. + */ + if (request()->boolean('export')) { + foreach ($this->getCustomAttributes() as $attribute) { + $this->addColumn([ + 'index' => $attribute->code, + 'label' => $attribute->name, + 'type' => 'string', + 'searchable' => false, + 'sortable' => false, + 'filterable' => false, + 'visibility' => false, + ]); + } + } + } + + /** + * Retrieve the user defined attributes for persons. + */ + protected function getCustomAttributes(): Collection + { + static $attributes; + + return $attributes ??= Attribute::query() + ->where('entity_type', 'persons') + ->where('is_user_defined', 1) + ->get(); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('contacts.persons.view')) { + $this->addAction([ + 'icon' => 'icon-eye', + 'title' => trans('admin::app.contacts.persons.index.datagrid.view'), + 'method' => 'GET', + 'url' => function ($row) { + return route('admin.contacts.persons.view', $row->id); + }, + ]); + } + + if (bouncer()->hasPermission('contacts.persons.edit')) { + $this->addAction([ + 'icon' => 'icon-edit', + 'title' => trans('admin::app.contacts.persons.index.datagrid.edit'), + 'method' => 'GET', + 'url' => function ($row) { + return route('admin.contacts.persons.edit', $row->id); + }, + ]); + } + + if (bouncer()->hasPermission('contacts.persons.delete')) { + $this->addAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.contacts.persons.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => function ($row) { + return route('admin.contacts.persons.delete', $row->id); + }, + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + if (bouncer()->hasPermission('contacts.persons.delete')) { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.contacts.persons.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.contacts.persons.mass_delete'), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Lead/LeadDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Lead/LeadDataGrid.php new file mode 100644 index 0000000..12ddbd9 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Lead/LeadDataGrid.php @@ -0,0 +1,388 @@ +pipeline = $this->pipelineRepository->find(request('pipeline_id')); + } else { + $this->pipeline = $this->pipelineRepository->getDefaultPipeline(); + } + } + + /** + * Prepare query builder. + */ + public function prepareQueryBuilder(): Builder + { + $tablePrefix = DB::getTablePrefix(); + + $queryBuilder = DB::table('leads') + ->addSelect( + 'leads.id', + 'leads.title', + 'leads.status', + 'leads.lead_value', + 'leads.expected_close_date', + 'lead_sources.name as lead_source_name', + 'lead_types.name as lead_type_name', + 'leads.created_at', + 'lead_pipeline_stages.name as stage', + 'lead_tags.tag_id as tag_id', + 'users.id as user_id', + 'users.name as sales_person', + 'persons.id as person_id', + 'persons.name as person_name', + 'tags.name as tag_name', + 'lead_pipelines.rotten_days as pipeline_rotten_days', + 'lead_pipeline_stages.code as stage_code', + DB::raw('CASE WHEN DATEDIFF(NOW(),'.$tablePrefix.'leads.created_at) >='.$tablePrefix.'lead_pipelines.rotten_days THEN 1 ELSE 0 END as rotten_lead'), + ) + ->leftJoin('users', 'leads.user_id', '=', 'users.id') + ->leftJoin('persons', 'leads.person_id', '=', 'persons.id') + ->leftJoin('lead_types', 'leads.lead_type_id', '=', 'lead_types.id') + ->leftJoin('lead_pipeline_stages', 'leads.lead_pipeline_stage_id', '=', 'lead_pipeline_stages.id') + ->leftJoin('lead_sources', 'leads.lead_source_id', '=', 'lead_sources.id') + ->leftJoin('lead_pipelines', 'leads.lead_pipeline_id', '=', 'lead_pipelines.id') + ->leftJoin('lead_tags', 'leads.id', '=', 'lead_tags.lead_id') + ->leftJoin('tags', 'tags.id', '=', 'lead_tags.tag_id') + ->groupBy('leads.id') + ->where('leads.lead_pipeline_id', $this->pipeline->id); + + /** + * Custom (user defined) attribute values live in the EAV `attribute_values` table, so they + * are pulled in as correlated sub-selects only when exporting. They are hidden from the + * grid display (see prepareColumns) but included in the exported file. + */ + if (request()->boolean('export')) { + foreach ($this->getCustomAttributes() as $attribute) { + $valueColumn = AttributeValue::$attributeTypeFields[$attribute->type] ?? 'text_value'; + + $queryBuilder->addSelect(DB::raw( + '(SELECT '.$tablePrefix.'attribute_values.'.$valueColumn. + ' FROM '.$tablePrefix.'attribute_values'. + ' WHERE '.$tablePrefix.'attribute_values.entity_id = '.$tablePrefix.'leads.id'. + ' AND '.$tablePrefix.'attribute_values.attribute_id = '.(int) $attribute->id. + ' AND '.$tablePrefix."attribute_values.entity_type = 'leads'". + ' LIMIT 1) as '.$attribute->code + )); + } + } + + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $queryBuilder->whereIn('leads.user_id', $userIds); + } + + if (! is_null(request()->input('rotten_lead.in'))) { + $queryBuilder->havingRaw($tablePrefix.'rotten_lead = ?', [ + (int) request()->input('rotten_lead.in'), + ]); + } + + $this->addFilter('id', 'leads.id'); + $this->addFilter('user', 'leads.user_id'); + $this->addFilter('sales_person', 'users.name'); + $this->addFilter('lead_source_name', 'lead_sources.id'); + $this->addFilter('lead_type_name', 'lead_types.id'); + $this->addFilter('person_name', 'persons.name'); + $this->addFilter('type', 'lead_pipeline_stages.code'); + $this->addFilter('stage', 'lead_pipeline_stages.id'); + $this->addFilter('tag_name', 'tags.name'); + $this->addFilter('expected_close_date', 'leads.expected_close_date'); + $this->addFilter('created_at', 'leads.created_at'); + $this->addFilter('rotten_lead', DB::raw('DATEDIFF(NOW(), '.$tablePrefix.'leads.created_at) >= '.$tablePrefix.'lead_pipelines.rotten_days')); + + return $queryBuilder; + } + + /** + * Prepare columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.leads.index.datagrid.id'), + 'type' => 'integer', + 'sortable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'sales_person', + 'label' => trans('admin::app.leads.index.datagrid.sales-person'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => UserRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + ]); + + $this->addColumn([ + 'index' => 'title', + 'label' => trans('admin::app.leads.index.datagrid.subject'), + 'type' => 'string', + 'searchable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'lead_source_name', + 'label' => trans('admin::app.leads.index.datagrid.source'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => $this->sourceRepository->all(['name as label', 'id as value'])->toArray(), + ]); + + $this->addColumn([ + 'index' => 'lead_value', + 'label' => trans('admin::app.leads.index.datagrid.lead-value'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => false, + 'filterable' => true, + 'closure' => fn ($row) => core()->formatBasePrice($row->lead_value, 2), + ]); + + $this->addColumn([ + 'index' => 'lead_type_name', + 'label' => trans('admin::app.leads.index.datagrid.lead-type'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => $this->typeRepository->all(['name as label', 'id as value'])->toArray(), + ]); + + $this->addColumn([ + 'index' => 'tag_name', + 'label' => trans('admin::app.leads.index.datagrid.tag-name'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'closure' => fn ($row) => $row->tag_name ?? '--', + 'filterable_options' => [ + 'repository' => TagRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + ]); + + $this->addColumn([ + 'index' => 'person_name', + 'label' => trans('admin::app.leads.index.datagrid.contact-person'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => PersonRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + 'closure' => function ($row) { + if (! $row->person_id) { + return '--'; + } + + $route = route('admin.contacts.persons.view', $row->person_id); + + return "".$row->person_name.''; + }, + ]); + + $this->addColumn([ + 'index' => 'stage', + 'label' => trans('admin::app.leads.index.datagrid.stage'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => $this->pipeline->stages->pluck('name', 'id') + ->map(function ($name, $id) { + return ['value' => $id, 'label' => $name]; + }) + ->values() + ->all(), + ]); + + $this->addColumn([ + 'index' => 'rotten_lead', + 'label' => trans('admin::app.leads.index.datagrid.rotten-lead'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => false, + 'closure' => function ($row) { + if (! $row->rotten_lead) { + return trans('admin::app.leads.index.datagrid.no'); + } + + if (in_array($row->stage_code, ['won', 'lost'])) { + return trans('admin::app.leads.index.datagrid.no'); + } + + return trans('admin::app.leads.index.datagrid.yes'); + }, + ]); + + $this->addColumn([ + 'index' => 'expected_close_date', + 'label' => trans('admin::app.leads.index.datagrid.date-to'), + 'type' => 'date', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + 'closure' => function ($row) { + if (! $row->expected_close_date) { + return '--'; + } + + return $row->expected_close_date; + }, + ]); + + $this->addColumn([ + 'index' => 'created_at', + 'label' => trans('admin::app.leads.index.datagrid.created-at'), + 'type' => 'date', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + ]); + + /** + * User defined attributes are hidden from the grid but included in the export so the data + * entered into custom fields can be exported alongside the built-in columns. + */ + if (request()->boolean('export')) { + foreach ($this->getCustomAttributes() as $attribute) { + $this->addColumn([ + 'index' => $attribute->code, + 'label' => $attribute->name, + 'type' => 'string', + 'searchable' => false, + 'sortable' => false, + 'filterable' => false, + 'visibility' => false, + ]); + } + } + } + + /** + * Retrieve the user defined attributes for leads. + */ + protected function getCustomAttributes(): Collection + { + static $attributes; + + return $attributes ??= Attribute::query() + ->where('entity_type', 'leads') + ->where('is_user_defined', 1) + ->get(); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('leads.view')) { + $this->addAction([ + 'icon' => 'icon-eye', + 'title' => trans('admin::app.leads.index.datagrid.view'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.leads.view', $row->id), + ]); + } + + if (bouncer()->hasPermission('leads.delete')) { + $this->addAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.leads.index.datagrid.delete'), + 'method' => 'delete', + 'url' => fn ($row) => route('admin.leads.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.leads.index.datagrid.mass-delete'), + 'method' => 'POST', + 'url' => route('admin.leads.mass_delete'), + ]); + + $this->addMassAction([ + 'title' => trans('admin::app.leads.index.datagrid.mass-update'), + 'url' => route('admin.leads.mass_update'), + 'method' => 'POST', + 'options' => $this->pipeline->stages->map(fn ($stage) => [ + 'label' => $stage->name, + 'value' => $stage->id, + ])->toArray(), + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Mail/EmailDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Mail/EmailDataGrid.php new file mode 100644 index 0000000..2c3ad71 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Mail/EmailDataGrid.php @@ -0,0 +1,226 @@ +select( + 'emails.id', + 'emails.name', + 'emails.from', + 'emails.subject', + 'emails.reply', + 'emails.is_read', + 'emails.created_at', + 'tags.name as tags', + DB::raw('COUNT(DISTINCT '.DB::getTablePrefix().'email_attachments.id) as attachments') + ) + ->leftJoin('email_attachments', 'emails.id', '=', 'email_attachments.email_id') + ->leftJoin('email_tags', 'emails.id', '=', 'email_tags.email_id') + ->leftJoin('tags', 'tags.id', '=', 'email_tags.tag_id') + ->groupBy('emails.id') + ->where('folders', 'like', '%"'.request('route').'"%') + ->whereNull('parent_id'); + + $this->addFilter('id', 'emails.id'); + $this->addFilter('name', 'emails.name'); + $this->addFilter('tags', 'tags.name'); + $this->addFilter('created_at', 'emails.created_at'); + + return $queryBuilder; + } + + /** + * Batch-load the tags for the whole page before the per-row closures run, + * so the `tags` column does not query the database once per row. + */ + protected function formatRecords($records): mixed + { + $this->emailTags = DB::table('email_tags') + ->join('tags', 'tags.id', '=', 'email_tags.tag_id') + ->whereIn('email_tags.email_id', collect($records)->pluck('id')->all()) + ->get(['email_tags.email_id', 'tags.name', 'tags.color']) + ->groupBy('email_id') + ->toArray(); + + return parent::formatRecords($records); + } + + /** + * Prepare Columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'attachments', + 'label' => trans('admin::app.mail.index.datagrid.attachments'), + 'type' => 'string', + 'searchable' => false, + 'filterable' => false, + 'sortable' => false, + 'closure' => fn ($row) => $row->attachments ? '' : '', + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.mail.index.datagrid.from'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'closure' => function ($row) { + return $row->name + ? trim($row->name, '"') + : trim($row->from, '"'); + }, + ]); + + $this->addColumn([ + 'index' => 'subject', + 'label' => trans('admin::app.mail.index.datagrid.subject'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'reply', + 'label' => trans('admin::app.mail.index.datagrid.content'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'tags', + 'label' => trans('admin::app.mail.index.datagrid.tags'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'closure' => fn ($row) => $this->emailTags[$row->id] ?? [], + 'filterable_options' => [ + 'repository' => TagRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + ]); + + $this->addColumn([ + 'index' => 'created_at', + 'label' => trans('admin::app.mail.index.datagrid.date'), + 'type' => 'date', + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + 'sortable' => true, + 'closure' => function ($row) { + return Carbon::parse($row->created_at)->isToday() + ? Carbon::parse($row->created_at)->format('h:i A') + : Carbon::parse($row->created_at)->format('M d'); + }, + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('mail.view')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => request('route') == 'draft' + ? 'icon-edit' + : 'icon-eye', + 'title' => request('route') == 'draft' + ? trans('admin::app.mail.index.datagrid.edit') + : trans('admin::app.mail.index.datagrid.view'), + 'method' => 'GET', + 'params' => [ + 'type' => request('route') == 'trash' + ? 'delete' + : 'trash', + ], + 'url' => fn ($row) => route('admin.mail.view', [request('route'), $row->id]), + ]); + } + + if (bouncer()->hasPermission('mail.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.mail.index.datagrid.delete'), + 'method' => 'DELETE', + 'params' => [ + 'type' => request('route') == 'trash' + ? 'delete' + : 'trash', + ], + 'url' => fn ($row) => route('admin.mail.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + if (request('route') == 'trash') { + $this->addMassAction([ + 'title' => trans('admin::app.mail.index.datagrid.move-to-inbox'), + 'method' => 'POST', + 'url' => route('admin.mail.mass_update', ['folders' => ['inbox']]), + 'options' => [ + [ + 'value' => 'trash', + 'label' => trans('admin::app.mail.index.datagrid.move-to-inbox'), + ], + ], + ]); + } + + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => request('route') == 'trash' + ? trans('admin::app.mail.index.datagrid.delete') + : trans('admin::app.mail.index.datagrid.move-to-trash'), + 'method' => 'POST', + 'url' => route('admin.mail.mass_delete', [ + 'type' => request('route') == 'trash' + ? 'delete' + : 'trash', + ]), + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Product/ProductDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Product/ProductDataGrid.php new file mode 100644 index 0000000..bf63df1 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Product/ProductDataGrid.php @@ -0,0 +1,174 @@ +leftJoin('product_inventories', 'products.id', '=', 'product_inventories.product_id') + ->leftJoin('product_tags', 'products.id', '=', 'product_tags.product_id') + ->leftJoin('tags', 'tags.id', '=', 'product_tags.tag_id') + ->select( + 'products.id', + 'products.sku', + 'products.name', + 'products.price', + 'tags.name as tag_name', + ) + ->addSelect(DB::raw('SUM('.$tablePrefix.'product_inventories.in_stock) as total_in_stock')) + ->addSelect(DB::raw('SUM('.$tablePrefix.'product_inventories.allocated) as total_allocated')) + ->addSelect(DB::raw('SUM('.$tablePrefix.'product_inventories.in_stock - '.$tablePrefix.'product_inventories.allocated) as total_on_hand')) + ->groupBy('products.id'); + + if (request()->route('id')) { + $queryBuilder->where('product_inventories.warehouse_id', request()->route('id')); + } + + $this->addFilter('id', 'products.id'); + $this->addFilter('sku', 'products.sku'); + $this->addFilter('name', 'products.name'); + $this->addFilter('price', 'products.price'); + $this->addFilter('total_in_stock', DB::raw('SUM('.$tablePrefix.'product_inventories.in_stock')); + $this->addFilter('total_allocated', DB::raw('SUM('.$tablePrefix.'product_inventories.allocated')); + $this->addFilter('total_on_hand', DB::raw('SUM('.$tablePrefix.'product_inventories.in_stock - '.$tablePrefix.'product_inventories.allocated')); + $this->addFilter('tag_name', 'tags.name'); + + return $queryBuilder; + } + + /** + * Add columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'sku', + 'label' => trans('admin::app.products.index.datagrid.sku'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.products.index.datagrid.name'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'price', + 'label' => trans('admin::app.products.index.datagrid.price'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'closure' => function ($row) { + return core()->formatBasePrice($row->price, 2); + }, + ]); + + $this->addColumn([ + 'index' => 'total_in_stock', + 'label' => trans('admin::app.products.index.datagrid.in-stock'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'total_allocated', + 'label' => trans('admin::app.products.index.datagrid.allocated'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'total_on_hand', + 'label' => trans('admin::app.products.index.datagrid.on-hand'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'tag_name', + 'label' => trans('admin::app.products.index.datagrid.tag-name'), + 'type' => 'string', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'closure' => fn ($row) => $row->tag_name ?? '--', + 'filterable_options' => [ + 'repository' => TagRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('products.view')) { + $this->addAction([ + 'index' => 'view', + 'icon' => 'icon-eye', + 'title' => trans('admin::app.products.index.datagrid.view'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.products.view', $row->id), + ]); + } + + if (bouncer()->hasPermission('products.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.products.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.products.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('products.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.products.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.products.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.products.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.products.mass_delete'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Quote/QuoteDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Quote/QuoteDataGrid.php new file mode 100644 index 0000000..8671def --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Quote/QuoteDataGrid.php @@ -0,0 +1,244 @@ +addSelect( + 'quotes.id', + 'quotes.subject', + 'quotes.expired_at', + 'quotes.sub_total', + 'quotes.discount_amount', + 'quotes.tax_amount', + 'quotes.adjustment_amount', + 'quotes.grand_total', + 'quotes.created_at', + 'users.id as user_id', + 'users.name as sales_person', + 'persons.id as person_id', + 'persons.name as person_name', + 'quotes.expired_at as expired_quotes' + ) + ->leftJoin('users', 'quotes.user_id', '=', 'users.id') + ->leftJoin('persons', 'quotes.person_id', '=', 'persons.id'); + + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $queryBuilder->whereIn('quotes.user_id', $userIds); + } + + $this->addFilter('id', 'quotes.id'); + $this->addFilter('user', 'quotes.user_id'); + $this->addFilter('sales_person', 'users.name'); + $this->addFilter('person_name', 'persons.name'); + $this->addFilter('expired_at', 'quotes.expired_at'); + $this->addFilter('created_at', 'quotes.created_at'); + + if (request()->input('expired_quotes.in') == 1) { + $this->addFilter('expired_quotes', DB::raw('DATEDIFF(NOW(), '.$tablePrefix.'quotes.expired_at) >= '.$tablePrefix.'NOW()')); + } else { + $this->addFilter('expired_quotes', DB::raw('DATEDIFF(NOW(), '.$tablePrefix.'quotes.expired_at) < '.$tablePrefix.'NOW()')); + } + + return $queryBuilder; + } + + /** + * Prepare columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'subject', + 'label' => trans('admin::app.quotes.index.datagrid.subject'), + 'type' => 'string', + 'filterable' => true, + 'searchable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'sales_person', + 'label' => trans('admin::app.quotes.index.datagrid.sales-person'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => UserRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + ]); + + $this->addColumn([ + 'index' => 'person_name', + 'label' => trans('admin::app.quotes.index.datagrid.person'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => PersonRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + 'closure' => function ($row) { + $route = route('admin.contacts.persons.view', $row->person_id); + + return "".$row->person_name.''; + }, + ]); + + $this->addColumn([ + 'index' => 'sub_total', + 'label' => trans('admin::app.quotes.index.datagrid.subtotal'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => true, + 'closure' => fn ($row) => core()->formatBasePrice($row->sub_total, 2), + ]); + + $this->addColumn([ + 'index' => 'discount_amount', + 'label' => trans('admin::app.quotes.index.datagrid.discount'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => true, + 'closure' => fn ($row) => core()->formatBasePrice($row->discount_amount, 2), + ]); + + $this->addColumn([ + 'index' => 'tax_amount', + 'label' => trans('admin::app.quotes.index.datagrid.tax'), + 'type' => 'string', + 'filterable' => true, + 'sortable' => true, + 'closure' => fn ($row) => core()->formatBasePrice($row->tax_amount, 2), + ]); + + $this->addColumn([ + 'index' => 'adjustment_amount', + 'label' => trans('admin::app.quotes.index.datagrid.adjustment'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => false, + 'closure' => fn ($row) => core()->formatBasePrice($row->adjustment_amount, 2), + ]); + + $this->addColumn([ + 'index' => 'grand_total', + 'label' => trans('admin::app.quotes.index.datagrid.grand-total'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => true, + 'closure' => fn ($row) => core()->formatBasePrice($row->grand_total, 2), + ]); + + $this->addColumn([ + 'index' => 'expired_at', + 'label' => trans('admin::app.quotes.index.datagrid.expired-at'), + 'type' => 'date', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'closure' => fn ($row) => core()->formatDate($row->expired_at, 'd M Y'), + ]); + + $this->addColumn([ + 'index' => 'created_at', + 'label' => trans('admin::app.quotes.index.datagrid.created-at'), + 'type' => 'date', + 'searchable' => false, + 'sortable' => true, + 'filterable' => true, + 'closure' => fn ($row) => core()->formatDate($row->created_at), + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('quotes.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.quotes.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.quotes.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('quotes.print')) { + $this->addAction([ + 'index' => 'print', + 'icon' => 'icon-print', + 'title' => trans('admin::app.quotes.index.datagrid.print'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.quotes.print', $row->id), + ]); + } + + if (bouncer()->hasPermission('quotes.mail')) { + $this->addAction([ + 'index' => 'mail', + 'icon' => 'icon-mail', + 'title' => trans('admin::app.quotes.index.datagrid.mail'), + 'method' => 'POST', + 'url' => fn ($row) => route('admin.leads.quotes.mail', ['quote_id' => $row->id]), + ]); + } + + if (bouncer()->hasPermission('quotes.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.quotes.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.quotes.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.quotes.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.quotes.mass_delete'), + ]); + + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.quotes.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.quotes.mass_delete'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/AttributeDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/AttributeDataGrid.php new file mode 100644 index 0000000..d42b479 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/AttributeDataGrid.php @@ -0,0 +1,165 @@ +select( + 'attributes.id', + 'attributes.code', + 'attributes.name', + 'attributes.type', + 'attributes.entity_type', + 'attributes.quick_add', + 'attributes.is_user_defined as attribute_type' + ) + ->where('entity_type', '<>', 'locations'); + + $this->addFilter('id', 'attributes.id'); + $this->addFilter('type', 'attributes.type'); + $this->addFilter('attribute_type', 'attributes.is_user_defined'); + $this->addFilter('quick_add', 'attributes.quick_add'); + + return $queryBuilder; + } + + /** + * Prepare columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.attributes.index.datagrid.id'), + 'type' => 'string', + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'code', + 'label' => trans('admin::app.settings.attributes.index.datagrid.code'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.attributes.index.datagrid.name'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'quick_add', + 'label' => trans('admin::app.settings.attributes.index.datagrid.quick-add'), + 'type' => 'boolean', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'closure' => fn ($value) => trans('admin::app.settings.attributes.index.datagrid.'.($value->quick_add ? 'yes' : 'no')), + ]); + + $this->addColumn([ + 'index' => 'entity_type', + 'label' => trans('admin::app.settings.attributes.index.datagrid.entity-type'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => false, + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => app(AttributeRepository::class) + ->select('entity_type as label', 'entity_type as value') + ->distinct() + ->get() + ->map(function ($item) { + $item->label = trans('admin::app.settings.attributes.index.datagrid.entity-types.'.$item->label); + + return $item; + }) + ->toArray(), + 'closure' => fn ($row) => ucfirst($row->entity_type), + ]); + + $this->addColumn([ + 'index' => 'type', + 'label' => trans('admin::app.settings.attributes.index.datagrid.type'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => app(AttributeRepository::class) + ->select('type as label', 'type as value') + ->distinct() + ->get() + ->map(function ($item) { + $item->label = trans('admin::app.settings.attributes.index.datagrid.types.'.$item->label); + + return $item; + }) + ->toArray(), + ]); + + $this->addColumn([ + 'index' => 'attribute_type', + 'label' => trans('admin::app.settings.attributes.index.datagrid.is-default'), + 'type' => 'boolean', + 'searchable' => true, + 'filterable' => false, + 'sortable' => true, + 'closure' => fn ($value) => trans('admin::app.settings.attributes.index.datagrid.'.($value->attribute_type ? 'no' : 'yes')), + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.automation.attributes.edit')) { + $this->addAction([ + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.attributes.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.attributes.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.automation.attributes.delete')) { + $this->addAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.attributes.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.attributes.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.attributes.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.settings.attributes.mass_delete'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/DataTransfer/ImportDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/DataTransfer/ImportDataGrid.php new file mode 100644 index 0000000..c4594b1 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/DataTransfer/ImportDataGrid.php @@ -0,0 +1,166 @@ +select( + 'id', + 'state', + 'file_path', + 'error_file_path', + 'started_at', + 'completed_at', + 'type', + 'summary', + ); + } + + /** + * Prepare Columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.data-transfer.imports.index.datagrid.id'), + 'type' => 'integer', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'type', + 'label' => trans('admin::app.settings.data-transfer.imports.index.datagrid.type'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'state', + 'label' => trans('admin::app.settings.data-transfer.imports.index.datagrid.state'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'file_path', + 'label' => trans('admin::app.settings.data-transfer.imports.index.datagrid.uploaded-file'), + 'type' => 'string', + 'closure' => function ($row) { + return ''.$row->file_path.''; + }, + ]); + + $this->addColumn([ + 'index' => 'error_file_path', + 'label' => trans('admin::app.settings.data-transfer.imports.index.datagrid.error-file'), + 'type' => 'string', + 'closure' => function ($row) { + if (empty($row->error_file_path)) { + return ''; + } + + return ''.$row->error_file_path.''; + }, + ]); + + $this->addColumn([ + 'index' => 'started_at', + 'label' => trans('admin::app.settings.data-transfer.imports.index.datagrid.started-at'), + 'type' => 'date', + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'completed_at', + 'label' => trans('admin::app.settings.data-transfer.imports.index.datagrid.completed-at'), + 'type' => 'date', + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'summary', + 'label' => trans('admin::app.settings.data-transfer.imports.index.datagrid.summary'), + 'type' => 'string', + 'closure' => function ($row) { + if (empty($row->summary)) { + return ''; + } + + $summary = json_decode($row->summary, true); + + $stats = []; + + foreach ($summary as $type => $value) { + $stats[] = trans('admin::app.settings.data-transfer.imports.index.datagrid.'.$type).': '.$summary[$type]; + } + + return implode(', ', $stats); + }, + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.automation.data_transfer.imports.import')) { + $this->addAction([ + 'index' => 'import', + 'icon' => 'icon-import', + 'title' => trans('admin::app.settings.data-transfer.imports.index.datagrid.import'), + 'method' => 'GET', + 'url' => function ($row) { + return route('admin.settings.data_transfer.imports.import', $row->id); + }, + ]); + } + + if (bouncer()->hasPermission('settings.automation.data_transfer.imports.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.data-transfer.imports.index.datagrid.edit'), + 'method' => 'GET', + 'url' => function ($row) { + return route('admin.settings.data_transfer.imports.edit', $row->id); + }, + ]); + } + + if (bouncer()->hasPermission('settings.automation.data_transfer.imports.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.data-transfer.imports.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => function ($row) { + return route('admin.settings.data_transfer.imports.delete', $row->id); + }, + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/EmailTemplateDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/EmailTemplateDataGrid.php new file mode 100644 index 0000000..94b976f --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/EmailTemplateDataGrid.php @@ -0,0 +1,87 @@ +addSelect( + 'email_templates.id', + 'email_templates.name', + 'email_templates.subject', + ); + + $this->addFilter('id', 'email_templates.id'); + + return $queryBuilder; + } + + /** + * Add columns. + * + * @return void + */ + public function prepareColumns() + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.email-template.index.datagrid.id'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.email-template.index.datagrid.name'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'subject', + 'label' => trans('admin::app.settings.email-template.index.datagrid.subject'), + 'type' => 'string', + 'sortable' => true, + ]); + } + + /** + * Prepare actions. + * + * @return void + */ + public function prepareActions() + { + if (bouncer()->hasPermission('settings.automation.email_templates.edit')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.email-template.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.email_templates.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.automation.email_templates.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.email-template.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.email_templates.delete', $row->id), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/GroupDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/GroupDataGrid.php new file mode 100644 index 0000000..7ab5b30 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/GroupDataGrid.php @@ -0,0 +1,84 @@ +addSelect( + 'groups.id', + 'groups.name', + 'groups.description' + ); + + $this->addFilter('id', 'groups.id'); + + return $queryBuilder; + } + + /** + * Prepare columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.groups.index.datagrid.id'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'type' => 'string', + 'label' => trans('admin::app.settings.groups.index.datagrid.name'), + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'description', + 'label' => trans('admin::app.settings.groups.index.datagrid.description'), + 'type' => 'string', + 'sortable' => false, + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.user.groups.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.groups.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.groups.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.user.groups.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.groups.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.groups.delete', $row->id), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/Marketing/CampaignDatagrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/Marketing/CampaignDatagrid.php new file mode 100644 index 0000000..8616987 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/Marketing/CampaignDatagrid.php @@ -0,0 +1,110 @@ +addSelect( + 'marketing_campaigns.id', + 'marketing_campaigns.name', + 'marketing_campaigns.subject', + 'marketing_campaigns.status', + ); + + $this->addFilter('id', 'marketing_campaigns.id'); + + return $queryBuilder; + } + + /** + * Add columns. + * + * @return void + */ + public function prepareColumns() + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.marketing.campaigns.index.datagrid.id'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.marketing.campaigns.index.datagrid.name'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'subject', + 'label' => trans('admin::app.settings.marketing.campaigns.index.datagrid.subject'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'status', + 'label' => trans('admin::app.settings.marketing.campaigns.index.datagrid.status'), + 'type' => 'string', + 'sortable' => true, + ]); + } + + /** + * Prepare actions. + * + * @return void + */ + public function prepareActions() + { + if (bouncer()->hasPermission('settings.automation.campaigns.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.marketing.campaigns.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.marketing.campaigns.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.automation.campaigns.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.marketing.campaigns.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.marketing.campaigns.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + if (bouncer()->hasPermission('settings.automation.campaigns.mass_delete')) { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.marketing.campaigns.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.settings.marketing.campaigns.mass_delete'), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/Marketing/EventDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/Marketing/EventDataGrid.php new file mode 100644 index 0000000..63dc2ac --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/Marketing/EventDataGrid.php @@ -0,0 +1,110 @@ +addSelect( + 'marketing_events.id', + 'marketing_events.name', + 'marketing_events.description', + 'marketing_events.date', + ); + + $this->addFilter('id', 'marketing_events.id'); + + return $queryBuilder; + } + + /** + * Add columns. + * + * @return void + */ + public function prepareColumns() + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.marketing.events.index.datagrid.id'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.marketing.events.index.datagrid.name'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'description', + 'label' => trans('admin::app.settings.marketing.events.index.datagrid.description'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'date', + 'label' => trans('admin::app.settings.marketing.events.index.datagrid.date'), + 'type' => 'string', + 'sortable' => true, + ]); + } + + /** + * Prepare actions. + * + * @return void + */ + public function prepareActions() + { + if (bouncer()->hasPermission('settings.automation.events.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.marketing.events.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.marketing.events.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.automation.events.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.marketing.events.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.marketing.events.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + if (bouncer()->hasPermission('settings.automation.events.delete')) { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.marketing.events.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.settings.marketing.events.mass_delete'), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/PipelineDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/PipelineDataGrid.php new file mode 100644 index 0000000..9626619 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/PipelineDataGrid.php @@ -0,0 +1,93 @@ +addSelect( + 'lead_pipelines.id', + 'lead_pipelines.name', + 'lead_pipelines.rotten_days', + 'lead_pipelines.is_default', + ); + + $this->addFilter('id', 'lead_pipelines.id'); + + return $queryBuilder; + } + + /** + * Prepare columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.pipelines.index.datagrid.id'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.pipelines.index.datagrid.name'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'rotten_days', + 'label' => trans('admin::app.settings.pipelines.index.datagrid.rotten-days'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'is_default', + 'label' => trans('admin::app.settings.pipelines.index.datagrid.is-default'), + 'type' => 'boolean', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + 'closure' => fn ($value) => trans('admin::app.settings.pipelines.index.datagrid.'.($value->is_default ? 'yes' : 'no')), + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.lead.pipelines.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.pipelines.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.pipelines.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.lead.pipelines.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.pipelines.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.pipelines.delete', $row->id), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/RoleDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/RoleDataGrid.php new file mode 100644 index 0000000..daf4758 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/RoleDataGrid.php @@ -0,0 +1,120 @@ +addSelect( + 'roles.id', + 'roles.name', + 'roles.description', + 'roles.permission_type' + ); + + /** + * Scope the listing to the acting user's data scope. A `global` scope (or a full + * administrator) returns null and sees every role; a `group` scope sees the roles created + * within their group; an `individual` scope sees only the roles they created. In every case + * the user's own assigned role is included, so they can always see the role they belong to. + */ + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $ownRoleId = auth()->guard('user')->user()?->role_id; + + $queryBuilder->where(function ($query) use ($userIds, $ownRoleId) { + $query->whereIn('roles.created_by', $userIds) + ->orWhere('roles.id', $ownRoleId); + }); + } + + $this->addFilter('id', 'roles.id'); + $this->addFilter('name', 'roles.name'); + + return $queryBuilder; + } + + /** + * Prepare Columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.roles.index.datagrid.id'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.roles.index.datagrid.name'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'description', + 'label' => trans('admin::app.settings.roles.index.datagrid.description'), + 'type' => 'string', + 'searchable' => true, + 'sortable' => false, + ]); + + $this->addColumn([ + 'index' => 'permission_type', + 'label' => trans('admin::app.settings.roles.index.datagrid.permission-type'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => [ + [ + 'label' => trans('admin::app.settings.roles.index.datagrid.custom'), + 'value' => 'custom', + ], + [ + 'label' => trans('admin::app.settings.roles.index.datagrid.all'), + 'value' => 'all', + ], + ], + 'sortable' => true, + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.user.roles.edit')) { + $this->addAction([ + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.roles.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.roles.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.user.roles.delete')) { + $this->addAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.roles.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.roles.delete', $row->id), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/SourceDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/SourceDataGrid.php new file mode 100644 index 0000000..47ead3b --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/SourceDataGrid.php @@ -0,0 +1,74 @@ +addSelect( + 'lead_sources.id', + 'lead_sources.name' + ); + + $this->addFilter('id', 'lead_sources.id'); + + return $queryBuilder; + } + + /** + * Prepare Columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.sources.index.datagrid.id'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.sources.index.datagrid.name'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.lead.sources.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.sources.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.sources.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.lead.sources.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.sources.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.sources.delete', $row->id), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/TagDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/TagDataGrid.php new file mode 100644 index 0000000..c012e63 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/TagDataGrid.php @@ -0,0 +1,124 @@ +addSelect( + 'tags.id', + 'tags.name', + 'tags.color', + 'tags.created_at', + 'users.name as user_name', + ) + ->leftJoin('users', 'tags.user_id', '=', 'users.id'); + + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $queryBuilder->whereIn('tags.user_id', $userIds); + } + + $this->addFilter('id', 'tags.id'); + $this->addFilter('name', 'tags.name'); + $this->addFilter('created_at', 'tags.created_at'); + $this->addFilter('user_name', 'users.id'); + + return $queryBuilder; + } + + /** + * Prepare Columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.tags.index.datagrid.id'), + 'type' => 'string', + 'searchable' => true, + 'sortable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.tags.index.datagrid.name'), + 'type' => 'string', + 'searchable' => true, + 'sortable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'user_name', + 'label' => trans('admin::app.settings.tags.index.datagrid.users'), + 'type' => 'string', + 'searchable' => true, + 'sortable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'created_at', + 'label' => trans('admin::app.settings.tags.index.datagrid.created-at'), + 'type' => 'date', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + 'filterable_type' => 'date_range', + 'closure' => fn ($row) => core()->formatDate($row->created_at), + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.other_settings.tags.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.tags.index.datagrid.edit'), + 'method' => 'GET', + 'url' => function ($row) { + return route('admin.settings.tags.edit', $row->id); + }, + ]); + } + + if (bouncer()->hasPermission('settings.other_settings.tags.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.tags.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => function ($row) { + return route('admin.settings.tags.delete', $row->id); + }, + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.tags.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.settings.tags.mass_delete'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/TypeDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/TypeDataGrid.php new file mode 100644 index 0000000..448cde9 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/TypeDataGrid.php @@ -0,0 +1,76 @@ +addSelect( + 'lead_types.id', + 'lead_types.name' + ); + + $this->addFilter('id', 'lead_types.id'); + + return $queryBuilder; + } + + /** + * Prepare Columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.types.index.datagrid.id'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.types.index.datagrid.name'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + } + + /** + * Prepare Actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.lead.types.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.roles.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.types.update', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.lead.types.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.roles.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.types.delete', $row->id), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/UserDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/UserDataGrid.php new file mode 100644 index 0000000..4a7a92e --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/UserDataGrid.php @@ -0,0 +1,190 @@ +addSelect( + 'users.id', + 'users.name', + 'users.email', + 'users.image', + 'users.status', + 'users.created_at', + DB::raw('GROUP_CONCAT(DISTINCT '.$tablePrefix.'groups.name ORDER BY '.$tablePrefix.'groups.name SEPARATOR \', \') as group_name') + ) + ->leftJoin('user_groups', 'users.id', '=', 'user_groups.user_id') + ->leftJoin('groups', 'user_groups.group_id', '=', 'groups.id') + ->groupBy('users.id'); + + /** + * Scope the listing to the acting user's data scope, mirroring how Krayin scopes lead and + * contact records by their owner. A `global` scope (or a full administrator) returns null and + * sees every user; a `group` scope sees the users belonging to their group; an `individual` + * scope sees the users they created together with their own account. What a user may then edit + * or delete is further constrained by the role hierarchy enforced in the controller. + */ + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $queryBuilder->where(function ($query) use ($userIds) { + $query->whereIn('users.created_by', $userIds) + ->orWhereIn('users.id', $userIds); + }); + } + + /** + * The `groups` join introduces columns that also exist on `users` (e.g. `name`, + * `created_at`), so the searchable and filterable columns are qualified to keep the + * generated where clauses unambiguous. + */ + $this->addFilter('name', 'users.name'); + $this->addFilter('email', 'users.email'); + $this->addFilter('created_at', 'users.created_at'); + $this->addFilter('group_name', 'groups.name'); + + return $queryBuilder; + } + + /** + * Add columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.users.index.datagrid.id'), + 'type' => 'string', + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.users.index.datagrid.name'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + 'closure' => function ($row) { + return [ + 'image' => $row->image ? Storage::url($row->image) : null, + 'name' => $row->name, + ]; + }, + ]); + + $this->addColumn([ + 'index' => 'email', + 'label' => trans('admin::app.settings.users.index.datagrid.email'), + 'type' => 'string', + 'sortable' => true, + 'searchable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'group_name', + 'label' => trans('admin::app.settings.users.index.datagrid.group'), + 'type' => 'string', + 'searchable' => true, + 'sortable' => true, + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => $this->groupRepository->all(['name as label', 'name as value'])->toArray(), + 'closure' => fn ($row) => $row->group_name ?: trans('admin::app.settings.users.index.datagrid.no-group'), + ]); + + $this->addColumn([ + 'index' => 'status', + 'label' => trans('admin::app.settings.users.index.datagrid.status'), + 'type' => 'boolean', + 'filterable' => true, + 'sortable' => true, + 'searchable' => true, + ]); + + $this->addColumn([ + 'index' => 'created_at', + 'label' => trans('admin::app.settings.users.index.datagrid.created-at'), + 'type' => 'date', + 'sortable' => true, + 'searchable' => true, + 'filterable_type' => 'date_range', + 'filterable' => true, + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.user.users.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.users.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.users.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.user.users.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.users.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.users.delete', $row->id), + ]); + } + } + + /** + * Prepare mass actions. + */ + public function prepareMassActions(): void + { + $this->addMassAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.users.index.datagrid.delete'), + 'method' => 'POST', + 'url' => route('admin.settings.users.mass_delete'), + ]); + + $this->addMassAction([ + 'title' => trans('admin::app.settings.users.index.datagrid.update-status'), + 'method' => 'POST', + 'url' => route('admin.settings.users.mass_update'), + 'options' => [ + [ + 'label' => trans('admin::app.settings.users.index.datagrid.active'), + 'value' => 1, + ], + [ + 'label' => trans('admin::app.settings.users.index.datagrid.inactive'), + 'value' => 0, + ], + ], + ]); + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/WarehouseDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/WarehouseDataGrid.php new file mode 100644 index 0000000..8ad1c69 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/WarehouseDataGrid.php @@ -0,0 +1,159 @@ +leftJoin('product_inventories', 'warehouses.id', '=', 'product_inventories.warehouse_id') + ->select( + 'warehouses.id', + 'warehouses.name', + 'warehouses.contact_name', + 'warehouses.contact_emails', + 'warehouses.contact_numbers', + 'warehouses.created_at', + ) + ->addSelect(DB::raw('count(DISTINCT '.DB::getTablePrefix().'product_inventories.product_id) as products')) + ->groupBy('warehouses.id'); + + $this->addFilter('id', 'warehouses.id'); + $this->addFilter('created_at', 'warehouses.created_at'); + + return $queryBuilder; + } + + /** + * Add columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.warehouses.index.datagrid.id'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.warehouses.index.datagrid.name'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => true, + ]); + + $this->addColumn([ + 'index' => 'contact_name', + 'label' => trans('admin::app.settings.warehouses.index.datagrid.contact-name'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'contact_emails', + 'label' => trans('admin::app.settings.warehouses.index.datagrid.contact-emails'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + 'closure' => function ($row) { + $emails = json_decode($row->contact_emails, true); + + if ($emails) { + return collect($emails)->pluck('value')->join(', '); + } + }, + ]); + + $this->addColumn([ + 'index' => 'contact_numbers', + 'label' => trans('admin::app.settings.warehouses.index.datagrid.contact-numbers'), + 'type' => 'string', + 'sortable' => false, + 'closure' => function ($row) { + $numbers = json_decode($row->contact_numbers, true); + + if ($numbers) { + return collect($numbers)->pluck('value')->join(', '); + } + }, + ]); + + $this->addColumn([ + 'index' => 'products', + 'label' => trans('admin::app.settings.warehouses.index.datagrid.products'), + 'type' => 'string', + 'sortable' => true, + 'filterable' => false, + ]); + + $this->addColumn([ + 'index' => 'created_at', + 'label' => trans('admin::app.settings.warehouses.index.datagrid.created-at'), + 'type' => 'date', + 'searchable' => true, + 'filterable' => true, + 'filterable_type' => 'date_range', + 'sortable' => true, + 'closure' => function ($row) { + return core()->formatDate($row->created_at); + }, + ]); + } + + /** + * Prepare actions. + * + * @return void + */ + public function prepareActions() + { + if (bouncer()->hasPermission('settings.inventory.warehouse')) { + $this->addAction([ + 'icon' => 'icon-eye', + 'title' => trans('admin::app.settings.warehouses.index.datagrid.view'), + 'method' => 'GET', + 'url' => function ($row) { + return route('admin.settings.warehouses.view', $row->id); + }, + ]); + } + + if (bouncer()->hasPermission('settings.inventory.warehouse.edit')) { + $this->addAction([ + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.warehouses.index.datagrid.edit'), + 'method' => 'GET', + 'url' => function ($row) { + return route('admin.settings.warehouses.edit', $row->id); + }, + ]); + } + + if (bouncer()->hasPermission('settings.inventory.warehouse.delete')) { + $this->addAction([ + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.warehouses.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => function ($row) { + return route('admin.settings.warehouses.delete', $row->id); + }, + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/WebhookDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/WebhookDataGrid.php new file mode 100644 index 0000000..4b8263b --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/WebhookDataGrid.php @@ -0,0 +1,96 @@ +addSelect( + 'webhooks.id', + 'webhooks.name', + 'webhooks.entity_type', + 'webhooks.end_point', + ); + + $this->addFilter('id', 'webhooks.id'); + + return $queryBuilder; + } + + /** + * Prepare Columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.webhooks.index.datagrid.id'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.webhooks.index.datagrid.name'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'entity_type', + 'label' => trans('admin::app.settings.webhooks.index.datagrid.entity-type'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'end_point', + 'label' => trans('admin::app.settings.webhooks.index.datagrid.end-point'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.automation.webhooks.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.webhooks.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.webhooks.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.automation.webhooks.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.webhooks.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.webhooks.delete', $row->id), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/DataGrids/Settings/WorkflowDataGrid.php b/packages/Webkul/Admin/src/DataGrids/Settings/WorkflowDataGrid.php new file mode 100644 index 0000000..4e35420 --- /dev/null +++ b/packages/Webkul/Admin/src/DataGrids/Settings/WorkflowDataGrid.php @@ -0,0 +1,76 @@ +addSelect( + 'workflows.id', + 'workflows.name' + ); + + $this->addFilter('id', 'workflows.id'); + + return $queryBuilder; + } + + /** + * Prepare Columns. + */ + public function prepareColumns(): void + { + $this->addColumn([ + 'index' => 'id', + 'label' => trans('admin::app.settings.workflows.index.datagrid.id'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + + $this->addColumn([ + 'index' => 'name', + 'label' => trans('admin::app.settings.workflows.index.datagrid.name'), + 'type' => 'string', + 'searchable' => true, + 'filterable' => true, + 'sortable' => true, + ]); + } + + /** + * Prepare actions. + */ + public function prepareActions(): void + { + if (bouncer()->hasPermission('settings.automation.workflows.edit')) { + $this->addAction([ + 'index' => 'edit', + 'icon' => 'icon-edit', + 'title' => trans('admin::app.settings.workflows.index.datagrid.edit'), + 'method' => 'GET', + 'url' => fn ($row) => route('admin.settings.workflows.edit', $row->id), + ]); + } + + if (bouncer()->hasPermission('settings.automation.workflows.delete')) { + $this->addAction([ + 'index' => 'delete', + 'icon' => 'icon-delete', + 'title' => trans('admin::app.settings.workflows.index.datagrid.delete'), + 'method' => 'DELETE', + 'url' => fn ($row) => route('admin.settings.workflows.delete', $row->id), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/Database/Migrations/2021_06_07_162808_add_lead_view_permission_column_in_users_table.php b/packages/Webkul/Admin/src/Database/Migrations/2021_06_07_162808_add_lead_view_permission_column_in_users_table.php new file mode 100644 index 0000000..71875c4 --- /dev/null +++ b/packages/Webkul/Admin/src/Database/Migrations/2021_06_07_162808_add_lead_view_permission_column_in_users_table.php @@ -0,0 +1,32 @@ +string('view_permission')->after('status')->default('global')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('view_permission'); + }); + } +}; diff --git a/packages/Webkul/Admin/src/Database/Migrations/2021_10_02_170105_insert_expected_closed_date_column_in_attributes_table.php b/packages/Webkul/Admin/src/Database/Migrations/2021_10_02_170105_insert_expected_closed_date_column_in_attributes_table.php new file mode 100644 index 0000000..29c9861 --- /dev/null +++ b/packages/Webkul/Admin/src/Database/Migrations/2021_10_02_170105_insert_expected_closed_date_column_in_attributes_table.php @@ -0,0 +1,42 @@ +insert([ + [ + 'id' => '7', + 'code' => 'expected_close_date', + 'name' => 'Expected Close Date', + 'type' => 'date', + 'entity_type' => 'leads', + 'lookup_type' => null, + 'validation' => null, + 'sort_order' => '8', + 'is_required' => '0', + 'is_unique' => '0', + 'quick_add' => '1', + 'is_user_defined' => '0', + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ], + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() {} +}; diff --git a/packages/Webkul/Admin/src/Database/Migrations/2026_06_10_000000_make_value_nullable_in_core_config_table.php b/packages/Webkul/Admin/src/Database/Migrations/2026_06_10_000000_make_value_nullable_in_core_config_table.php new file mode 100644 index 0000000..3b6ba7a --- /dev/null +++ b/packages/Webkul/Admin/src/Database/Migrations/2026_06_10_000000_make_value_nullable_in_core_config_table.php @@ -0,0 +1,28 @@ +text('value')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('core_config', function (Blueprint $table) { + $table->text('value')->nullable(false)->change(); + }); + } +}; diff --git a/packages/Webkul/Admin/src/Exceptions/Handler.php b/packages/Webkul/Admin/src/Exceptions/Handler.php new file mode 100755 index 0000000..a50e701 --- /dev/null +++ b/packages/Webkul/Admin/src/Exceptions/Handler.php @@ -0,0 +1,119 @@ +jsonErrorMessages = [ + '404' => trans('admin::app.errors.404.title'), + '403' => trans('admin::app.errors.403.title'), + '401' => trans('admin::app.errors.401.title'), + '500' => trans('admin::app.errors.500.title'), + ]; + } + + /** + * Render an exception into an HTTP response. + * + * @param Request $request + * @return Response + */ + public function render($request, Throwable $exception) + { + if (! config('app.debug')) { + return $this->renderCustomResponse($exception); + } + + return parent::render($request, $exception); + } + + /** + * Convert an authentication exception into a response. + * + * @param Request $request + * @return Response + */ + protected function unauthenticated($request, AuthenticationException $exception) + { + if ($request->expectsJson()) { + return response()->json(['message' => $this->jsonErrorMessages[401]], 401); + } + + return redirect()->guest(route('customer.session.index')); + } + + /** + * Render custom HTTP response. + * + * @return Response|null + */ + private function renderCustomResponse(Throwable $exception) + { + if ($exception instanceof HttpException) { + $statusCode = in_array($exception->getStatusCode(), [401, 403, 404, 503]) + ? $exception->getStatusCode() + : 500; + + return $this->response($statusCode); + } + + if ($exception instanceof ValidationException) { + return parent::render(request(), $exception); + } + + if ($exception instanceof ModelNotFoundException) { + return $this->response(404); + } elseif ($exception instanceof PDOException || $exception instanceof \ParseError) { + return $this->response(500); + } else { + return $this->response(500); + } + } + + /** + * Return custom response. + * + * @param string $path + * @param string $errorCode + * @return mixed + */ + private function response($errorCode) + { + if (request()->expectsJson()) { + return response()->json([ + 'message' => isset($this->jsonErrorMessages[$errorCode]) + ? $this->jsonErrorMessages[$errorCode] + : trans('admin::app.common.something-went-wrong'), + ], $errorCode); + } + + return response()->view('admin::errors.index', compact('errorCode')); + } +} diff --git a/packages/Webkul/Admin/src/Facades/Bouncer.php b/packages/Webkul/Admin/src/Facades/Bouncer.php new file mode 100755 index 0000000..b62fed9 --- /dev/null +++ b/packages/Webkul/Admin/src/Facades/Bouncer.php @@ -0,0 +1,18 @@ + $this->leadReporting->getTotalWonLeadValueProgress(), + 'total_lost_revenue' => $this->leadReporting->getTotalLostLeadValueProgress(), + ]; + } + + /** + * Returns the overall statistics. + */ + public function getOverAllStats(): array + { + return [ + 'total_leads' => $this->leadReporting->getTotalLeadsProgress(), + 'average_lead_value' => $this->leadReporting->getAverageLeadValueProgress(), + 'average_leads_per_day' => $this->leadReporting->getAverageLeadsPerDayProgress(), + 'total_quotations' => $this->quoteReporting->getTotalQuotesProgress(), + 'total_persons' => $this->personReporting->getTotalPersonsProgress(), + 'total_organizations' => $this->organizationReporting->getTotalOrganizationsProgress(), + ]; + } + + /** + * Returns leads statistics. + */ + public function getTotalLeadsStats(): array + { + return [ + 'all' => [ + 'over_time' => $this->leadReporting->getTotalLeadsOverTime(), + ], + + 'won' => [ + 'over_time' => $this->leadReporting->getTotalWonLeadsOverTime(), + ], + 'lost' => [ + 'over_time' => $this->leadReporting->getTotalLostLeadsOverTime(), + ], + ]; + } + + /** + * Returns leads revenue statistics by sources. + */ + public function getLeadsStatsBySources(): mixed + { + return $this->leadReporting->getTotalWonLeadValueBySources(); + } + + /** + * Returns leads revenue statistics by types. + */ + public function getLeadsStatsByTypes(): mixed + { + return $this->leadReporting->getTotalWonLeadValueByTypes(); + } + + /** + * Returns open leads statistics by states. + */ + public function getOpenLeadsByStates(): mixed + { + return $this->leadReporting->getOpenLeadsByStates(); + } + + /** + * Returns top selling products statistics. + */ + public function getTopSellingProducts(): Collection + { + return $this->productReporting->getTopSellingProductsByRevenue(5); + } + + /** + * Returns top selling products statistics. + */ + public function getTopPersons(): Collection + { + return $this->personReporting->getTopCustomersByRevenue(5); + } + + /** + * Get the start date. + * + * @return \Carbon\Carbon + */ + public function getStartDate(): Carbon + { + return $this->leadReporting->getStartDate(); + } + + /** + * Get the end date. + * + * @return \Carbon\Carbon + */ + public function getEndDate(): Carbon + { + return $this->leadReporting->getEndDate(); + } + + /** + * Returns date range. The year is included only when the range spans more + * than one calendar year, so longer ranges stay unambiguous. + */ + public function getDateRange(): string + { + $startDate = $this->getStartDate(); + + $endDate = $this->getEndDate(); + + $format = $startDate->year === $endDate->year ? 'd M' : 'd M Y'; + + return $startDate->format($format).' - '.$endDate->format($format); + } +} diff --git a/packages/Webkul/Admin/src/Helpers/Reporting/AbstractReporting.php b/packages/Webkul/Admin/src/Helpers/Reporting/AbstractReporting.php new file mode 100644 index 0000000..98e2d59 --- /dev/null +++ b/packages/Webkul/Admin/src/Helpers/Reporting/AbstractReporting.php @@ -0,0 +1,381 @@ +setStartDate(request()->date('start')); + + $this->setEndDate(request()->date('end')); + } + + /** + * Set the start date, or fall back to the range configured under + * `general.settings.dashboard` when none is provided. + * + * @param \Carbon\Carbon|null $startDate + * @return void + */ + public function setStartDate(?Carbon $startDate = null): self + { + $this->startDate = $startDate ? $startDate->startOfDay() : $this->getDefaultStartDate(); + + $this->setLastStartDate(); + + return $this; + } + + /** + * Resolve the default start date from the configured dashboard date range. + * + * @return \Carbon\Carbon + */ + public function getDefaultStartDate(): Carbon + { + $range = core()->getConfigData('general.settings.dashboard.date_range'); + + $startDate = match ($range) { + '3_months' => now()->subMonths(3), + '9_months' => now()->subMonths(9), + '1_year' => now()->subYear(), + '2_years' => now()->subYears(2), + 'custom' => now()->subDays($this->getCustomRangeInDays()), + default => now()->subMonth(), + }; + + return $startDate->startOfDay(); + } + + /** + * Number of days configured for the custom range. Falls back when the + * stored value is missing or not a positive number. + */ + protected function getCustomRangeInDays(): int + { + $days = (int) core()->getConfigData('general.settings.dashboard.custom_days'); + + return $days > 0 ? $days : self::FALLBACK_RANGE_IN_DAYS; + } + + /** + * Sets the end date to the provided date's end of day, or to the current + * date if not provided or if the provided date is in the future. + * + * @param \Carbon\Carbon|null $endDate + * @return void + */ + public function setEndDate(?Carbon $endDate = null): self + { + $this->endDate = ($endDate && $endDate->endOfDay() <= now()) ? $endDate->endOfDay() : now(); + + $this->setLastEndDate(); + + return $this; + } + + /** + * Get the start date. + * + * @return \Carbon\Carbon + */ + public function getStartDate(): Carbon + { + return $this->startDate; + } + + /** + * Get the end date. + * + * @return \Carbon\Carbon + */ + public function getEndDate(): Carbon + { + return $this->endDate; + } + + /** + * Sets the start date for the last period. + */ + private function setLastStartDate(): void + { + if (! isset($this->startDate)) { + $this->setStartDate(request()->date('start')); + } + + if (! isset($this->endDate)) { + $this->setEndDate(request()->date('end')); + } + + $this->lastStartDate = $this->startDate->clone()->subDays($this->startDate->diffInDays($this->endDate)); + } + + /** + * Sets the end date for the last period. + */ + private function setLastEndDate(): void + { + $this->lastEndDate = $this->startDate->clone(); + } + + /** + * Get the last start date. + * + * @return \Carbon\Carbon + */ + public function getLastStartDate(): Carbon + { + return $this->lastStartDate; + } + + /** + * Get the last end date. + * + * @return \Carbon\Carbon + */ + public function getLastEndDate(): Carbon + { + return $this->lastEndDate; + } + + /** + * Calculate the percentage change between previous and current values. + * + * @param float|int $previous + * @param float|int $current + */ + public function getPercentageChange($previous, $current): float|int + { + if (! $previous) { + return $current ? 100 : 0; + } + + return ($current - $previous) / $previous * 100; + } + + /** + * Returns time intervals. + * + * @param \Carbon\Carbon $startDate + * @param \Carbon\Carbon $endDate + * @param string $period + * @return array + */ + public function getTimeInterval($startDate, $endDate, $dateColumn, $period) + { + if ($period == 'auto') { + $totalMonths = $startDate->diffInMonths($endDate) + 1; + + /** + * If the difference between the start and end date is more than 5 months + */ + $intervals = $this->getMonthsInterval($startDate, $endDate); + + if (! empty($intervals)) { + return [ + 'group_column' => "MONTH($dateColumn)", + 'intervals' => $intervals, + ]; + } + + /** + * If the difference between the start and end date is more than 6 weeks + */ + $intervals = $this->getWeeksInterval($startDate, $endDate); + + if (! empty($intervals)) { + return [ + 'group_column' => "WEEK($dateColumn)", + 'intervals' => $intervals, + ]; + } + + /** + * If the difference between the start and end date is less than 6 weeks + */ + return [ + 'group_column' => "DAYOFYEAR($dateColumn)", + 'intervals' => $this->getDaysInterval($startDate, $endDate), + ]; + } else { + $datePeriod = CarbonPeriod::create($this->startDate, "1 $period", $this->endDate); + + if ($period == 'year') { + $formatter = '?'; + } elseif ($period == 'month') { + $formatter = '?-?'; + } else { + $formatter = '?-?-?'; + } + + $groupColumn = 'DATE_FORMAT('.$dateColumn.', "'.Str::replaceArray('?', ['%Y', '%m', '%d'], $formatter).'")'; + + $intervals = []; + + foreach ($datePeriod as $date) { + $formattedDate = $date->format(Str::replaceArray('?', ['Y', 'm', 'd'], $formatter)); + + $intervals[] = [ + 'filter' => $formattedDate, + 'start' => $formattedDate, + ]; + } + + return [ + 'group_column' => $groupColumn, + 'intervals' => $intervals, + ]; + } + } + + /** + * Returns time intervals. + * + * @param \Carbon\Carbon $startDate + * @param \Carbon\Carbon $endDate + * @return array + */ + public function getMonthsInterval($startDate, $endDate) + { + $intervals = []; + + $totalMonths = $startDate->diffInMonths($endDate) + 1; + + /** + * If the difference between the start and end date is less than 5 months + */ + if ($totalMonths <= 5) { + return $intervals; + } + + for ($i = 0; $i < $totalMonths; $i++) { + $intervalStartDate = clone $startDate; + + $intervalStartDate->addMonths($i); + + $start = $intervalStartDate->startOfDay(); + + $end = ($totalMonths - 1 == $i) + ? $endDate + : $intervalStartDate->addMonth()->subDay()->endOfDay(); + + $intervals[] = [ + 'filter' => $start->month, + 'start' => $start->format('d M'), + 'end' => $end->format('d M'), + ]; + } + + return $intervals; + } + + /** + * Returns time intervals. + * + * @param \Carbon\Carbon $startDate + * @param \Carbon\Carbon $endDate + * @return array + */ + public function getWeeksInterval($startDate, $endDate) + { + $intervals = []; + + $startWeekDay = Carbon::createFromTimeString(core()->xWeekRange($startDate, 0).' 00:00:01'); + + $endWeekDay = Carbon::createFromTimeString(core()->xWeekRange($endDate, 1).' 23:59:59'); + + $totalWeeks = $startWeekDay->diffInWeeks($endWeekDay); + + /** + * If the difference between the start and end date is less than 6 weeks + */ + if ($totalWeeks <= 6) { + return $intervals; + } + + for ($i = 0; $i < $totalWeeks; $i++) { + $intervalStartDate = clone $startDate; + + $intervalStartDate->addWeeks($i); + + $start = $i == 0 + ? $startDate + : Carbon::createFromTimeString(core()->xWeekRange($intervalStartDate, 0).' 00:00:01'); + + $end = ($totalWeeks - 1 == $i) + ? $endDate + : Carbon::createFromTimeString(core()->xWeekRange($intervalStartDate->subDay(), 1).' 23:59:59'); + + $intervals[] = [ + 'filter' => $start->week, + 'start' => $start->format('d M'), + 'end' => $end->format('d M'), + ]; + } + + return $intervals; + } + + /** + * Returns time intervals. + * + * @param \Carbon\Carbon $startDate + * @param \Carbon\Carbon $endDate + * @return array + */ + public function getDaysInterval($startDate, $endDate) + { + $intervals = []; + + $totalDays = $startDate->diffInDays($endDate) + 1; + + for ($i = 0; $i < $totalDays; $i++) { + $intervalStartDate = clone $startDate; + + $intervalStartDate->addDays($i); + + $intervals[] = [ + 'filter' => $intervalStartDate->dayOfYear, + 'start' => $intervalStartDate->startOfDay()->format('d M'), + 'end' => $intervalStartDate->endOfDay()->format('d M'), + ]; + } + + return $intervals; + } +} diff --git a/packages/Webkul/Admin/src/Helpers/Reporting/Activity.php b/packages/Webkul/Admin/src/Helpers/Reporting/Activity.php new file mode 100644 index 0000000..4c5cc72 --- /dev/null +++ b/packages/Webkul/Admin/src/Helpers/Reporting/Activity.php @@ -0,0 +1,5 @@ +pipeline = request('pipeline_id') + ? $this->pipelineRepository->find(request('pipeline_id')) + : null; + + if (! $this->pipeline) { + $this->pipeline = $this->pipelineRepository->getDefaultPipeline(); + } + + $stages = $this->pipeline->stages; + + $this->allStageIds = $stages->pluck('id')->toArray(); + + $this->wonStageIds = $stages->where('code', 'won')->pluck('id')->toArray(); + + $this->lostStageIds = $stages->where('code', 'lost')->pluck('id')->toArray(); + + parent::__construct(); + } + + /** + * Returns current customers over time + * + * @param string $period + */ + public function getTotalLeadsOverTime($period = 'auto'): array + { + $this->stageIds = $this->allStageIds; + + $period = $this->determinePeriod($period); + + return $this->getOverTimeStats($this->startDate, $this->endDate, 'leads.id', 'created_at', $period); + } + + /** + * Returns current customers over time + * + * @param string $period + */ + public function getTotalWonLeadsOverTime($period = 'auto'): array + { + $this->stageIds = $this->wonStageIds; + + $period = $this->determinePeriod($period); + + return $this->getOverTimeStats($this->startDate, $this->endDate, 'leads.id', 'closed_at', $period); + } + + /** + * Returns current customers over time + * + * @param string $period + */ + public function getTotalLostLeadsOverTime($period = 'auto'): array + { + $this->stageIds = $this->lostStageIds; + + $period = $this->determinePeriod($period); + + return $this->getOverTimeStats($this->startDate, $this->endDate, 'leads.id', 'closed_at', $period); + } + + /** + * Determine the appropriate period based on date range + * + * @param string $period + */ + protected function determinePeriod($period = 'auto'): string + { + if ($period !== 'auto') { + return $period; + } + + $diffInDays = $this->startDate->diffInDays($this->endDate); + $diffInMonths = $this->startDate->diffInMonths($this->endDate); + $diffInYears = $this->startDate->diffInYears($this->endDate); + + if ($diffInYears > 3) { + return 'year'; + } elseif ($diffInMonths > 6) { + return 'month'; + } elseif ($diffInDays > 60) { + return 'week'; + } else { + return 'day'; + } + } + + /** + * Retrieves total leads and their progress. + */ + public function getTotalLeadsProgress(): array + { + return [ + 'previous' => $previous = $this->getTotalLeads($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getTotalLeads($this->startDate, $this->endDate), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves total leads by date + * + * @param Carbon $startDate + * @param Carbon $endDate + */ + public function getTotalLeads($startDate, $endDate): int + { + return $this->leadRepository + ->resetModel() + ->where('lead_pipeline_id', $this->pipeline->id) + ->whereBetween('created_at', [$startDate, $endDate]) + ->count(); + } + + /** + * Retrieves average leads per day and their progress. + */ + public function getAverageLeadsPerDayProgress(): array + { + return [ + 'previous' => $previous = $this->getAverageLeadsPerDay($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getAverageLeadsPerDay($this->startDate, $this->endDate), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves average leads per day + * + * @param Carbon $startDate + * @param Carbon $endDate + */ + public function getAverageLeadsPerDay($startDate, $endDate): float + { + $days = $startDate->diffInDays($endDate); + + if ($days == 0) { + return 0; + } + + return $this->getTotalLeads($startDate, $endDate) / $days; + } + + /** + * Retrieves total lead value and their progress. + */ + public function getTotalLeadValueProgress(): array + { + return [ + 'previous' => $previous = $this->getTotalLeadValue($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getTotalLeadValue($this->startDate, $this->endDate), + 'formatted_total' => core()->formatBasePrice($current), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves total lead value + * + * @param Carbon $startDate + * @param Carbon $endDate + */ + public function getTotalLeadValue($startDate, $endDate): float + { + return $this->leadRepository + ->resetModel() + ->where('lead_pipeline_id', $this->pipeline->id) + ->whereBetween('created_at', [$startDate, $endDate]) + ->sum('lead_value'); + } + + /** + * Retrieves average lead value and their progress. + */ + public function getAverageLeadValueProgress(): array + { + return [ + 'previous' => $previous = $this->getAverageLeadValue($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getAverageLeadValue($this->startDate, $this->endDate), + 'formatted_total' => core()->formatBasePrice($current), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves average lead value + * + * @param Carbon $startDate + * @param Carbon $endDate + */ + public function getAverageLeadValue($startDate, $endDate): float + { + return $this->leadRepository + ->resetModel() + ->where('lead_pipeline_id', $this->pipeline->id) + ->whereBetween('created_at', [$startDate, $endDate]) + ->avg('lead_value') ?? 0; + } + + /** + * Retrieves total won lead value and their progress. + */ + public function getTotalWonLeadValueProgress(): array + { + return [ + 'previous' => $previous = $this->getTotalWonLeadValue($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getTotalWonLeadValue($this->startDate, $this->endDate), + 'formatted_total' => core()->formatBasePrice($current), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves average won lead value + * + * @param Carbon $startDate + * @param Carbon $endDate + * @return array + */ + public function getTotalWonLeadValue($startDate, $endDate): ?float + { + return $this->leadRepository + ->resetModel() + ->whereIn('lead_pipeline_stage_id', $this->wonStageIds) + ->whereBetween('created_at', [$startDate, $endDate]) + ->sum('lead_value'); + } + + /** + * Retrieves average lost lead value and their progress. + */ + public function getTotalLostLeadValueProgress(): array + { + return [ + 'previous' => $previous = $this->getTotalLostLeadValue($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getTotalLostLeadValue($this->startDate, $this->endDate), + 'formatted_total' => core()->formatBasePrice($current), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves average lost lead value + * + * @param Carbon $startDate + * @param Carbon $endDate + * @return array + */ + public function getTotalLostLeadValue($startDate, $endDate): ?float + { + return $this->leadRepository + ->resetModel() + ->whereIn('lead_pipeline_stage_id', $this->lostStageIds) + ->whereBetween('created_at', [$startDate, $endDate]) + ->sum('lead_value'); + } + + /** + * Retrieves total lead value by sources. + */ + public function getTotalWonLeadValueBySources() + { + return $this->leadRepository + ->resetModel() + ->select( + 'lead_sources.name', + DB::raw('SUM(lead_value) as total') + ) + ->leftJoin('lead_sources', 'leads.lead_source_id', '=', 'lead_sources.id') + ->whereIn('lead_pipeline_stage_id', $this->wonStageIds) + ->whereBetween('leads.created_at', [$this->startDate, $this->endDate]) + ->groupBy('lead_source_id') + ->get(); + } + + /** + * Retrieves total lead value by types. + */ + public function getTotalWonLeadValueByTypes() + { + return $this->leadRepository + ->resetModel() + ->select( + 'lead_types.name', + DB::raw('SUM(lead_value) as total') + ) + ->leftJoin('lead_types', 'leads.lead_type_id', '=', 'lead_types.id') + ->whereIn('lead_pipeline_stage_id', $this->wonStageIds) + ->whereBetween('leads.created_at', [$this->startDate, $this->endDate]) + ->groupBy('lead_type_id') + ->get(); + } + + /** + * Retrieves open leads by states. + */ + public function getOpenLeadsByStates() + { + return $this->leadRepository + ->resetModel() + ->select( + 'lead_pipeline_stages.name', + DB::raw('COUNT(lead_value) as total') + ) + ->leftJoin('lead_pipeline_stages', 'leads.lead_pipeline_stage_id', '=', 'lead_pipeline_stages.id') + ->whereNotIn('lead_pipeline_stage_id', $this->wonStageIds) + ->whereNotIn('lead_pipeline_stage_id', $this->lostStageIds) + ->whereBetween('leads.created_at', [$this->startDate, $this->endDate]) + ->groupBy('lead_pipeline_stage_id') + ->orderByDesc('total') + ->get(); + } + + /** + * Returns over time stats. + * + * @param Carbon $startDate + * @param Carbon $endDate + * @param string $valueColumn + * @param string $dateColumn + * @param string $period + */ + public function getOverTimeStats($startDate, $endDate, $valueColumn, $dateColumn = 'created_at', $period = 'auto'): array + { + $period = $this->determinePeriod($period); + + $intervals = $this->generateTimeIntervals($startDate, $endDate, $period); + + $groupColumn = $this->getGroupColumn($dateColumn, $period); + + $query = $this->leadRepository + ->resetModel() + ->select( + DB::raw("$groupColumn AS date"), + DB::raw('COUNT(DISTINCT id) AS count'), + DB::raw('SUM('.\DB::getTablePrefix()."$valueColumn) AS total") + ) + ->whereIn('lead_pipeline_stage_id', $this->stageIds) + ->whereBetween($dateColumn, [$startDate, $endDate]) + ->groupBy(DB::raw($groupColumn)) + ->orderBy(DB::raw($groupColumn)); + + $results = $query->get(); + $resultLookup = $results->keyBy('date'); + + $stats = []; + + foreach ($intervals as $interval) { + $result = $resultLookup->get($interval['key']); + + $stats[] = [ + 'label' => $interval['label'], + 'count' => $result ? (int) $result->count : 0, + 'total' => $result ? (float) $result->total : 0, + ]; + } + + return $stats; + } + + /** + * Generate time intervals based on period + */ + protected function generateTimeIntervals(Carbon $startDate, Carbon $endDate, string $period): array + { + $intervals = []; + $current = $startDate->copy(); + + while ($current <= $endDate) { + $interval = [ + 'key' => $this->formatDateForGrouping($current, $period), + 'label' => $this->formatDateForLabel($current, $period), + ]; + + $intervals[] = $interval; + + switch ($period) { + case 'day': + $current->addDay(); + + break; + case 'week': + $current->addWeek(); + + break; + case 'month': + $current->addMonth(); + + break; + case 'year': + $current->addYear(); + + break; + } + } + + return $intervals; + } + + /** + * Get the SQL group column based on period + */ + protected function getGroupColumn(string $dateColumn, string $period): string + { + switch ($period) { + case 'day': + return "DATE($dateColumn)"; + case 'week': + return "DATE_FORMAT($dateColumn, '%Y-%u')"; + case 'month': + return "DATE_FORMAT($dateColumn, '%Y-%m')"; + case 'year': + return "YEAR($dateColumn)"; + default: + return "DATE($dateColumn)"; + } + } + + /** + * Format date for grouping key + */ + protected function formatDateForGrouping(Carbon $date, string $period): string + { + switch ($period) { + case 'day': + return $date->format('Y-m-d'); + case 'week': + return $date->format('Y-W'); + case 'month': + return $date->format('Y-m'); + case 'year': + return $date->format('Y'); + default: + return $date->format('Y-m-d'); + } + } + + /** + * Format date for display label + */ + protected function formatDateForLabel(Carbon $date, string $period): string + { + switch ($period) { + case 'day': + return $date->format('M d'); + case 'week': + return 'Week '.$date->format('W, Y'); + case 'month': + return $date->format('M Y'); + case 'year': + return $date->format('Y'); + default: + return $date->format('M d'); + } + } +} diff --git a/packages/Webkul/Admin/src/Helpers/Reporting/Organization.php b/packages/Webkul/Admin/src/Helpers/Reporting/Organization.php new file mode 100644 index 0000000..e566c05 --- /dev/null +++ b/packages/Webkul/Admin/src/Helpers/Reporting/Organization.php @@ -0,0 +1,81 @@ + $previous = $this->getTotalOrganizations($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getTotalOrganizations($this->startDate, $this->endDate), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves total organizations by date + * + * @param Carbon $startDate + * @param Carbon $endDate + */ + public function getTotalOrganizations($startDate, $endDate): int + { + return $this->organizationRepository + ->resetModel() + ->whereBetween('created_at', [$startDate, $endDate]) + ->count(); + } + + /** + * Gets top customers by revenue. + * + * @param int $limit + */ + public function getTopOrganizationsByRevenue($limit = null): Collection + { + $tablePrefix = DB::getTablePrefix(); + + $items = $this->organizationRepository + ->resetModel() + ->leftJoin('persons', 'organizations.id', '=', 'persons.organization_id') + ->leftJoin('leads', 'persons.id', '=', 'leads.person_id') + ->select('*', 'persons.id as id') + ->addSelect(DB::raw('SUM('.$tablePrefix.'leads.lead_value) as revenue')) + ->whereBetween('leads.closed_at', [$this->startDate, $this->endDate]) + ->having(DB::raw('SUM('.$tablePrefix.'leads.lead_value)'), '>', 0) + ->groupBy('organization_id') + ->orderBy('revenue', 'DESC') + ->limit($limit) + ->get(); + + $items = $items->map(function ($item) { + return [ + 'id' => $item->id, + 'name' => $item->name, + 'revenue' => $item->revenue, + 'formatted_revenue' => core()->formatBasePrice($item->revenue), + ]; + }); + + return $items; + } +} diff --git a/packages/Webkul/Admin/src/Helpers/Reporting/Person.php b/packages/Webkul/Admin/src/Helpers/Reporting/Person.php new file mode 100644 index 0000000..f337ea7 --- /dev/null +++ b/packages/Webkul/Admin/src/Helpers/Reporting/Person.php @@ -0,0 +1,82 @@ + $previous = $this->getTotalPersons($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getTotalPersons($this->startDate, $this->endDate), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves total persons by date + * + * @param Carbon $startDate + * @param Carbon $endDate + */ + public function getTotalPersons($startDate, $endDate): int + { + return $this->personRepository + ->resetModel() + ->whereBetween('created_at', [$startDate, $endDate]) + ->count(); + } + + /** + * Gets top customers by revenue. + * + * @param int $limit + */ + public function getTopCustomersByRevenue($limit = null): Collection + { + $tablePrefix = DB::getTablePrefix(); + + $items = $this->personRepository + ->resetModel() + ->leftJoin('leads', 'persons.id', '=', 'leads.person_id') + ->select('*', 'persons.id as id') + ->addSelect(DB::raw('SUM('.$tablePrefix.'leads.lead_value) as revenue')) + ->whereBetween('leads.closed_at', [$this->startDate, $this->endDate]) + ->having(DB::raw('SUM('.$tablePrefix.'leads.lead_value)'), '>', 0) + ->groupBy('person_id') + ->orderBy('revenue', 'DESC') + ->limit($limit) + ->get(); + + $items = $items->map(function ($item) { + return [ + 'id' => $item->id, + 'name' => $item->name, + 'emails' => $item->emails, + 'contact_numbers' => $item->contact_numbers, + 'revenue' => $item->revenue, + 'formatted_revenue' => core()->formatBasePrice($item->revenue), + ]; + }); + + return $items; + } +} diff --git a/packages/Webkul/Admin/src/Helpers/Reporting/Product.php b/packages/Webkul/Admin/src/Helpers/Reporting/Product.php new file mode 100644 index 0000000..f993e33 --- /dev/null +++ b/packages/Webkul/Admin/src/Helpers/Reporting/Product.php @@ -0,0 +1,94 @@ +productRepository + ->resetModel() + ->with('product') + ->leftJoin('leads', 'lead_products.lead_id', '=', 'leads.id') + ->leftJoin('products', 'lead_products.product_id', '=', 'products.id') + ->select('*') + ->addSelect(DB::raw('SUM('.$tablePrefix.'lead_products.amount) as revenue')) + ->whereBetween('leads.closed_at', [$this->startDate, $this->endDate]) + ->having(DB::raw('SUM('.$tablePrefix.'lead_products.amount)'), '>', 0) + ->groupBy('product_id') + ->orderBy('revenue', 'DESC') + ->limit($limit) + ->get(); + + $items = $items->map(function ($item) { + return [ + 'id' => $item->product_id, + 'name' => $item->name, + 'price' => $item->product?->price, + 'formatted_price' => core()->formatBasePrice($item->price), + 'revenue' => $item->revenue, + 'formatted_revenue' => core()->formatBasePrice($item->revenue), + ]; + }); + + return $items; + } + + /** + * Gets top-selling products by quantity. + * + * @param int $limit + */ + public function getTopSellingProductsByQuantity($limit = null): Collection + { + $tablePrefix = DB::getTablePrefix(); + + $items = $this->productRepository + ->resetModel() + ->with('product') + ->leftJoin('leads', 'lead_products.lead_id', '=', 'leads.id') + ->leftJoin('products', 'lead_products.product_id', '=', 'products.id') + ->select('*') + ->addSelect(DB::raw('SUM('.$tablePrefix.'lead_products.quantity) as total_qty_ordered')) + ->whereBetween('leads.closed_at', [$this->startDate, $this->endDate]) + ->having(DB::raw('SUM('.$tablePrefix.'lead_products.quantity)'), '>', 0) + ->groupBy('product_id') + ->orderBy('total_qty_ordered', 'DESC') + ->limit($limit) + ->get(); + + $items = $items->map(function ($item) { + return [ + 'id' => $item->product_id, + 'name' => $item->name, + 'price' => $item->product?->price, + 'formatted_price' => core()->formatBasePrice($item->price), + 'total_qty_ordered' => $item->total_qty_ordered, + ]; + }); + + return $items; + } +} diff --git a/packages/Webkul/Admin/src/Helpers/Reporting/Quote.php b/packages/Webkul/Admin/src/Helpers/Reporting/Quote.php new file mode 100644 index 0000000..133f8b3 --- /dev/null +++ b/packages/Webkul/Admin/src/Helpers/Reporting/Quote.php @@ -0,0 +1,45 @@ + $previous = $this->getTotalQuotes($this->lastStartDate, $this->lastEndDate), + 'current' => $current = $this->getTotalQuotes($this->startDate, $this->endDate), + 'progress' => $this->getPercentageChange($previous, $current), + ]; + } + + /** + * Retrieves total quotes by date + * + * @param Carbon $startDate + * @param Carbon $endDate + */ + public function getTotalQuotes($startDate, $endDate): int + { + return $this->quoteRepository + ->resetModel() + ->whereBetween('created_at', [$startDate, $endDate]) + ->count(); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Activity/ActivityController.php b/packages/Webkul/Admin/src/Http/Controllers/Activity/ActivityController.php new file mode 100755 index 0000000..33b759c --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Activity/ActivityController.php @@ -0,0 +1,286 @@ +user_id], + $activity->participants->pluck('user_id')->filter()->all() + ); + } + + /** + * Display a listing of the resource. + */ + public function index(): View + { + return view('admin::activities.index'); + } + + /** + * Returns a listing of the resource. + */ + public function get(): JsonResponse + { + if (! request()->has('view_type')) { + return datagrid(ActivityDataGrid::class)->process(); + } + + $startDate = request()->get('startDate') + ? Carbon::createFromTimeString(request()->get('startDate').' 00:00:01') + : Carbon::now()->startOfWeek()->format('Y-m-d H:i:s'); + + $endDate = request()->get('endDate') + ? Carbon::createFromTimeString(request()->get('endDate').' 23:59:59') + : Carbon::now()->endOfWeek()->format('Y-m-d H:i:s'); + + $activities = $this->activityRepository->getActivities([$startDate, $endDate])->toArray(); + + return response()->json([ + 'activities' => $activities, + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): RedirectResponse|JsonResponse + { + $this->validate(request(), [ + 'type' => 'required', + 'comment' => 'required_if:type,note', + 'schedule_from' => 'required_unless:type,note,file', + 'schedule_to' => 'required_unless:type,note,file', + 'file' => 'required_if:type,file', + ]); + + if (request('type') === 'meeting') { + /** + * Check if meeting is overlapping with other meetings. + */ + $isOverlapping = $this->activityRepository->isDurationOverlapping( + request()->input('schedule_from'), + request()->input('schedule_to'), + request()->input('participants'), + request()->input('id') + ); + + if ($isOverlapping) { + if (request()->ajax()) { + return response()->json([ + 'message' => trans('admin::app.activities.overlapping-error'), + ], 400); + } + + session()->flash('success', trans('admin::app.activities.overlapping-error')); + + return redirect()->back(); + } + } + + Event::dispatch('activity.create.before'); + + $activity = $this->activityRepository->create(array_merge(request()->all(), [ + 'is_done' => request('type') == 'note' ? 1 : 0, + 'user_id' => auth()->guard('user')->user()->id, + ])); + + Event::dispatch('activity.create.after', $activity); + + if (request()->ajax()) { + return response()->json([ + 'data' => new ActivityResource($activity), + 'message' => trans('admin::app.activities.create-success'), + ]); + } + + session()->flash('success', trans('admin::app.activities.create-success')); + + return redirect()->back(); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View + { + $activity = $this->activityRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($this->activityOwnerIds($activity)); + + $leadId = old('lead_id') ?? optional($activity->leads()->first())->id; + + $lookUpEntityData = $this->attributeRepository->getLookUpEntity('leads', $leadId); + + return view('admin::activities.edit', compact('activity', 'lookUpEntityData')); + } + + /** + * Update the specified resource in storage. + */ + public function update($id): RedirectResponse|JsonResponse + { + $this->preventUnauthorizedAccess($this->activityOwnerIds($this->activityRepository->findOrFail($id))); + + Event::dispatch('activity.update.before', $id); + + $data = request()->all(); + + $activity = $this->activityRepository->update($data, $id); + + /** + * We will not use `empty` directly here because `lead_id` can be a blank string + * from the activity form. However, on the activity view page, we are only updating the + * `is_done` field, so `lead_id` will not be present in that case. + */ + if (isset($data['lead_id'])) { + $activity->leads()->sync( + ! empty($data['lead_id']) + ? [$data['lead_id']] + : [] + ); + } + + Event::dispatch('activity.update.after', $activity); + + if (request()->ajax()) { + return response()->json([ + 'data' => new ActivityResource($activity), + 'message' => trans('admin::app.activities.update-success'), + ]); + } + + session()->flash('success', trans('admin::app.activities.update-success')); + + return redirect()->route('admin.activities.index'); + } + + /** + * Mass Update the specified resources. + */ + public function massUpdate(MassUpdateRequest $massUpdateRequest): JsonResponse + { + $activities = $this->filterAuthorizedRecords( + $this->activityRepository->findWhereIn('id', $massUpdateRequest->input('indices')), + fn ($activity) => $this->activityOwnerIds($activity) + ); + + foreach ($activities as $activity) { + Event::dispatch('activity.update.before', $activity->id); + + $activity = $this->activityRepository->update([ + 'is_done' => $massUpdateRequest->input('value'), + ], $activity->id); + + Event::dispatch('activity.update.after', $activity); + } + + return response()->json([ + 'message' => trans('admin::app.activities.mass-update-success'), + ]); + } + + /** + * Download file from storage. + */ + public function download(int $id): StreamedResponse + { + $file = $this->fileRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($this->activityOwnerIds($file->activity)); + + try { + return Storage::download($file->path); + } catch (\Exception $exception) { + abort(404); + } + } + + /* + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $activity = $this->activityRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($this->activityOwnerIds($activity)); + + try { + Event::dispatch('activity.delete.before', $id); + + $activity?->delete($id); + + Event::dispatch('activity.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.activities.destroy-success'), + ], 200); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.activities.destroy-failed'), + ], 400); + } + } + + /** + * Mass Delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $activities = $this->filterAuthorizedRecords( + $this->activityRepository->findWhereIn('id', $massDestroyRequest->input('indices')), + fn ($activity) => $this->activityOwnerIds($activity) + ); + + try { + foreach ($activities as $activity) { + Event::dispatch('activity.delete.before', $activity->id); + + $this->activityRepository->delete($activity->id); + + Event::dispatch('activity.delete.after', $activity->id); + } + + return response()->json([ + 'message' => trans('admin::app.activities.mass-destroy-success'), + ]); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.activities.mass-delete-failed'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Configuration/ConfigurationController.php b/packages/Webkul/Admin/src/Http/Controllers/Configuration/ConfigurationController.php new file mode 100644 index 0000000..9ac0cc3 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Configuration/ConfigurationController.php @@ -0,0 +1,85 @@ +route('slug') + && request()->route('slug2') + ) { + return view('admin::configuration.edit'); + } + + return view('admin::configuration.index'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(ConfigurationForm $request): RedirectResponse + { + Event::dispatch('core.configuration.save.before'); + + $this->configurationRepository->create($request->all()); + + Event::dispatch('core.configuration.save.after'); + + session()->flash('success', trans('admin::app.configuration.index.save-success')); + + return redirect()->back(); + } + + /** + * download the file for the specified resource. + * + * @return Response + */ + public function download() + { + $path = request()->route()->parameters()['path']; + + $fileName = 'configuration/'.$path; + + $config = $this->configurationRepository->findOneByField('value', $fileName); + + return Storage::download($config['value']); + } + + /** + * Search for configurations. + */ + public function search(): JsonResponse + { + $results = $this->configurationRepository->search( + system_config()->getItems(), + request()->query('query') + ); + + return new JsonResponse([ + 'data' => $results, + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Contact/OrganizationController.php b/packages/Webkul/Admin/src/Http/Controllers/Contact/OrganizationController.php new file mode 100644 index 0000000..4033da8 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Contact/OrganizationController.php @@ -0,0 +1,145 @@ +request->add(['entity_type' => 'organizations']); + } + + /** + * Display a listing of the resource. + */ + public function index(): View|JsonResponse + { + if (request()->ajax()) { + return datagrid(OrganizationDataGrid::class)->process(); + } + + return view('admin::contacts.organizations.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::contacts.organizations.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(AttributeForm $request): RedirectResponse|JsonResponse + { + Event::dispatch('contacts.organization.create.before'); + + $organization = $this->organizationRepository->create(request()->all()); + + Event::dispatch('contacts.organization.create.after', $organization); + + if (request()->ajax()) { + return response()->json([ + 'data' => $organization, + 'message' => trans('admin::app.contacts.organizations.index.create-success'), + ]); + } + + session()->flash('success', trans('admin::app.contacts.organizations.index.create-success')); + + return redirect()->route('admin.contacts.organizations.index'); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View + { + $organization = $this->organizationRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($organization->user_id); + + return view('admin::contacts.organizations.edit', compact('organization')); + } + + /** + * Update the specified resource in storage. + */ + public function update(AttributeForm $request, int $id): RedirectResponse + { + $this->preventUnauthorizedAccess($this->organizationRepository->findOrFail($id)->user_id); + + Event::dispatch('contacts.organization.update.before', $id); + + $organization = $this->organizationRepository->update(request()->all(), $id); + + Event::dispatch('contacts.organization.update.after', $organization); + + session()->flash('success', trans('admin::app.contacts.organizations.index.update-success')); + + return redirect()->route('admin.contacts.organizations.index'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $this->preventUnauthorizedAccess($this->organizationRepository->findOrFail($id)->user_id); + + try { + Event::dispatch('contact.organization.delete.before', $id); + + $this->organizationRepository->delete($id); + + Event::dispatch('contact.organization.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.contacts.organizations.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.contacts.organizations.index.delete-failed'), + ], 400); + } + } + + /** + * Mass Delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $organizations = $this->filterAuthorizedRecords( + $this->organizationRepository->findWhereIn('id', $massDestroyRequest->input('indices')) + ); + + foreach ($organizations as $organization) { + Event::dispatch('contact.organization.delete.before', $organization); + + $this->organizationRepository->delete($organization->id); + + Event::dispatch('contact.organization.delete.after', $organization); + } + + return response()->json([ + 'message' => trans('admin::app.contacts.organizations.index.delete-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/ActivityController.php b/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/ActivityController.php new file mode 100644 index 0000000..cc109fc --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/ActivityController.php @@ -0,0 +1,39 @@ + 'person_activities', 'foreign' => 'person_id']; + } + + /** + * Emails are linked to the person through the `person_id` column. + */ + protected function timelineEmailColumn(): ?string + { + return 'person_id'; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/GoogleContactExportController.php b/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/GoogleContactExportController.php new file mode 100644 index 0000000..44b692d --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/GoogleContactExportController.php @@ -0,0 +1,88 @@ +validate([ + 'filters' => ['sometimes', 'array'], + 'sort' => ['sometimes', 'array'], + ]); + + $userId = auth()->guard('user')->id(); + + if (! $this->googleContactAccountRepository->findOneByField('user_id', $userId)) { + return response()->json([ + 'message' => trans('admin::app.contacts.persons.index.datagrid.google-export-not-connected'), + ], 422); + } + + $personIds = $personDataGrid->getFilteredQueryBuilder()->pluck('persons.id')->all(); + + if (empty($personIds)) { + return response()->json([ + 'message' => trans('admin::app.export.no-records'), + ], 400); + } + + $batch = $this->contactExportBatchRepository->create([ + 'user_id' => $userId, + 'state' => ContactExportBatch::STATE_PENDING, + 'total_count' => count($personIds), + ]); + + PrepareExportBatch::dispatch($batch->id, $personIds); + + return response()->json([ + 'message' => trans('admin::app.contacts.persons.index.datagrid.google-export-started'), + 'batch_id' => $batch->id, + ]); + } + + /** + * Report progress/result counts for a single export batch, polled by the frontend. + */ + public function stats(int $batch): JsonResponse + { + $exportBatch = $this->contactExportBatchRepository->find($batch); + + if (! $exportBatch || $exportBatch->user_id !== auth()->guard('user')->id()) { + abort(404); + } + + return response()->json([ + 'state' => $exportBatch->state, + 'finished' => $exportBatch->isFinished(), + 'total_count' => $exportBatch->total_count, + 'exported_count' => $exportBatch->exported_count, + 'duplicate_count' => $exportBatch->duplicate_count, + 'failed_count' => $exportBatch->failed_count, + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/PersonController.php b/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/PersonController.php new file mode 100644 index 0000000..220c3db --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/PersonController.php @@ -0,0 +1,260 @@ +request->add(['entity_type' => 'persons']); + } + + /** + * Display a listing of the resource. + */ + public function index() + { + if (request()->ajax()) { + return datagrid(PersonDataGrid::class)->process(); + } + + return view('admin::contacts.persons.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::contacts.persons.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(AttributeForm $request): RedirectResponse|JsonResponse + { + Event::dispatch('contacts.person.create.before'); + + $data = $request->all(); + + if (request()->has('quick_add') && empty($data['user_id'])) { + $data['user_id'] = auth()->guard('user')->user()->id; + } + + $person = $this->personRepository->create($data); + + Event::dispatch('contacts.person.create.after', $person); + + if (request()->ajax()) { + return response()->json([ + 'data' => $person, + 'message' => trans('admin::app.contacts.persons.index.create-success'), + ]); + } + + session()->flash('success', trans('admin::app.contacts.persons.index.create-success')); + + return redirect()->route('admin.contacts.persons.index'); + } + + /** + * Display the specified resource. + */ + public function show(int $id): View + { + $person = $this->personRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($person->user_id); + + return view('admin::contacts.persons.view', compact('person')); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View + { + $person = $this->personRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($person->user_id); + + return view('admin::contacts.persons.edit', compact('person')); + } + + /** + * Update the specified resource in storage. + */ + public function update(AttributeForm $request, int $id): RedirectResponse|JsonResponse + { + $this->preventUnauthorizedAccess($this->personRepository->findOrFail($id)->user_id); + + Event::dispatch('contacts.person.update.before', $id); + + $person = $this->personRepository->update($request->all(), $id); + + Event::dispatch('contacts.person.update.after', $person); + + if (request()->ajax()) { + return response()->json([ + 'data' => $person, + 'message' => trans('admin::app.contacts.persons.index.update-success'), + ], 200); + } + + session()->flash('success', trans('admin::app.contacts.persons.index.update-success')); + + return redirect()->route('admin.contacts.persons.index'); + } + + /** + * Search person results. + */ + public function search(): JsonResource + { + $personRepository = $this->personRepository + ->pushCriteria(app(RequestCriteria::class)); + + if ($searchTerm = request()->query('query')) { + $personRepository = $personRepository->scopeQuery(function ($query) use ($searchTerm) { + return $query->where(function ($q) use ($searchTerm) { + $q->where('name', 'like', '%'.$searchTerm.'%') + ->orWhere('emails', 'like', '%'.$searchTerm.'%') + ->orWhere('contact_numbers', 'like', '%'.$searchTerm.'%'); + }); + }); + } + + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $persons = $personRepository->findWhereIn('user_id', $userIds); + } else { + $persons = $personRepository->all(); + } + + return PersonResource::collection($persons); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $person = $this->personRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($person->user_id); + + if ( + $person->leads + && $person->leads->count() > 0 + ) { + return response()->json([ + 'message' => trans('admin::app.contacts.persons.index.delete-failed'), + ], 400); + } + + try { + Event::dispatch('contacts.person.delete.before', $person); + + $person->delete(); + + Event::dispatch('contacts.person.delete.after', $person); + + return response()->json([ + 'message' => trans('admin::app.contacts.persons.index.delete-success'), + ], 200); + + } catch (Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.contacts.persons.index.delete-failed'), + ], 400); + } + } + + /** + * Mass destroy the specified resources from storage. + */ + public function massDestroy(MassDestroyRequest $request): JsonResponse + { + try { + $persons = $this->filterAuthorizedRecords( + $this->personRepository->findWhereIn('id', $request->input('indices', [])) + ); + + $deletedCount = 0; + + $blockedCount = 0; + + foreach ($persons as $person) { + if ( + $person->leads + && $person->leads->count() > 0 + ) { + $blockedCount++; + + continue; + } + + Event::dispatch('contact.person.delete.before', $person); + + $this->personRepository->delete($person->id); + + Event::dispatch('contact.person.delete.after', $person); + + $deletedCount++; + } + + $statusCode = 200; + + switch (true) { + case $deletedCount > 0 && $blockedCount === 0: + $message = trans('admin::app.contacts.persons.index.all-delete-success'); + + break; + + case $deletedCount > 0 && $blockedCount > 0: + $message = trans('admin::app.contacts.persons.index.partial-delete-warning'); + + break; + + case $deletedCount === 0 && $blockedCount > 0: + $message = trans('admin::app.contacts.persons.index.none-delete-warning'); + + $statusCode = 400; + + break; + + default: + $message = trans('admin::app.contacts.persons.index.no-selection'); + + $statusCode = 400; + + break; + } + + return response()->json(['message' => $message], $statusCode); + } catch (Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.contacts.persons.index.delete-failed'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/TagController.php b/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/TagController.php new file mode 100644 index 0000000..232b1ec --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Contact/Persons/TagController.php @@ -0,0 +1,56 @@ +personRepository->find($id); + + if (! $person->tags->contains(request()->input('tag_id'))) { + $person->tags()->attach(request()->input('tag_id')); + } + + Event::dispatch('persons.tag.create.after', $person); + + return response()->json([ + 'message' => trans('admin::app.contacts.persons.view.tags.create-success'), + ]); + } + + /** + * Remove the specified resource from storage. + */ + public function detach(int $personId): JsonResponse + { + Event::dispatch('persons.tag.delete.before', $personId); + + $person = $this->personRepository->find($personId); + + $person->tags()->detach(request()->input('tag_id')); + + Event::dispatch('persons.tag.delete.after', $person); + + return response()->json([ + 'message' => trans('admin::app.contacts.persons.view.tags.destroy-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Controller.php b/packages/Webkul/Admin/src/Http/Controllers/Controller.php new file mode 100755 index 0000000..7927877 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Controller.php @@ -0,0 +1,67 @@ +route('admin.session.create'); + } + + /** + * Prevent access to a record that is not owned by (or shared with) the current user. + * + * @param int|array|null $ownerIds The user id (or ids, e.g. owner + participants) tied to the record. + * @return void + */ + protected function preventUnauthorizedAccess($ownerIds) + { + $userIds = bouncer()->getAuthorizedUserIds(); + + if ($userIds === null) { + return; + } + + if (empty(array_intersect((array) $ownerIds, $userIds))) { + abort(401, trans('admin::app.errors.unauthorized')); + } + } + + /** + * Filter a collection of records down to the ones the current user is authorized to manage. + * + * @param Collection $records + * @param callable|null $ownerIdsResolver Resolves the owning user id(s) for a record; defaults to `user_id`. + * @return Collection + */ + protected function filterAuthorizedRecords($records, ?callable $ownerIdsResolver = null) + { + $userIds = bouncer()->getAuthorizedUserIds(); + + if ($userIds === null) { + return $records; + } + + return $records->filter(function ($record) use ($userIds, $ownerIdsResolver) { + $ownerIds = $ownerIdsResolver + ? (array) $ownerIdsResolver($record) + : [$record->user_id]; + + return ! empty(array_intersect($ownerIds, $userIds)); + })->values(); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/DashboardController.php b/packages/Webkul/Admin/src/Http/Controllers/DashboardController.php new file mode 100755 index 0000000..2f20dc0 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/DashboardController.php @@ -0,0 +1,131 @@ + 'getOverAllStats', + 'revenue-stats' => 'getRevenueStats', + 'total-leads' => 'getTotalLeadsStats', + 'revenue-by-sources' => 'getLeadsStatsBySources', + 'revenue-by-types' => 'getLeadsStatsByTypes', + 'top-selling-products' => 'getTopSellingProducts', + 'top-persons' => 'getTopPersons', + 'open-leads-by-states' => 'getOpenLeadsByStates', + ]; + + /** + * Create a new controller instance. + * + * @return void + */ + public function __construct( + protected Dashboard $dashboardHelper, + protected PipelineRepository $pipelineRepository + ) {} + + /** + * Display a listing of the resource. + * + * @return View + */ + public function index() + { + return view('admin::dashboard.index')->with([ + 'startDate' => $this->dashboardHelper->getStartDate(), + 'endDate' => $this->dashboardHelper->getEndDate(), + 'pipelines' => $this->pipelineRepository->all(), + 'defaultPipeline' => $this->pipelineRepository->getDefaultPipeline(), + ]); + } + + /** + * Display a listing of the resource. + * + * @return JsonResponse + */ + public function stats() + { + $stats = $this->dashboardHelper->{$this->typeFunctions[request()->query('type')]}(); + + return response()->json([ + 'statistics' => $stats, + 'date_range' => $this->dashboardHelper->getDateRange(), + ]); + } + + /** + * Export the dashboard report to PDF for the currently applied filters. + */ + public function exportPdf(): Response|StreamedResponse + { + $revenueStats = $this->dashboardHelper->getRevenueStats(); + + $overAllStats = $this->dashboardHelper->getOverAllStats(); + + $totalLeadsStats = $this->dashboardHelper->getTotalLeadsStats(); + + $revenueBySources = $this->dashboardHelper->getLeadsStatsBySources(); + + $revenueByTypes = $this->dashboardHelper->getLeadsStatsByTypes(); + + $openLeadsByStates = $this->dashboardHelper->getOpenLeadsByStates(); + + $topSellingProducts = $this->dashboardHelper->getTopSellingProducts(); + + $topPersons = $this->dashboardHelper->getTopPersons(); + + return $this->downloadPDF( + view('admin::dashboard.pdf', [ + 'logo' => $this->getLogoDataUri(), + 'startDate' => $this->dashboardHelper->getStartDate(), + 'endDate' => $this->dashboardHelper->getEndDate(), + 'revenueStats' => $revenueStats, + 'overAllStats' => $overAllStats, + 'totalLeadsStats' => $totalLeadsStats, + 'revenueBySources' => $revenueBySources, + 'revenueByTypes' => $revenueByTypes, + 'openLeadsByStates' => $openLeadsByStates, + 'topSellingProducts' => $topSellingProducts, + 'topPersons' => $topPersons, + ])->render(), + 'Dashboard_Report_'.now()->format('d-m-Y') + ); + } + + /** + * Resolve the configured admin logo as a base64 data URI so dompdf can + * render it regardless of the storage disk or symlink configuration. + */ + private function getLogoDataUri(): ?string + { + $path = core()->getConfigData('general.general.admin_logo.logo_image'); + + if (! $path || ! Storage::disk('public')->exists($path)) { + return null; + } + + $mimeType = Storage::disk('public')->mimeType($path); + + $contents = base64_encode(Storage::disk('public')->get($path)); + + return "data:{$mimeType};base64,{$contents}"; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/DataGrid/SavedFilterController.php b/packages/Webkul/Admin/src/Http/Controllers/DataGrid/SavedFilterController.php new file mode 100644 index 0000000..abb43a0 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/DataGrid/SavedFilterController.php @@ -0,0 +1,117 @@ +guard()->user()->id; + + $this->validate(request(), [ + 'name' => 'required|unique:datagrid_saved_filters,name,NULL,id,src,'.request('src').',user_id,'.$userId, + ]); + + Event::dispatch('datagrid.saved_filter.create.before'); + + $savedFilter = $this->savedFilterRepository->create([ + 'user_id' => $userId, + 'name' => request('name'), + 'src' => request('src'), + 'applied' => request('applied'), + ]); + + Event::dispatch('datagrid.saved_filter.create.after', $savedFilter); + + return response()->json([ + 'data' => $savedFilter, + 'message' => trans('admin::app.components.datagrid.toolbar.filter.saved-success'), + ]); + } + + /** + * Retrieves the saved filters. + */ + public function get() + { + $savedFilters = $this->savedFilterRepository->findWhere([ + 'src' => request()->get('src'), + 'user_id' => auth()->guard()->user()->id, + ]); + + return response()->json(['data' => $savedFilters]); + } + + /** + * Update the saved filter. + */ + public function update(int $id) + { + $userId = auth()->guard()->user()->id; + + $this->validate(request(), [ + 'name' => 'required|unique:datagrid_saved_filters,name,'.$id.',id,src,'.request('src').',user_id,'.$userId, + ]); + + $savedFilter = $this->savedFilterRepository->findOneWhere([ + 'id' => $id, + 'user_id' => auth()->guard()->user()->id, + ]); + + if (! $savedFilter) { + return response()->json([], 404); + } + + Event::dispatch('datagrid.saved_filter.update.before', $id); + + $updatedFilter = $this->savedFilterRepository->update(request()->only([ + 'name', + 'src', + 'applied', + ]), $id); + + Event::dispatch('datagrid.saved_filter.update.after', $updatedFilter); + + return response()->json([ + 'data' => $updatedFilter, + 'message' => trans('admin::app.components.datagrid.toolbar.filter.updated-success'), + ]); + } + + /** + * Delete the saved filter. + */ + public function destroy(int $id) + { + Event::dispatch('datagrid.saved_filter.delete.before', $id); + + $success = $this->savedFilterRepository->deleteWhere([ + 'id' => $id, + 'user_id' => auth()->guard()->user()->id, + ]); + + Event::dispatch('datagrid.saved_filter.delete.after', $id); + + if (! $success) { + return response()->json([ + 'message' => trans('admin::app.components.datagrid.toolbar.filter.delete-error'), + ]); + } + + return response()->json([ + 'message' => trans('admin::app.components.datagrid.toolbar.filter.delete-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/DataGridController.php b/packages/Webkul/Admin/src/Http/Controllers/DataGridController.php new file mode 100644 index 0000000..855ec25 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/DataGridController.php @@ -0,0 +1,42 @@ +validate(request(), [ + 'datagrid_id' => ['required'], + 'column' => ['required'], + 'search' => ['required', 'min:2'], + ]); + + /** + * Preparing the datagrid instance and only columns. + */ + $datagrid = app(Crypt::decryptString($params['datagrid_id'])); + $datagrid->prepareColumns(); + + /** + * Finding the first column from the collection. + */ + $column = collect($datagrid->getColumns())->map(fn ($column) => $column->toArray())->where('index', $params['column'])->firstOrFail(); + + /** + * Fetching on the basis of column options. + */ + return app($column['filterable_options']['repository']) + ->select([$column['filterable_options']['column']['label'].' as label', $column['filterable_options']['column']['value'].' as value']) + ->where($column['filterable_options']['column']['label'], 'LIKE', '%'.$params['search'].'%') + ->get(); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/HelpController.php b/packages/Webkul/Admin/src/Http/Controllers/HelpController.php new file mode 100644 index 0000000..6b176c9 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/HelpController.php @@ -0,0 +1,18 @@ + config('support'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Lead/ActivityController.php b/packages/Webkul/Admin/src/Http/Controllers/Lead/ActivityController.php new file mode 100644 index 0000000..9983075 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Lead/ActivityController.php @@ -0,0 +1,39 @@ + 'lead_activities', 'foreign' => 'lead_id']; + } + + /** + * Emails are linked to the lead through the `lead_id` column. + */ + protected function timelineEmailColumn(): ?string + { + return 'lead_id'; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Lead/EmailController.php b/packages/Webkul/Admin/src/Http/Controllers/Lead/EmailController.php new file mode 100644 index 0000000..647e3cc --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Lead/EmailController.php @@ -0,0 +1,84 @@ +getContent(), true); + + return response()->json([ + 'data' => $this->transformToActivity($response['data']), + 'message' => $response['message'], + ]); + + return $response; + } + + /** + * Store a newly created resource in storage. + * + * @param int $id + * @return Response + */ + public function detach($id) + { + Event::dispatch('email.update.before', request()->input('email_id')); + + $email = $this->emailRepository->update([ + 'lead_id' => null, + ], request()->input('email_id')); + + Event::dispatch('email.update.after', $email); + + return response()->json([ + 'message' => trans('admin::app.mail.update-success'), + ]); + } + + /** + * Transform the email data to activity resource. + * + * @param array $data + * @return ActivityResource + */ + public function transformToActivity($data) + { + return new ActivityResource((object) [ + 'id' => $data['id'], + 'parent_id' => $data['parent_id'], + 'title' => $data['subject'], + 'type' => 'email', + 'is_done' => 1, + 'comment' => $data['reply'], + 'schedule_from' => null, + 'schedule_to' => null, + 'user' => auth()->guard('user')->user(), + 'participants' => [], + 'location' => null, + 'additional' => json_encode([ + 'folders' => $data['folders'], + 'from' => $data['from'], + 'to' => $data['reply_to'], + 'cc' => $data['cc'], + 'bcc' => $data['bcc'], + ]), + 'files' => array_map(function ($attachment) { + return (object) $attachment; + }, $data['attachments']), + 'created_at' => $data['created_at'], + 'updated_at' => $data['updated_at'], + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Lead/LeadController.php b/packages/Webkul/Admin/src/Http/Controllers/Lead/LeadController.php new file mode 100755 index 0000000..f4105ff --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Lead/LeadController.php @@ -0,0 +1,848 @@ +request->add(['entity_type' => 'leads']); + } + + /** + * Display a listing of the resource. + */ + public function index() + { + if (request()->ajax()) { + return datagrid(LeadDataGrid::class)->process(); + } + + if (request('pipeline_id')) { + $pipeline = $this->pipelineRepository->find(request('pipeline_id')); + } else { + $pipeline = $this->pipelineRepository->getDefaultPipeline(); + } + + return view('admin::leads.index', [ + 'pipeline' => $pipeline, + 'columns' => $this->getKanbanColumns(), + ]); + } + + /** + * Returns a listing of the resource. + */ + public function get(): JsonResponse + { + if (request()->query('pipeline_id')) { + $pipeline = $this->pipelineRepository->find(request()->query('pipeline_id')); + } else { + $pipeline = $this->pipelineRepository->getDefaultPipeline(); + } + + if (request()->query('pipeline_stage_id')) { + $stages = $pipeline->stages->where('id', request()->query('pipeline_stage_id')); + } else { + $stages = $pipeline->stages; + } + + foreach ($stages as $stage) { + /** + * We have to create a new instance of the lead repository every time, which is + * why we're not using the injected one. + */ + $query = app(LeadRepository::class) + ->pushCriteria(app(RequestCriteria::class)) + ->where([ + 'lead_pipeline_id' => $pipeline->id, + 'lead_pipeline_stage_id' => $stage->id, + ]); + + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $query->whereIn('leads.user_id', $userIds); + } + + $this->applyDateRangeFilters($query); + + $stage->lead_value = (clone $query)->sum('lead_value'); + + $data[$stage->sort_order] = (new StageResource($stage))->jsonSerialize(); + + $data[$stage->sort_order]['leads'] = [ + 'data' => LeadResource::collection($paginator = $query->with([ + 'tags', + 'type', + 'source', + 'user', + 'person', + 'person.organization', + 'pipeline', + 'pipeline.stages', + 'stage', + 'attribute_values', + ])->orderBy('updated_at', 'desc')->paginate(10)), + + 'meta' => [ + 'current_page' => $paginator->currentPage(), + 'from' => $paginator->firstItem(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'to' => $paginator->lastItem(), + 'total' => $paginator->total(), + ], + ]; + } + + return response()->json($data); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + $attributes = $this->attributeRepository + ->where('entity_type', 'leads') + ->where(function ($query) { + $query->whereIn('code', ['description', 'title', 'lead_value', 'lead_type_id', 'lead_source_id', 'expected_close_date', 'user_id']) + ->orWhere('is_user_defined', 1); + }) + ->orderBy('sort_order', 'asc') + ->get(); + + return view('admin::leads.create', compact('attributes')); + } + + /** + * Store a newly created resource in storage. + */ + public function store(LeadForm $request): RedirectResponse|JsonResponse + { + Event::dispatch('lead.create.before'); + + $data = $request->all(); + + $data['status'] = 1; + + if (request()->has('quick_add') && empty($data['user_id'])) { + $data['user_id'] = auth()->guard('user')->user()->id; + } + + if (! empty($data['lead_pipeline_stage_id'])) { + $stage = $this->stageRepository->findOrFail($data['lead_pipeline_stage_id']); + + $data['lead_pipeline_id'] = $stage->lead_pipeline_id; + } else { + if (empty($data['lead_pipeline_id'])) { + $pipeline = $this->pipelineRepository->getDefaultPipeline(); + + $data['lead_pipeline_id'] = $pipeline->id; + } else { + $pipeline = $this->pipelineRepository->findOrFail($data['lead_pipeline_id']); + } + + $stage = $pipeline->stages()->first(); + + $data['lead_pipeline_stage_id'] = $stage->id; + } + + if (in_array($stage->code, ['won', 'lost'])) { + $data['closed_at'] = Carbon::now(); + } + + $lead = $this->leadRepository->create($data); + + if (request()->ajax()) { + return response()->json([ + 'message' => trans('admin::app.leads.create-success'), + 'data' => new LeadResource($lead), + ]); + } + + Event::dispatch('lead.create.after', $lead); + + session()->flash('success', trans('admin::app.leads.create-success')); + + if (! empty($data['lead_pipeline_id'])) { + $params['pipeline_id'] = $data['lead_pipeline_id']; + } + + return redirect()->route('admin.leads.index', $params ?? []); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View + { + $attributes = $this->attributeRepository + ->where('entity_type', 'leads') + ->where(function ($query) { + $query->whereIn('code', ['description', 'title', 'lead_value', 'lead_type_id', 'lead_source_id', 'expected_close_date', 'user_id']) + ->orWhere('is_user_defined', 1); + }) + ->orderBy('sort_order', 'asc') + ->get(); + + $lead = $this->leadRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($lead->user_id); + + return view('admin::leads.edit', compact('lead', 'attributes')); + } + + /** + * Display a resource. + */ + public function view(int $id) + { + $lead = $this->leadRepository->findOrFail($id); + + $userIds = bouncer()->getAuthorizedUserIds(); + + if ( + $userIds + && ! in_array($lead->user_id, $userIds) + ) { + return redirect()->route('admin.leads.index'); + } + + return view('admin::leads.view', compact('lead')); + } + + /** + * Update the specified resource in storage. + */ + public function update(LeadForm $request, int $id): RedirectResponse|JsonResponse + { + $this->preventUnauthorizedAccess($this->leadRepository->findOrFail($id)->user_id); + + Event::dispatch('lead.update.before', $id); + + $data = $request->all(); + + if (isset($data['lead_pipeline_stage_id'])) { + $stage = $this->stageRepository->findOrFail($data['lead_pipeline_stage_id']); + + $data['lead_pipeline_id'] = $stage->lead_pipeline_id; + } else { + $pipeline = $this->pipelineRepository->getDefaultPipeline(); + + $stage = $pipeline->stages()->first(); + + $data['lead_pipeline_id'] = $pipeline->id; + + $data['lead_pipeline_stage_id'] = $stage->id; + } + + $lead = $this->leadRepository->update($data, $id); + + Event::dispatch('lead.update.after', $lead); + + if (request()->ajax()) { + return response()->json([ + 'message' => trans('admin::app.leads.update-success'), + ]); + } + + session()->flash('success', trans('admin::app.leads.update-success')); + + if (request()->has('closed_at')) { + return redirect()->back(); + } else { + return redirect()->route('admin.leads.index', $data['lead_pipeline_id']); + } + } + + /** + * Update the lead attributes. + */ + public function updateAttributes(int $id) + { + $this->preventUnauthorizedAccess($this->leadRepository->findOrFail($id)->user_id); + + $data = request()->all(); + + $attributes = $this->attributeRepository->findWhere([ + 'entity_type' => 'leads', + ['code', 'NOTIN', ['title', 'description']], + ]); + + Event::dispatch('lead.update.before', $id); + + $lead = $this->leadRepository->update($data, $id, $attributes); + + Event::dispatch('lead.update.after', $lead); + + return response()->json([ + 'message' => trans('admin::app.leads.update-success'), + ]); + } + + /** + * Update the lead stage. + */ + public function updateStage(int $id) + { + $this->validate(request(), [ + 'lead_pipeline_stage_id' => 'required', + ]); + + $lead = $this->leadRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($lead->user_id); + + $stage = $lead->pipeline->stages() + ->where('id', request()->input('lead_pipeline_stage_id')) + ->firstOrFail(); + + Event::dispatch('lead.update.before', $id); + + $payload = request()->merge([ + 'entity_type' => 'leads', + 'lead_pipeline_stage_id' => $stage->id, + ])->only([ + 'closed_at', + 'lost_reason', + 'lead_pipeline_stage_id', + 'entity_type', + ]); + + $lead = $this->leadRepository->update($payload, $id, ['lead_pipeline_stage_id']); + + Event::dispatch('lead.update.after', $lead); + + return response()->json([ + 'message' => trans('admin::app.leads.update-success'), + ]); + } + + /** + * Search person results. + */ + public function search(): AnonymousResourceCollection + { + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $results = $this->leadRepository + ->pushCriteria(app(RequestCriteria::class)) + ->findWhereIn('user_id', $userIds); + } else { + $results = $this->leadRepository + ->pushCriteria(app(RequestCriteria::class)) + ->all(); + } + + return LeadResource::collection($results); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $this->preventUnauthorizedAccess($this->leadRepository->findOrFail($id)->user_id); + + try { + Event::dispatch('lead.delete.before', $id); + + $this->leadRepository->delete($id); + + Event::dispatch('lead.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.leads.destroy-success'), + ]); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.leads.destroy-failed'), + ], 400); + } + } + + /** + * Mass update the specified resources. + */ + public function massUpdate(MassUpdateRequest $massUpdateRequest): JsonResponse + { + $leads = $this->filterAuthorizedRecords( + $this->leadRepository->findWhereIn('id', $massUpdateRequest->input('indices')) + ); + + try { + foreach ($leads as $lead) { + Event::dispatch('lead.update.before', $lead->id); + + $lead = $this->leadRepository->find($lead->id); + + $lead?->update(['lead_pipeline_stage_id' => $massUpdateRequest->input('value')]); + + Event::dispatch('lead.update.before', $lead->id); + } + + return response()->json([ + 'message' => trans('admin::app.leads.update-success'), + ]); + } catch (\Exception $th) { + return response()->json([ + 'message' => trans('admin::app.leads.update-failed'), + ], 400); + } + } + + /** + * Mass delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $leads = $this->filterAuthorizedRecords( + $this->leadRepository->findWhereIn('id', $massDestroyRequest->input('indices')) + ); + + try { + foreach ($leads as $lead) { + Event::dispatch('lead.delete.before', $lead->id); + + $this->leadRepository->delete($lead->id); + + Event::dispatch('lead.delete.after', $lead->id); + } + + return response()->json([ + 'message' => trans('admin::app.leads.destroy-success'), + ]); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.leads.destroy-failed'), + ]); + } + } + + /** + * Attach product to lead. + */ + public function addProduct(int $leadId): JsonResponse + { + $this->preventUnauthorizedAccess($this->leadRepository->findOrFail($leadId)->user_id); + + $product = $this->productRepository->updateOrCreate( + [ + 'lead_id' => $leadId, + 'product_id' => request()->input('product_id'), + ], + array_merge( + request()->all(), + [ + 'lead_id' => $leadId, + 'amount' => request()->input('price') * request()->input('quantity'), + ], + ) + ); + + return response()->json([ + 'data' => $product, + 'message' => trans('admin::app.leads.update-success'), + ]); + } + + /** + * Remove product attached to lead. + */ + public function removeProduct(int $id): JsonResponse + { + $this->preventUnauthorizedAccess($this->leadRepository->findOrFail($id)->user_id); + + try { + Event::dispatch('lead.product.delete.before', $id); + + $this->productRepository->deleteWhere([ + 'lead_id' => $id, + 'product_id' => request()->input('product_id'), + ]); + + Event::dispatch('lead.product.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.leads.destroy-success'), + ]); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.leads.destroy-failed'), + ]); + } + } + + /** + * Kanban lookup. + */ + public function kanbanLookup() + { + $params = $this->validate(request(), [ + 'column' => ['required'], + 'search' => ['required', 'min:2'], + ]); + + /** + * Finding the first column from the collection. + */ + $column = collect($this->getKanbanColumns())->where('index', $params['column'])->firstOrFail(); + + /** + * Fetching on the basis of column options. + */ + return app($column['filterable_options']['repository']) + ->select([$column['filterable_options']['column']['label'].' as label', $column['filterable_options']['column']['value'].' as value']) + ->where($column['filterable_options']['column']['label'], 'LIKE', '%'.$params['search'].'%') + ->get() + ->map + ->only('label', 'value'); + } + + /** + * Get columns for the kanban view. + */ + private function getKanbanColumns(): array + { + return [ + [ + 'index' => 'id', + 'label' => trans('admin::app.leads.index.kanban.columns.id'), + 'type' => 'integer', + 'searchable' => false, + 'search_field' => 'in', + 'filterable' => true, + 'filterable_type' => null, + 'filterable_options' => [], + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + ], + [ + 'index' => 'lead_value', + 'label' => trans('admin::app.leads.index.kanban.columns.lead-value'), + 'type' => 'string', + 'searchable' => false, + 'search_field' => 'in', + 'filterable' => true, + 'filterable_type' => null, + 'filterable_options' => [], + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + ], + [ + 'index' => 'user_id', + 'label' => trans('admin::app.leads.index.kanban.columns.sales-person'), + 'type' => 'string', + 'searchable' => false, + 'search_field' => 'in', + 'filterable' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => UserRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'id', + ], + ], + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + ], + [ + 'index' => 'person.id', + 'label' => trans('admin::app.leads.index.kanban.columns.contact-person'), + 'type' => 'string', + 'searchable' => false, + 'search_field' => 'in', + 'filterable' => true, + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => PersonRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'id', + ], + ], + ], + [ + 'index' => 'lead_type_id', + 'label' => trans('admin::app.leads.index.kanban.columns.lead-type'), + 'type' => 'string', + 'searchable' => false, + 'search_field' => 'in', + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => $this->typeRepository->all(['name as label', 'id as value'])->toArray(), + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + ], + [ + 'index' => 'lead_source_id', + 'label' => trans('admin::app.leads.index.kanban.columns.source'), + 'type' => 'string', + 'searchable' => false, + 'search_field' => 'in', + 'filterable' => true, + 'filterable_type' => 'dropdown', + 'filterable_options' => $this->sourceRepository->all(['name as label', 'id as value'])->toArray(), + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + ], + [ + 'index' => 'tags.name', + 'label' => trans('admin::app.leads.index.kanban.columns.tags'), + 'type' => 'string', + 'searchable' => false, + 'search_field' => 'in', + 'filterable' => true, + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + 'filterable_type' => 'searchable_dropdown', + 'filterable_options' => [ + 'repository' => TagRepository::class, + 'column' => [ + 'label' => 'name', + 'value' => 'name', + ], + ], + ], + [ + 'index' => 'expected_close_date', + 'label' => trans('admin::app.leads.index.kanban.columns.date-to'), + 'type' => 'date', + 'searchable' => false, + 'search_field' => 'between', + 'filterable' => true, + 'filterable_type' => 'date_range', + 'filterable_options' => DateRangeOptionEnum::options(), + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + ], + [ + 'index' => 'created_at', + 'label' => trans('admin::app.leads.index.kanban.columns.created-at'), + 'type' => 'date', + 'searchable' => false, + 'search_field' => 'between', + 'filterable' => true, + 'filterable_type' => 'date_range', + 'filterable_options' => DateRangeOptionEnum::options(), + 'allow_multiple_values' => true, + 'sortable' => true, + 'visibility' => true, + ], + ]; + } + + /** + * Apply the kanban date range filters to the given lead query. These filters are sent as a + * dedicated parameter rather than through the search string, so they are applied here. + * + * The datagrid's date column type is reused to resolve the requested value, which keeps the + * quick filter options, the partial ranges and the day boundaries consistent between the + * kanban and the lead listing. + * + * @param mixed $query + */ + private function applyDateRangeFilters($query): void + { + foreach ($this->getKanbanDateColumns() as $index => $columnName) { + $requestedDates = request($index); + + if (empty($requestedDates)) { + continue; + } + + $column = new DateColumn([ + 'index' => $index, + 'label' => $index, + 'type' => 'date', + 'filterable' => true, + 'filterable_type' => 'date_range', + ]); + + $column->setColumnName($columnName); + + $column->processFilter($query, $requestedDates); + } + } + + /** + * Returns the kanban date columns, mapped to their qualified table column name. + */ + private function getKanbanDateColumns(): array + { + return [ + 'expected_close_date' => 'leads.expected_close_date', + 'created_at' => 'leads.created_at', + ]; + } + + /** + * Create lead with specified AI. + */ + public function createByAI() + { + $leadData = []; + + $errorMessages = []; + + foreach (request()->file('files') as $file) { + $lead = $this->processFile($file); + + if ( + isset($lead['status']) + && $lead['status'] === 'error' + ) { + $errorMessages[] = $lead['message']; + } else { + $leadData[] = $lead; + } + } + + if (isset($errorMessages[0]['code'])) { + return response()->json(MagicAI::errorHandler($errorMessages[0]['message'])); + } + + if ( + empty($leadData) + && ! empty($errorMessages) + ) { + return response()->json(MagicAI::errorHandler(implode(', ', $errorMessages)), 400); + } + + if (empty($leadData)) { + return response()->json(MagicAI::errorHandler(trans('admin::app.leads.no-valid-files')), 400); + } + + return response()->json([ + 'message' => trans('admin::app.leads.create-success'), + 'leads' => $this->createLeads($leadData), + ]); + } + + /** + * Process file. + * + * @param mixed $file + */ + private function processFile($file) + { + $validator = Validator::make( + ['file' => $file], + ['file' => 'required|extensions:'.str_replace(' ', '', self::SUPPORTED_TYPES)] + ); + + if ($validator->fails()) { + return MagicAI::errorHandler($validator->errors()->first()); + } + + $base64Pdf = base64_encode(file_get_contents($file->getRealPath())); + + $extractedData = MagicAIService::extractDataFromFile($base64Pdf); + + $lead = MagicAI::mapAIDataToLead($extractedData); + + return $lead; + } + + /** + * Create multiple leads. + */ + private function createLeads($rawLeads): array + { + $leads = []; + + foreach ($rawLeads as $rawLead) { + Event::dispatch('lead.create.before'); + + foreach ($rawLead['person']['emails'] as $email) { + $person = $this->personRepository + ->whereJsonContains('emails', [['value' => $email['value']]]) + ->first(); + + if ($person) { + $rawLead['person']['id'] = $person->id; + + break; + } + } + + $pipeline = $this->pipelineRepository->getDefaultPipeline(); + + $stage = $pipeline->stages()->first(); + + $lead = $this->leadRepository->create(array_merge($rawLead, [ + 'lead_pipeline_id' => $pipeline->id, + 'lead_pipeline_stage_id' => $stage->id, + ])); + + Event::dispatch('lead.create.after', $lead); + + $leads[] = $lead; + } + + return $leads; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Lead/QuoteController.php b/packages/Webkul/Admin/src/Http/Controllers/Lead/QuoteController.php new file mode 100644 index 0000000..b6e7947 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Lead/QuoteController.php @@ -0,0 +1,186 @@ +leadRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($lead->user_id); + + Event::dispatch('leads.quote.create.before'); + + if (! $lead->quotes->contains(request('id'))) { + $lead->quotes()->attach(request('id')); + } + + Event::dispatch('leads.quote.create.after', $lead); + + return response()->json([ + 'message' => trans('admin::app.leads.quote-create-success'), + ], 200); + } + + /** + * Remove the specified resource from storage. + * + * @param int $leadId + * @param int $tagId + * @return Response + */ + public function delete($leadId) + { + $lead = $this->leadRepository->findOrFail($leadId); + + $this->preventUnauthorizedAccess($lead->user_id); + + Event::dispatch('leads.quote.delete.before', $leadId); + + $lead->quotes()->detach(request('quote_id')); + + Event::dispatch('leads.quote.delete.after', $lead); + + return response()->json([ + 'message' => trans('admin::app.leads.view.quotes.destroy-success'), + ], 200); + } + + /** + * Send quote PDF to lead person email. + * + * @param int $leadId + * @param int $quoteId + * @return Response + */ + public function mail($quoteId) + { + $quote = $this->quoteRepository->findOrFail($quoteId); + + $this->preventUnauthorizedAccess($quote->user_id); + + $lead = $quote->leads->first(); + + if (! $quote) { + return response()->json([ + 'message' => trans('admin::app.leads.view.quotes.quote-not-found'), + ], 404); + } + + $to = []; + + if ($lead) { + $to[] = data_get($lead->person?->emails, '0.value'); + } + + if ($quote->person) { + $to[] = data_get($quote->person?->emails, '0.value'); + } + + if (empty($to)) { + return response()->json([ + 'message' => trans('admin::app.leads.view.quotes.person-email-unavailable'), + ], 422); + } + + try { + $pdfContent = $this->renderQuotePdfContent($quote); + + Mail::send(new Common([ + 'to' => $to, + 'subject' => trans('admin::app.leads.view.quotes.mail-subject', ['subject' => $quote->subject]), + 'body' => trans('admin::app.leads.view.quotes.mail-body'), + 'attachments' => [[ + 'content' => $pdfContent, + 'name' => 'Quote_'.$quote->id.'.pdf', + 'mime' => 'application/pdf', + ]], + ])); + + return response()->json([ + 'message' => trans('admin::app.mail.create-success'), + ], 200); + } catch (\Throwable $exception) { + return response()->json([ + 'message' => trans('admin::app.leads.view.quotes.mail-send-failed'), + ], 500); + } + } + + /** + * Render quote PDF and return raw content. + */ + private function renderQuotePdfContent($quote): string + { + $html = view('admin::quotes.pdf', compact('quote'))->render(); + + $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); + + if (in_array($direction = app()->getLocale(), ['ar', 'he'])) { + $mPDF = new Mpdf([ + 'margin_left' => 0, + 'margin_right' => 0, + 'margin_top' => 0, + 'margin_bottom' => 0, + ]); + + $mPDF->SetDirectionality($direction); + + $mPDF->SetDisplayMode('fullpage'); + + $mPDF->WriteHTML($this->adjustArabicAndPersianContent($html)); + + return $mPDF->Output('', 'S'); + } + + return Pdf::loadHTML($this->adjustArabicAndPersianContent($html)) + ->setPaper('A4', 'portrait') + ->set_option('defaultFont', 'Courier') + ->output(); + } + + /** + * Adjust arabic and persian content. + */ + private function adjustArabicAndPersianContent(string $html): string + { + $arabic = new Arabic; + + $parts = $arabic->arIdentify($html); + + for ($i = count($parts) - 1; $i >= 0; $i -= 2) { + $utf8ar = $arabic->utf8Glyphs(substr($html, $parts[$i - 1], $parts[$i] - $parts[$i - 1])); + $html = substr_replace($html, $utf8ar, $parts[$i - 1], $parts[$i] - $parts[$i - 1]); + } + + return $html; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Lead/TagController.php b/packages/Webkul/Admin/src/Http/Controllers/Lead/TagController.php new file mode 100644 index 0000000..fdb9f2d --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Lead/TagController.php @@ -0,0 +1,62 @@ +leadRepository->find($id); + + if (! $lead->tags->contains(request()->input('tag_id'))) { + $lead->tags()->attach(request()->input('tag_id')); + } + + Event::dispatch('leads.tag.create.after', $lead); + + return response()->json([ + 'message' => trans('admin::app.leads.view.tags.create-success'), + ]); + } + + /** + * Remove the specified resource from storage. + * + * @param int $leadId + * @return Response + */ + public function detach($leadId) + { + Event::dispatch('leads.tag.delete.before', $leadId); + + $lead = $this->leadRepository->find($leadId); + + $lead->tags()->detach(request()->input('tag_id')); + + Event::dispatch('leads.tag.delete.after', $lead); + + return response()->json([ + 'message' => trans('admin::app.leads.view.tags.destroy-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Mail/EmailController.php b/packages/Webkul/Admin/src/Http/Controllers/Mail/EmailController.php new file mode 100644 index 0000000..d0184df --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Mail/EmailController.php @@ -0,0 +1,378 @@ +route('admin.mail.index', ['route' => SupportedFolderEnum::INBOX->value]); + } + + if (! bouncer()->hasPermission('mail.'.$route)) { + abort(401, trans('admin::app.mail.unauthorized')); + } + + if (request()->ajax()) { + return datagrid(EmailDataGrid::class)->process(); + } + + return view('admin::mail.index', compact('route')); + } + + /** + * Display a resource. + * + * @return View + */ + public function view() + { + $route = request('route'); + + $email = $this->emailRepository + ->with([ + 'emails', + 'attachments', + 'emails.attachments', + 'lead', + 'lead.person', + 'lead.tags', + 'lead.source', + 'lead.type', + 'person', + ]) + ->findOrFail(request('id')); + + /** + * A mail linked to a lead or person is scoped to whoever owns that record. The mailbox + * itself is shared, so unlinked mail stays visible to everyone with mail access; but a + * linked mail belonging to a lead/person outside the acting user's data scope is denied + * outright, rather than merely hiding its `lead_id` while still returning the subject, + * body, thread and attachments. + */ + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $ownerIds = array_filter([ + $email->lead?->user_id, + $email->person?->user_id, + ]); + + if ($ownerIds && empty(array_intersect($ownerIds, $userIds))) { + abort(401, trans('admin::app.errors.unauthorized')); + } + } + + if ($route == SupportedFolderEnum::DRAFT->value) { + return response()->json([ + 'data' => new EmailResource($email), + ]); + } + + return view('admin::mail.view', compact('email', 'route')); + } + + /** + * Store a newly created resource in storage. + * + * @return Response + */ + public function store() + { + $this->validate(request(), [ + 'reply_to' => 'required|array|min:1', + 'reply_to.*' => 'email', + 'reply' => 'required', + 'attachments' => 'sometimes|array', + 'attachments.*' => 'file|max:20480', + ]); + + Event::dispatch('email.create.before'); + + $email = $this->emailRepository->create(request()->all()); + + if (! request('is_draft')) { + try { + Mail::send(new Email($email)); + + $this->emailRepository->update([ + 'folders' => [SupportedFolderEnum::SENT->value], + ], $email->id); + } catch (Exception $e) { + } + } + + Event::dispatch('email.create.after', $email); + + if (request()->ajax()) { + return response()->json([ + 'data' => new EmailResource($email), + 'message' => trans('admin::app.mail.create-success'), + ]); + } + + if (request('is_draft')) { + session()->flash('success', trans('admin::app.mail.saved-to-draft')); + + return redirect()->route('admin.mail.index', ['route' => SupportedFolderEnum::DRAFT->value]); + } + + session()->flash('success', trans('admin::app.mail.create-success')); + + return redirect()->route('admin.mail.index', ['route' => SupportedFolderEnum::SENT->value]); + } + + /** + * Update the specified resource in storage. + * + * @param int $id + * @return Response + */ + public function update($id) + { + Event::dispatch('email.update.before', $id); + + $data = request()->all(); + + if (! is_null(request('is_draft'))) { + $data['folders'] = request('is_draft') ? [SupportedFolderEnum::DRAFT->value] : [SupportedFolderEnum::OUTBOX->value]; + } + + $email = $this->emailRepository->update($data, request('id') ?? $id); + + Event::dispatch('email.update.after', $email); + + if (! is_null(request('is_draft')) && ! request('is_draft')) { + try { + Mail::send(new Email($email)); + + $this->emailRepository->update([ + 'folders' => [SupportedFolderEnum::INBOX->value, SupportedFolderEnum::SENT->value], + ], $email->id); + } catch (Exception $e) { + } + } + + if (! is_null(request('is_draft'))) { + if (request('is_draft')) { + session()->flash('success', trans('admin::app.mail.saved-to-draft')); + + return redirect()->route('admin.mail.index', ['route' => SupportedFolderEnum::DRAFT->value]); + } else { + session()->flash('success', trans('admin::app.mail.create-success')); + + return redirect()->route('admin.mail.index', ['route' => SupportedFolderEnum::INBOX->value]); + } + } + + if (request()->ajax()) { + return response()->json([ + 'data' => new EmailResource($email->refresh()), + 'message' => trans('admin::app.mail.update-success'), + ]); + } + + session()->flash('success', trans('admin::app.mail.update-success')); + + return redirect()->back(); + } + + /** + * Run process inbound parse email. + * + * @return Response + */ + public function inboundParse(InboundEmailProcessor $inboundEmailProcessor) + { + $inboundEmailProcessor->processMessage(request('email')); + + return response()->json([], 200); + } + + /** + * Download file from storage + * + * @param int $id + * @return View + */ + public function download($id) + { + /** + * Downloading an attachment requires the mail view permission — the same permission that + * gates the mailbox it belongs to. This closes the missing function-level authorization that + * let any authenticated user retrieve an attachment by its id, independent of the ACL route + * map (defense in depth). + */ + abort_unless(bouncer()->hasPermission('mail.view'), 401, trans('admin::app.errors.unauthorized')); + + $attachment = $this->attachmentRepository->findOrFail($id); + + /** + * Bind the download to the acting user's data scope through the attachment's parent mail: + * an attachment on a lead/person-linked mail outside that scope is refused, so an + * unauthorized mail id can no longer be turned into an unauthorized file download. + */ + if ($userIds = bouncer()->getAuthorizedUserIds()) { + $email = $attachment->email; + + $ownerIds = array_filter([ + $email?->lead?->user_id, + $email?->person?->user_id, + ]); + + if ($ownerIds && empty(array_intersect($ownerIds, $userIds))) { + abort(401, trans('admin::app.errors.unauthorized')); + } + } + + try { + return Storage::disk(AttachmentRepository::resolveDisk($attachment->path)) + ->download($attachment->path, $attachment->name); + } catch (Exception $e) { + session()->flash('error', $e->getMessage()); + + return redirect()->back(); + } + } + + /** + * Mass Update the specified resources. + */ + public function massUpdate(MassUpdateRequest $massUpdateRequest): JsonResponse + { + $emails = $this->emailRepository->findWhereIn('id', $massUpdateRequest->input('indices')); + + try { + foreach ($emails as $email) { + Event::dispatch('email.update.before', $email->id); + + $this->emailRepository->update([ + 'folders' => request('folders'), + ], $email->id); + + Event::dispatch('email.update.after', $email->id); + } + + return response()->json([ + 'message' => trans('admin::app.mail.mass-update-success'), + ]); + } catch (Exception) { + return response()->json([ + 'message' => trans('admin::app.mail.mass-update-success'), + ], 400); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse|RedirectResponse + { + $email = $this->emailRepository->findOrFail($id); + + try { + Event::dispatch('email.'.request('type').'.before', $id); + + $parentId = $email->parent_id; + + if (request('type') == SupportedFolderEnum::TRASH->value) { + $this->emailRepository->update([ + 'folders' => [SupportedFolderEnum::TRASH->value], + ], $id); + } else { + $this->emailRepository->delete($id); + } + + Event::dispatch('email.'.request('type').'.after', $id); + + if (request()->ajax()) { + return response()->json([ + 'message' => trans('admin::app.mail.delete-success'), + ], 200); + } + + session()->flash('success', trans('admin::app.mail.delete-success')); + + if ($parentId) { + return redirect()->back(); + } + + return redirect()->route('admin.mail.index', ['route' => SupportedFolderEnum::INBOX->value]); + } catch (Exception $exception) { + if (request()->ajax()) { + return response()->json([ + 'message' => trans('admin::app.mail.delete-failed'), + ], 400); + } + + session()->flash('error', trans('admin::app.mail.delete-failed')); + + return redirect()->back(); + } + } + + /** + * Mass Delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $mails = $this->emailRepository->findWhereIn('id', $massDestroyRequest->input('indices')); + + try { + foreach ($mails as $email) { + Event::dispatch('email.'.$massDestroyRequest->input('type').'.before', $email->id); + + if ($massDestroyRequest->input('type') == SupportedFolderEnum::TRASH->value) { + $this->emailRepository->update(['folders' => [SupportedFolderEnum::TRASH->value]], $email->id); + } else { + $this->emailRepository->delete($email->id); + } + + Event::dispatch('email.'.$massDestroyRequest->input('type').'.after', $email->id); + } + + return response()->json([ + 'message' => trans('admin::app.mail.delete-success'), + ]); + } catch (Exception $e) { + return response()->json([ + 'message' => trans('admin::app.mail.delete-success'), + ]); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Mail/TagController.php b/packages/Webkul/Admin/src/Http/Controllers/Mail/TagController.php new file mode 100644 index 0000000..29877e4 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Mail/TagController.php @@ -0,0 +1,56 @@ +emailRepository->find($id); + + if (! $mail->tags->contains(request()->input('tag_id'))) { + $mail->tags()->attach(request()->input('tag_id')); + } + + Event::dispatch('mails.tag.create.after', $mail); + + return response()->json([ + 'message' => trans('admin::app.mail.view.tags.create-success'), + ]); + } + + /** + * Remove the specified resource from storage. + */ + public function detach(int $mailId): JsonResponse + { + Event::dispatch('mails.tag.delete.before', $mailId); + + $mail = $this->emailRepository->find($mailId); + + $mail->tags()->detach(request()->input('tag_id')); + + Event::dispatch('mails.tag.delete.after', $mail); + + return response()->json([ + 'message' => trans('admin::app.mail.view.tags.destroy-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Products/ActivityController.php b/packages/Webkul/Admin/src/Http/Controllers/Products/ActivityController.php new file mode 100644 index 0000000..012965b --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Products/ActivityController.php @@ -0,0 +1,29 @@ + 'product_activities', 'foreign' => 'product_id']; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Products/ProductController.php b/packages/Webkul/Admin/src/Http/Controllers/Products/ProductController.php new file mode 100644 index 0000000..97288f5 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Products/ProductController.php @@ -0,0 +1,231 @@ +request->add(['entity_type' => 'products']); + } + + /** + * Display a listing of the resource. + */ + public function index(): View|JsonResponse + { + if (request()->ajax()) { + return datagrid(ProductDataGrid::class)->process(); + } + + return view('admin::products.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::products.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(AttributeForm $request) + { + Event::dispatch('product.create.before'); + + $product = $this->productRepository->create($request->all()); + + Event::dispatch('product.create.after', $product); + + if (request()->ajax()) { + return response()->json([ + 'data' => $product, + 'message' => trans('admin::app.products.index.create-success'), + ]); + } + + session()->flash('success', trans('admin::app.products.index.create-success')); + + return redirect()->route('admin.products.index'); + } + + /** + * Show the form for viewing the specified resource. + */ + public function view(int $id): View + { + $product = $this->productRepository->findOrFail($id); + + return view('admin::products.view', compact('product')); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View|JsonResponse + { + $product = $this->productRepository->findOrFail($id); + + $inventories = $product->inventories() + ->with('location') + ->get() + ->map(function ($inventory) { + return [ + 'id' => $inventory->id, + 'name' => $inventory->location->name, + 'warehouse_id' => $inventory->warehouse_id, + 'warehouse_location_id' => $inventory->warehouse_location_id, + 'in_stock' => $inventory->in_stock, + 'allocated' => $inventory->allocated, + ]; + }); + + return view('admin::products.edit', compact('product', 'inventories')); + } + + /** + * Update the specified resource in storage. + */ + public function update(AttributeForm $request, int $id) + { + Event::dispatch('product.update.before', $id); + + $product = $this->productRepository->update($request->all(), $id); + + Event::dispatch('product.update.after', $product); + + if (request()->ajax()) { + return response()->json([ + 'message' => trans('admin::app.products.index.update-success'), + ]); + } + + session()->flash('success', trans('admin::app.products.index.update-success')); + + return redirect()->route('admin.products.index'); + } + + /** + * Store a newly created resource in storage. + */ + public function storeInventories(int $id, ?int $warehouseId = null): JsonResponse + { + $this->validate(request(), [ + 'inventories' => 'array', + 'inventories.*.warehouse_location_id' => 'required', + 'inventories.*.warehouse_id' => 'required', + 'inventories.*.in_stock' => 'required|integer|min:0', + 'inventories.*.allocated' => 'required|integer|min:0', + ]); + + $product = $this->productRepository->findOrFail($id); + + Event::dispatch('product.update.before', $id); + + $this->productRepository->saveInventories(request()->all(), $id, $warehouseId); + + Event::dispatch('product.update.after', $product); + + return new JsonResponse([ + 'message' => trans('admin::app.products.index.update-success'), + ], 200); + } + + /** + * Search product results + */ + public function search(): JsonResource + { + $query = $this->productRepository + ->pushCriteria(app(RequestCriteria::class)) + ->orderBy('created_at', 'desc'); + + $excludedIds = request()->input('exclude_ids', []); + + if (is_string($excludedIds)) { + $excludedIds = array_filter(array_map('trim', explode(',', $excludedIds))); + } + + if (! empty($excludedIds)) { + $query->whereNotIn('products.id', $excludedIds); + } + + $products = $query->get(); + + return ProductResource::collection($products); + } + + /** + * Returns product inventories grouped by warehouse. + */ + public function warehouses(int $id): JsonResponse + { + $warehouses = $this->productRepository->getInventoriesGroupedByWarehouse($id); + + return response()->json(array_values($warehouses)); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $product = $this->productRepository->findOrFail($id); + + try { + Event::dispatch('settings.products.delete.before', $id); + + $product->delete($id); + + Event::dispatch('settings.products.delete.after', $id); + + return new JsonResponse([ + 'message' => trans('admin::app.products.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return new JsonResponse([ + 'message' => trans('admin::app.products.index.delete-failed'), + ], 400); + } + } + + /** + * Mass Delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $indices = $massDestroyRequest->input('indices'); + + foreach ($indices as $index) { + Event::dispatch('product.delete.before', $index); + + $this->productRepository->delete($index); + + Event::dispatch('product.delete.after', $index); + } + + return new JsonResponse([ + 'message' => trans('admin::app.products.index.delete-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Products/TagController.php b/packages/Webkul/Admin/src/Http/Controllers/Products/TagController.php new file mode 100644 index 0000000..52f0493 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Products/TagController.php @@ -0,0 +1,62 @@ +productRepository->findOrFail($id); + + if (! $product->tags->contains(request()->input('tag_id'))) { + $product->tags()->attach(request()->input('tag_id')); + } + + Event::dispatch('products.tag.create.after', $product); + + return response()->json([ + 'message' => trans('admin::app.leads.view.tags.create-success'), + ]); + } + + /** + * Remove the specified resource from storage. + * + * @param int $productId + * @return Response + */ + public function detach($productId) + { + Event::dispatch('products.tag.delete.before', $productId); + + $product = $this->productRepository->find($productId); + + $product->tags()->detach(request()->input('tag_id')); + + Event::dispatch('products.tag.delete.after', $product); + + return response()->json([ + 'message' => trans('admin::app.leads.view.tags.destroy-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Quote/QuoteController.php b/packages/Webkul/Admin/src/Http/Controllers/Quote/QuoteController.php new file mode 100644 index 0000000..42d38f4 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Quote/QuoteController.php @@ -0,0 +1,324 @@ +request->add(['entity_type' => 'quotes']); + } + + /** + * Display a listing of the resource. + */ + public function index(): View|JsonResponse + { + if (request()->ajax()) { + return datagrid(QuoteDataGrid::class)->process(); + } + + return view('admin::quotes.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + $leadId = request('lead_id'); + + $lead = $leadId ? $this->leadRepository->find($leadId) : null; + + $quote = $this->quoteRepository->getModel(); + + if ($lead) { + $quote->fill([ + 'person_id' => $lead->person_id, + 'user_id' => $lead->user_id, + 'billing_address' => $lead->person->organization?->address, + 'expired_at' => $lead->expected_close_date ?? now()->toDateString(), + ]); + } + + $leadProducts = $this->getLeadProductsForQuote($lead); + + $lookUpEntityData = $this->attributeRepository->getLookUpEntity('leads', $leadId); + + return view('admin::quotes.create', compact('lead', 'quote', 'leadProducts', 'lookUpEntityData')); + } + + /** + * Store a newly created resource in storage. + */ + public function store(AttributeForm $request): RedirectResponse|JsonResponse + { + if (! request()->has('quick_add')) { + $this->additionalValidation(); + } + + $this->syncShippingAddressWithBilling($request); + + Event::dispatch('quote.create.before'); + + $quote = $this->quoteRepository->create($request->all()); + + $leadId = request('lead_id'); + + if ($leadId) { + $lead = $this->leadRepository->find($leadId); + + $lead->quotes()->attach($quote->id); + } + + Event::dispatch('quote.create.after', $quote); + + if (request()->ajax()) { + return response()->json([ + 'data' => $quote, + 'message' => trans('admin::app.quotes.index.create-success'), + ]); + } + + session()->flash('success', trans('admin::app.quotes.index.create-success')); + + return request()->query('from') === 'lead' && $leadId + ? redirect()->route('admin.leads.view', ['id' => $leadId, 'from' => 'quotes']) + : redirect()->route('admin.quotes.index'); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View + { + $quote = $this->quoteRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($quote->user_id); + + $leadId = old('lead_id') ?? optional($quote->leads->first())->id; + + $linkedLead = $leadId ? $this->leadRepository->find($leadId) : null; + + $initialQuoteItems = $quote->items; + + if ($initialQuoteItems->isEmpty() && $linkedLead?->products?->isNotEmpty()) { + $initialQuoteItems = collect($this->getLeadProductsForQuote($linkedLead)); + } + + $lookUpEntityData = $this->attributeRepository->getLookUpEntity('leads', $leadId); + + return view('admin::quotes.edit', compact('quote', 'linkedLead', 'initialQuoteItems', 'lookUpEntityData')); + } + + /** + * Update the specified resource in storage. + */ + public function update(AttributeForm $request, int $id): RedirectResponse + { + $this->preventUnauthorizedAccess($this->quoteRepository->findOrFail($id)->user_id); + + $this->additionalValidation(); + + $this->syncShippingAddressWithBilling($request); + + Event::dispatch('quote.update.before', $id); + + $quote = $this->quoteRepository->update($request->all(), $id); + + $quote->leads()->detach(); + + $leadId = request('lead_id'); + + if ($leadId) { + $lead = $this->leadRepository->find($leadId); + + $lead->quotes()->attach($quote->id); + } + + Event::dispatch('quote.update.after', $quote); + + session()->flash('success', trans('admin::app.quotes.index.update-success')); + + return request()->query('from') === 'lead' && $leadId + ? redirect()->route('admin.leads.view', ['id' => $leadId, 'from' => 'quotes']) + : redirect()->route('admin.quotes.index'); + } + + /** + * Search the quotes. + */ + public function search(): AnonymousResourceCollection + { + $quotes = $this->quoteRepository + ->pushCriteria(app(RequestCriteria::class)) + ->all(); + + return QuoteResource::collection($quotes); + } + + /** + * Return products for the selected lead in quote payload format. + */ + public function leadProducts(int $leadId): JsonResponse + { + $lead = $this->leadRepository->findOrFail($leadId); + + return response()->json([ + 'data' => $this->getLeadProductsForQuote($lead), + ]); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $this->preventUnauthorizedAccess($this->quoteRepository->findOrFail($id)->user_id); + + try { + Event::dispatch('quote.delete.before', $id); + + $this->quoteRepository->delete($id); + + Event::dispatch('quote.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.quotes.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.quotes.index.delete-failed'), + ], 400); + } + } + + /** + * Mass Delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $quotes = $this->filterAuthorizedRecords( + $this->quoteRepository->findWhereIn('id', $massDestroyRequest->input('indices')) + ); + + try { + foreach ($quotes as $quotes) { + Event::dispatch('quote.delete.before', $quotes->id); + + $this->quoteRepository->delete($quotes->id); + + Event::dispatch('quote.delete.after', $quotes->id); + } + + return response()->json([ + 'message' => trans('admin::app.quotes.index.delete-success'), + ]); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.quotes.index.delete-failed'), + ], 400); + } + } + + /** + * Print and download the for the specified resource. + */ + public function print($id): Response|StreamedResponse + { + $quote = $this->quoteRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($quote->user_id); + + return $this->downloadPDF( + view('admin::quotes.pdf', compact('quote'))->render(), + 'Quote_'.$quote->subject.'_'.$quote->created_at->format('d-m-Y') + ); + } + + /** + * Mirror the billing address into the shipping address when "same as billing" is enabled. + */ + private function syncShippingAddressWithBilling(AttributeForm $request): void + { + if ($request->boolean('shipping_address_same_as_billing')) { + $request->merge([ + 'shipping_address' => $request->input('billing_address'), + ]); + } + } + + /** + * Additional validation for quote product items. + */ + private function additionalValidation(): void + { + $this->validate(request(), [ + 'items' => 'required|array', + 'items.*.product_id' => 'required|exists:products,id', + 'items.*.quantity' => 'required|numeric|min:0', + 'items.*.price' => 'required|numeric|min:0', + 'items.*.total' => 'required|numeric|min:0', + 'items.*.discount_amount' => 'required|numeric|min:0', + 'items.*.tax_amount' => 'required|numeric|min:0', + 'items.*.final_total' => 'required|numeric|min:0', + ]); + } + + /** + * Map linked lead products to quote item payload format. + */ + private function getLeadProductsForQuote($lead): array + { + if (! $lead?->products?->isNotEmpty()) { + return []; + } + + return $lead->products + ->map(function ($product) { + $quantity = (float) ($product->quantity ?: 1); + $price = (float) ($product->price ?: 0); + + return [ + 'id' => null, + 'product_id' => $product->product_id, + 'name' => $product->name, + 'quantity' => $quantity, + 'total' => $price * $quantity, + 'price' => $price, + 'discount_amount' => 0, + 'tax_amount' => 0, + ]; + }) + ->values() + ->toArray(); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/AttributeController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/AttributeController.php new file mode 100644 index 0000000..2bc1cf5 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/AttributeController.php @@ -0,0 +1,265 @@ +ajax()) { + return datagrid(AttributeDataGrid::class)->process(); + } + + return view('admin::settings.attributes.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::settings.attributes.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): RedirectResponse|JsonResponse + { + $this->validate(request(), [ + 'code' => ['required', 'unique:attributes,code,NULL,NULL,entity_type,'.request('entity_type'), new Code], + 'name' => 'required', + 'type' => 'required', + 'validation' => 'nullable|in:numeric,email,decimal,url', + ]); + + Event::dispatch('settings.attribute.create.before'); + + $attribute = $this->attributeRepository->create(request()->all()); + + Event::dispatch('settings.attribute.create.after', $attribute); + + if (request()->ajax()) { + return response()->json([ + 'data' => $attribute, + 'message' => trans('admin::app.settings.attributes.index.create-success'), + ]); + } + + session()->flash('success', trans('admin::app.settings.attributes.index.create-success')); + + return redirect()->route('admin.settings.attributes.index'); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View + { + $attribute = $this->attributeRepository->findOrFail($id); + + return view('admin::settings.attributes.edit', compact('attribute')); + } + + /** + * Update the specified resource in storage. + */ + public function update($id): RedirectResponse + { + $this->validate(request(), [ + 'code' => ['required', 'unique:attributes,code,NULL,NULL,entity_type,'.$id, new Code], + 'name' => 'required', + 'type' => 'required', + 'validation' => 'nullable|in:numeric,email,decimal,url', + ]); + + Event::dispatch('settings.attribute.update.before', $id); + + $quickAdd = request()->has('quick_add') ? 1 : 0; + + request()->merge(['quick_add' => $quickAdd]); + + $attribute = $this->attributeRepository->update(request()->all(), $id); + + Event::dispatch('settings.attribute.update.after', $attribute); + + session()->flash('success', trans('admin::app.settings.attributes.index.update-success')); + + return redirect()->route('admin.settings.attributes.index'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $attribute = $this->attributeRepository->findOrFail($id); + + if (! $attribute->is_user_defined) { + return response()->json([ + 'message' => trans('admin::app.settings.attributes.index.user-define-error'), + ], 400); + } + + try { + Event::dispatch('settings.attribute.delete.before', $id); + + $this->attributeRepository->delete($id); + + Event::dispatch('settings.attribute.delete.after', $id); + + return response()->json([ + 'status' => true, + 'message' => trans('admin::app.settings.attributes.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.settings.attributes.index.delete-failed'), + ], 400); + } + } + + /** + * Check unique validation. + * + * @return void + */ + public function checkUniqueValidation() + { + $attribute = $this->attributeRepository->findOneWhere([ + 'code' => request('attribute_code'), + ]); + + return response()->json([ + 'validated' => $this->attributeValueRepository->isValueUnique( + request('entity_id'), + request('entity_type'), + $attribute, + request('attribute_value'), + ), + ]); + } + + /** + * Search attribute lookup results + */ + public function lookup($lookup): JsonResponse + { + $results = $this->attributeRepository->getLookUpOptions($lookup, request()->input('query')); + + return response()->json($results); + } + + /** + * Search attribute lookup results + */ + public function lookupEntity(string $lookup): JsonResponse + { + $result = $this->attributeRepository->getLookUpEntity($lookup, request()->input('query')); + + return response()->json($result); + } + + /** + * Mass Delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $count = 0; + + $attributes = $this->attributeRepository->findWhereIn('id', $massDestroyRequest->input('indices')); + + foreach ($attributes as $attribute) { + $attribute = $this->attributeRepository->find($attribute->id); + + if (! $attribute->is_user_defined) { + continue; + } + + Event::dispatch('settings.attribute.delete.before', $attribute->id); + + $this->attributeRepository->delete($attribute->id); + + Event::dispatch('settings.attribute.delete.after', $attribute->id); + + $count++; + } + + if (! $count) { + return response()->json([ + 'message' => trans('admin::app.settings.attributes.index.mass-delete-failed'), + ], 400); + } + + return response()->json([ + 'message' => trans('admin::app.settings.attributes.index.delete-success'), + ]); + } + + /** + * Get attribute options associated with attribute. + * + * @return View + */ + public function getAttributeOptions(int $id) + { + $attribute = $this->attributeRepository->findOrFail($id); + + return $attribute->options()->orderBy('sort_order')->get(); + } + + /** + * Download image or file + */ + public function download() + { + $path = request('path'); + + /** + * The path arrives from the query string, so it is resolved against the attribute values + * that actually reference a stored file before anything is served. Handing it straight to + * the disk let any path on the shared storage disk be downloaded through this endpoint — + * import artefacts, activity attachments and configuration uploads all live alongside + * attribute files. The type check matters because `text_value` is shared with the text, + * textarea, multiselect and checkbox types, which hold no file reference. + */ + if (! is_string($path) || $path === '') { + abort(404); + } + + $attributeValue = $this->attributeValueRepository + ->findWhere(['text_value' => $path]) + ->first(fn ($value) => in_array($value->attribute?->type, ['file', 'image'], true)); + + if (! $attributeValue) { + abort(404); + } + + return Storage::download($attributeValue->text_value); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/DataTransfer/ImportController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/DataTransfer/ImportController.php new file mode 100755 index 0000000..7c23a9d --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/DataTransfer/ImportController.php @@ -0,0 +1,531 @@ +ajax()) { + return datagrid(ImportDataGrid::class)->process(); + } + + return view('admin::settings.data-transfer.imports.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::settings.data-transfer.imports.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): RedirectResponse + { + $importers = array_keys(config('importers')); + + $this->validate(request(), [ + 'type' => 'required|in:'.implode(',', $importers), + 'action' => 'required:in:append,delete', + 'validation_strategy' => 'required:in:stop-on-errors,skip-errors', + 'allowed_errors' => 'required|integer|min:0', + 'field_separator' => 'required', + 'file' => 'required|mimes:csv,xls,xlsx,txt', + ]); + + Event::dispatch('data_transfer.imports.create.before'); + + $data = request()->only([ + 'type', + 'action', + 'process_in_queue', + 'validation_strategy', + 'validation_strategy', + 'allowed_errors', + 'field_separator', + ]); + + /** + * Cast the toggle to a real boolean. The switch submits its value even when off (as `false`, + * `"0"` or `""`), so relying on the key's presence stored `process_in_queue = 1` for every + * import — which then fails to run on the default `sync` queue (issue #2629). + */ + $data['process_in_queue'] = request()->boolean('process_in_queue'); + + $import = $this->importRepository->create( + array_merge( + [ + 'file_path' => request()->file('file')->storeAs( + 'imports', + time().'-'.request()->file('file')->getClientOriginalName(), + 'public' + ), + ], + $data + ) + ); + + Event::dispatch('data_transfer.imports.create.after', $import); + + session()->flash('success', trans('admin::app.settings.data-transfer.imports.create-success')); + + return redirect()->route('admin.settings.data_transfer.imports.import', $import->id); + } + + /** + * Show the form for editing a new resource. + */ + public function edit(int $id): View + { + $import = $this->importRepository->findOrFail($id); + + return view('admin::settings.data-transfer.imports.edit', compact('import')); + } + + /** + * Update a resource in storage. + */ + public function update(int $id): RedirectResponse + { + $importers = array_keys(config('importers')); + + $import = $this->importRepository->findOrFail($id); + + $this->validate(request(), [ + 'type' => 'required|in:'.implode(',', $importers), + 'action' => 'required:in:append,delete', + 'validation_strategy' => 'required:in:stop-on-errors,skip-errors', + 'allowed_errors' => 'required|integer|min:0', + 'field_separator' => 'required', + 'file' => 'mimes:csv,xls,xlsx,txt', + ]); + + Event::dispatch('data_transfer.imports.update.before'); + + $data = array_merge( + request()->only([ + 'type', + 'action', + 'process_in_queue', + 'validation_strategy', + 'validation_strategy', + 'allowed_errors', + 'field_separator', + ]), + [ + 'state' => 'pending', + 'processed_rows_count' => 0, + 'invalid_rows_count' => 0, + 'errors_count' => 0, + 'errors' => null, + 'error_file_path' => null, + 'started_at' => null, + 'completed_at' => null, + 'summary' => null, + ] + ); + + Storage::disk('public')->delete($import->error_file_path ?? ''); + + if (request()->file('file') && request()->file('file')->isValid()) { + Storage::disk('public')->delete($import->file_path); + + $data['file_path'] = request()->file('file')->storeAs( + 'imports', + time().'-'.request()->file('file')->getClientOriginalName(), + 'public' + ); + } + + $data['process_in_queue'] = request()->boolean('process_in_queue'); + + $import = $this->importRepository->update($data, $import->id); + + Event::dispatch('data_transfer.imports.update.after', $import); + + session()->flash('success', trans('admin::app.settings.data-transfer.imports.update-success')); + + return redirect()->route('admin.settings.data_transfer.imports.import', $import->id); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $import = $this->importRepository->findOrFail($id); + + try { + Storage::disk('public')->delete($import->file_path); + + Storage::disk('public')->delete($import->error_file_path ?? ''); + + $this->importRepository->delete($id); + + return response()->json([ + 'message' => trans('admin::app.settings.data-transfer.imports.delete-success'), + ]); + } catch (\Exception $e) { + } + + return response()->json([ + 'message' => trans('admin::app.settings.data-transfer.imports.delete-failed'), + ], 500); + } + + /** + * Show the form for creating a new resource. + */ + public function import(int $id): View + { + $import = $this->importRepository->findOrFail($id); + + $isValid = $this->importHelper + ->setImport($import) + ->isValid(); + + if ($import->state == Import::STATE_LINKING) { + if ($this->importHelper->isIndexingRequired()) { + $state = Import::STATE_INDEXING; + } else { + $state = Import::STATE_COMPLETED; + } + } elseif ($import->state == Import::STATE_INDEXING) { + $state = Import::STATE_COMPLETED; + } else { + $state = Import::STATE_COMPLETED; + } + + $stats = $this->importHelper->stats($state); + + $import->unsetRelations(); + + return view('admin::settings.data-transfer.imports.import', compact('import', 'isValid', 'stats')); + } + + /** + * Store a newly created resource in storage. + */ + public function validateImport(int $id): JsonResponse + { + $import = $this->importRepository->findOrFail($id); + + $isValid = $this->importHelper + ->setImport($import) + ->validate(); + + return new JsonResponse([ + 'is_valid' => $isValid, + 'import' => $this->importHelper->getImport()->unsetRelations(), + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function start(int $id): JsonResponse + { + $import = $this->importRepository->findOrFail($id); + + if (! $import->processed_rows_count) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.data-transfer.imports.nothing-to-import'), + ], 400); + } + + $this->importHelper->setImport($import); + + if (! $this->importHelper->isValid()) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.data-transfer.imports.not-valid'), + ], 400); + } + + if ( + $import->process_in_queue + && config('queue.default') == 'sync' + ) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.data-transfer.imports.setup-queue-error'), + ], 400); + } + + /** + * Set the import state to processing + */ + if ($import->state == Import::STATE_VALIDATED) { + $this->importHelper->started(); + } + + /** + * Get the first pending batch to import + */ + $importBatch = $import->batches->where('state', Import::STATE_PENDING)->first(); + + if ($importBatch) { + /** + * Start the import process + */ + try { + if ($import->process_in_queue) { + $this->importHelper->start(); + } else { + $this->importHelper->start($importBatch); + } + } catch (\Exception $e) { + return new JsonResponse([ + 'message' => $e->getMessage(), + ], 400); + } + } else { + if ($this->importHelper->isLinkingRequired()) { + $this->importHelper->linking(); + } elseif ($this->importHelper->isIndexingRequired()) { + $this->importHelper->indexing(); + } else { + $this->importHelper->completed(); + } + } + + return new JsonResponse([ + 'stats' => $this->importHelper->stats(Import::STATE_PROCESSED), + 'import' => $this->importHelper->getImport()->unsetRelations(), + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function link(int $id): JsonResponse + { + $import = $this->importRepository->findOrFail($id); + + if (! $import->processed_rows_count) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.data-transfer.imports.nothing-to-import'), + ], 400); + } + + $this->importHelper->setImport($import); + + if (! $this->importHelper->isValid()) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.data-transfer.imports.not-valid'), + ], 400); + } + + /** + * Set the import state to linking + */ + if ($import->state == Import::STATE_PROCESSED) { + $this->importHelper->linking(); + } + + /** + * Get the first processing batch to link + */ + $importBatch = $import->batches->where('state', Import::STATE_PROCESSED)->first(); + + /** + * Set the import state to linking/completed + */ + if ($importBatch) { + /** + * Start the resource linking process + */ + try { + $this->importHelper->link($importBatch); + } catch (\Exception $e) { + return new JsonResponse([ + 'message' => $e->getMessage(), + ], 400); + } + } else { + if ($this->importHelper->isIndexingRequired()) { + $this->importHelper->indexing(); + } else { + $this->importHelper->completed(); + } + } + + return new JsonResponse([ + 'stats' => $this->importHelper->stats(Import::STATE_LINKED), + 'import' => $this->importHelper->getImport()->unsetRelations(), + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function indexData(int $id): JsonResponse + { + $import = $this->importRepository->findOrFail($id); + + if (! $import->processed_rows_count) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.data-transfer.imports.nothing-to-import'), + ], 400); + } + + $this->importHelper->setImport($import); + + if (! $this->importHelper->isValid()) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.data-transfer.imports.not-valid'), + ], 400); + } + + /** + * Set the import state to linking + */ + if ($import->state == Import::STATE_LINKED) { + $this->importHelper->indexing(); + } + + /** + * Get the first processing batch to link + */ + $importBatch = $import->batches->where('state', Import::STATE_LINKED)->first(); + + /** + * Set the import state to linking/completed + */ + if ($importBatch) { + /** + * Start the resource linking process + */ + try { + $this->importHelper->index($importBatch); + } catch (\Exception $e) { + return new JsonResponse([ + 'message' => $e->getMessage(), + ], 400); + } + } else { + /** + * Set the import state to completed + */ + $this->importHelper->completed(); + } + + return new JsonResponse([ + 'stats' => $this->importHelper->stats(Import::STATE_INDEXED), + 'import' => $this->importHelper->getImport()->unsetRelations(), + ]); + } + + /** + * Returns import stats + */ + public function stats(int $id, string $state = Import::STATE_PROCESSED): JsonResponse + { + $import = $this->importRepository->findOrFail($id); + + $stats = $this->importHelper + ->setImport($import) + ->stats($state); + + return new JsonResponse([ + 'stats' => $stats, + 'stuck' => $this->importHelper->isStuck(), + 'import' => $this->importHelper->getImport()->unsetRelations(), + ]); + } + + /** + * Download import error report + */ + public function downloadSample(string $type) + { + $importer = config('importers.'.$type); + + $content = Storage::get($importer['sample_path']); + + /** + * Append the user defined attribute codes as extra columns so the downloaded sample always + * reflects the custom attributes configured for the entity type. Only codes that are valid + * import column names (lowercase, matching the importer's column rule) are included so the + * generated sample remains importable. + */ + $customAttributeCodes = $this->attributeRepository + ->findWhere(['entity_type' => $type, 'is_user_defined' => 1]) + ->pluck('code') + ->filter(fn ($code) => preg_match('/^[a-z][a-z0-9_]*$/', $code)) + ->all(); + + if (! empty($customAttributeCodes)) { + $content = $this->appendColumnsToCsv($content, $customAttributeCodes); + } + + return response($content, 200, [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="'.basename($importer['sample_path']).'"', + ]); + } + + /** + * Append the given columns to the header row and an empty value to every data row of a CSV. + */ + protected function appendColumnsToCsv(string $content, array $columns): string + { + $lines = preg_split('/\r\n|\r|\n/', rtrim($content, "\r\n")); + + $suffix = str_repeat(',', count($columns)); + + return collect($lines) + ->filter(fn ($line) => $line !== '') + ->map(fn ($line, $index) => $index === 0 + ? $line.','.implode(',', $columns) + : $line.$suffix) + ->implode("\n")."\n"; + } + + /** + * Download import error report + */ + public function download(int $id) + { + $import = $this->importRepository->findOrFail($id); + + return Storage::disk('public')->download($import->file_path); + } + + /** + * Download import error report + */ + public function downloadErrorReport(int $id) + { + $import = $this->importRepository->findOrFail($id); + + return Storage::disk('public')->download($import->file_path); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/EmailTemplateController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/EmailTemplateController.php new file mode 100755 index 0000000..60474e4 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/EmailTemplateController.php @@ -0,0 +1,133 @@ +ajax()) { + return datagrid(EmailTemplateDataGrid::class)->process(); + } + + return view('admin::settings.email-templates.index'); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function create() + { + $placeholders = $this->workflowEntityHelper->getEmailTemplatePlaceholders(); + + return view('admin::settings.email-templates.create', compact('placeholders')); + } + + /** + * Store a newly created email templates in storage. + */ + public function store(): RedirectResponse + { + $this->validate(request(), [ + 'name' => 'required|unique:email_templates,name', + 'subject' => 'required', + 'content' => 'required', + ]); + + Event::dispatch('settings.email_templates.create.before'); + + $emailTemplate = $this->emailTemplateRepository->create(request()->all()); + + Event::dispatch('settings.email_templates.create.after', $emailTemplate); + + session()->flash('success', trans('admin::app.settings.email-template.index.create-success')); + + return redirect()->route('admin.settings.email_templates.index'); + } + + /** + * Show the form for editing the specified email template. + */ + public function edit(int $id): View + { + $emailTemplate = $this->emailTemplateRepository->findOrFail($id); + + $placeholders = $this->workflowEntityHelper->getEmailTemplatePlaceholders(); + + return view('admin::settings.email-templates.edit', compact('emailTemplate', 'placeholders')); + } + + /** + * Update the specified email template in storage. + */ + public function update(int $id): RedirectResponse + { + $this->validate(request(), [ + 'name' => 'required|unique:email_templates,name,'.$id, + 'subject' => 'required', + 'content' => 'required', + ]); + + Event::dispatch('settings.email_templates.update.before', $id); + + $emailTemplate = $this->emailTemplateRepository->update(request()->all(), $id); + + Event::dispatch('settings.email_templates.update.after', $emailTemplate); + + session()->flash('success', trans('admin::app.settings.email-template.index.update-success')); + + return redirect()->route('admin.settings.email_templates.index'); + } + + /** + * Remove the specified email template from storage. + */ + public function destroy(int $id): JsonResponse + { + $emailTemplate = $this->emailTemplateRepository->findOrFail($id); + + try { + Event::dispatch('settings.email_templates.delete.before', $id); + + $emailTemplate->delete($id); + + Event::dispatch('settings.email_templates.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.settings.email-template.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.settings.email-template.index.delete-failed'), + ], 400); + } + + return response()->json([ + 'message' => trans('admin::app.settings.email-template.index.delete-failed'), + ], 400); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/GoogleContactController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/GoogleContactController.php new file mode 100644 index 0000000..80ed0ce --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/GoogleContactController.php @@ -0,0 +1,130 @@ +googleContactAccountRepository->findOneByField('user_id', auth()->guard('user')->id()); + + return view('admin::settings.google-contacts.index', compact('account')); + } + + /** + * Redirect the user to Google's OAuth consent screen. + */ + public function connect(): RedirectResponse + { + /** + * Guard against missing OAuth credentials so the user sees a clear message instead of an + * unhandled exception from the Google client (issue #2633). + */ + if (! $this->googleClientFactory->isConfigured()) { + session()->flash('error', trans('admin::app.settings.google-contacts.index.not-configured')); + + return redirect()->route('admin.settings.google_contacts.index'); + } + + $client = $this->googleClientFactory->make(); + + return redirect()->away($client->createAuthUrl()); + } + + /** + * Handle the OAuth callback from Google, exchange the code for tokens, + * and store the connection for the current user. + */ + public function callback(Request $request): RedirectResponse + { + if ($request->filled('error')) { + session()->flash('error', trans('admin::app.settings.google-contacts.index.connect-failed')); + + return redirect()->route('admin.settings.google_contacts.index'); + } + + $request->validate([ + 'code' => 'required|string', + ]); + + $client = $this->googleClientFactory->make(); + + $token = $client->fetchAccessTokenWithAuthCode($request->input('code')); + + if (isset($token['error'])) { + session()->flash('error', trans('admin::app.settings.google-contacts.index.connect-failed')); + + return redirect()->route('admin.settings.google_contacts.index'); + } + + $client->setAccessToken($token); + + $googleEmail = (new Oauth2($client))->userinfo->get()->email; + + $this->googleContactAccountRepository->updateOrCreate( + ['user_id' => auth()->guard('user')->id()], + [ + 'user_id' => auth()->guard('user')->id(), + 'google_email' => $googleEmail, + 'access_token' => $token['access_token'], + 'refresh_token' => $token['refresh_token'] ?? null, + 'expires_at' => now()->addSeconds($token['expires_in'] ?? 3600), + 'scopes' => explode(' ', $token['scope'] ?? ''), + ] + ); + + session()->flash('success', trans('admin::app.settings.google-contacts.index.connect-success')); + + return redirect()->route('admin.settings.google_contacts.index'); + } + + /** + * Disconnect the current user's Google account. + */ + public function disconnect(): RedirectResponse + { + $account = $this->googleContactAccountRepository->findOneByField('user_id', auth()->guard('user')->id()); + + if ($account) { + try { + $client = $this->googleClientFactory->make(); + + $client->setAccessToken([ + 'access_token' => $account->access_token, + 'refresh_token' => $account->refresh_token, + ]); + + $client->revokeToken(); + } catch (\Exception $exception) { + // Token may already be invalid/expired on Google's side; proceed with local cleanup regardless. + } + + $this->googleContactAccountRepository->delete($account->id); + } + + session()->flash('success', trans('admin::app.settings.google-contacts.index.disconnect-success')); + + return redirect()->route('admin.settings.google_contacts.index'); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/GroupController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/GroupController.php new file mode 100755 index 0000000..1574465 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/GroupController.php @@ -0,0 +1,128 @@ +ajax()) { + return datagrid(GroupDataGrid::class)->process(); + } + + return view('admin::settings.groups.index'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): JsonResponse + { + $this->validate(request(), [ + 'name' => 'required|unique:groups,name|max:50', + 'description' => 'required|max:250', + ]); + + Event::dispatch('settings.group.create.before'); + + $group = $this->groupRepository->create(request()->only([ + 'name', + 'description', + ])); + + Event::dispatch('settings.group.create.after', $group); + + return new JsonResponse([ + 'data' => $group, + 'message' => trans('admin::app.settings.groups.index.create-success'), + ]); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): JsonResource + { + $group = $this->groupRepository->findOrFail($id); + + return new JsonResource([ + 'data' => $group, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(int $id): JsonResponse + { + $this->validate(request(), [ + 'name' => 'required|max:50|unique:groups,name,'.$id, + 'description' => 'required|max:250', + ]); + + Event::dispatch('settings.group.update.before', $id); + + $group = $this->groupRepository->update(request()->only([ + 'name', + 'description', + ]), $id); + + Event::dispatch('settings.group.update.after', $group); + + return new JsonResponse([ + 'data' => $group, + 'message' => trans('admin::app.settings.groups.index.update-success'), + ]); + } + + /** + * Remove the specified resource from storage. + * + * @return Response + */ + public function destroy(int $id): JsonResponse + { + $group = $this->groupRepository->findOrFail($id); + + if ($group->users()->exists()) { + return response()->json([ + 'message' => trans('admin::app.settings.groups.index.delete-failed-associated-users'), + ], 400); + } + + try { + Event::dispatch('settings.group.delete.before', $id); + + $group->delete($id); + + Event::dispatch('settings.group.delete.after', $id); + + return new JsonResponse([ + 'message' => trans('admin::app.settings.groups.index.destroy-success'), + ], 200); + } catch (\Exception $exception) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.groups.index.delete-failed'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/LocationController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/LocationController.php new file mode 100644 index 0000000..b7ef3df --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/LocationController.php @@ -0,0 +1,80 @@ +locationRepository + ->pushCriteria(app(RequestCriteria::class)) + ->all(); + + return response()->json([ + 'data' => $results, + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(AttributeForm $request): JsonResponse + { + Event::dispatch('settings.location.create.before'); + + $location = $this->locationRepository->create(request()->all()); + + Event::dispatch('settings.location.create.after', $location); + + return new JsonResponse([ + 'data' => $location, + 'message' => trans('admin::app.settings.warehouses.view.locations.create-success'), + ]); + } + + /** + * Remove the specified resource from storage. + * + * @return Response + */ + public function destroy(int $id): JsonResponse + { + $this->locationRepository->findOrFail($id); + + try { + Event::dispatch('settings.location.delete.before', $id); + + $this->locationRepository->delete($id); + + Event::dispatch('settings.location.delete.after', $id); + + return new JsonResponse([ + 'message' => trans('admin::app.settings.warehouses.view.locations.delete-success'), + ], 200); + } catch (\Exception $exception) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.warehouses.view.locations.delete-failed'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/Marketing/CampaignsController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/Marketing/CampaignsController.php new file mode 100644 index 0000000..e16a3cd --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/Marketing/CampaignsController.php @@ -0,0 +1,157 @@ +isXmlHttpRequest()) { + return datagrid(CampaignDatagrid::class)->process(); + } + + return view('admin::settings.marketing.campaigns.index'); + } + + /** + * Get marketing events. + */ + public function getEvents(): JsonResponse + { + $events = $this->eventRepository->get(['id', 'name']); + + return response()->json([ + 'data' => $events, + ]); + } + + /** + * Get Email Templates. + */ + public function getEmailTemplates(): JsonResponse + { + $emailTemplates = $this->emailTemplateRepository->get(['id', 'name']); + + return response()->json([ + 'data' => $emailTemplates, + ]); + } + + /** + * Store a newly created marketing campaign in storage. + */ + public function store(): JsonResponse + { + $validatedData = $this->validate(request(), [ + 'name' => 'required|string|max:255', + 'subject' => 'required|string|max:255', + 'marketing_template_id' => 'required|exists:email_templates,id', + 'marketing_event_id' => 'required|exists:marketing_events,id', + 'status' => 'sometimes|required|in:0,1', + ]); + + Event::dispatch('settings.marketing.campaigns.create.before'); + + $marketingCampaign = $this->campaignRepository->create($validatedData); + + Event::dispatch('settings.marketing.campaigns.create.after', $marketingCampaign); + + return response()->json([ + 'message' => trans('admin::app.settings.marketing.campaigns.index.create-success'), + ]); + } + + /** + * Show the specified Resource. + */ + public function show(int $id): JsonResponse + { + $campaign = $this->campaignRepository->findOrFail($id); + + return response()->json([ + 'data' => $campaign, + ]); + } + + /** + * Update the specified marketing campaign in storage. + */ + public function update(int $id): JsonResponse + { + $validatedData = $this->validate(request(), [ + 'name' => 'required|string|max:255', + 'subject' => 'required|string|max:255', + 'marketing_template_id' => 'required|exists:email_templates,id', + 'marketing_event_id' => 'required|exists:marketing_events,id', + 'status' => 'sometimes|required|in:0,1', + ]); + + Event::dispatch('settings.marketing.campaigns.update.before', $id); + + $marketingCampaign = $this->campaignRepository->update($validatedData, $id); + + Event::dispatch('settings.marketing.campaigns.update.after', $marketingCampaign); + + return response()->json([ + 'message' => trans('admin::app.settings.marketing.campaigns.index.update-success'), + ]); + } + + /** + * Remove the specified marketing campaign from storage. + */ + public function destroy(int $id): JsonResponse + { + Event::dispatch('settings.marketing.campaigns.delete.before', $id); + + $this->campaignRepository->delete($id); + + Event::dispatch('settings.marketing.campaigns.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.settings.marketing.campaigns.index.delete-success'), + ]); + } + + /** + * Remove the specified marketing campaigns from storage. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $campaigns = $this->campaignRepository->findWhereIn('id', $massDestroyRequest->input('indices')); + + foreach ($campaigns as $campaign) { + Event::dispatch('settings.marketing.campaigns.delete.before', $campaign); + + $this->campaignRepository->delete($campaign->id); + + Event::dispatch('settings.marketing.campaigns.delete.after', $campaign); + } + + return response()->json([ + 'message' => trans('admin::app.settings.marketing.campaigns.index.mass-delete-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/Marketing/EventController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/Marketing/EventController.php new file mode 100644 index 0000000..82cae22 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/Marketing/EventController.php @@ -0,0 +1,169 @@ +ajax()) { + return datagrid(EventDataGrid::class)->process(); + } + + return view('admin::settings.marketing.events.index'); + } + + /** + * Store a newly created marketing event in storage. + */ + public function store(): JsonResponse + { + $validatedData = $this->validate(request(), [ + 'name' => 'required|max:60', + 'description' => 'required', + 'date' => 'required|date|after_or_equal:today', + ]); + + Event::dispatch('settings.marketing.events.create.before'); + + $marketingEvent = $this->eventRepository->create($validatedData); + + Event::dispatch('settings.marketing.events.create.after', $marketingEvent); + + return response()->json([ + 'message' => trans('admin::app.settings.marketing.events.index.create-success'), + 'data' => $marketingEvent, + ]); + } + + /** + * Update the specified marketing event in storage. + */ + public function update(int $id): JsonResponse + { + $validatedData = $this->validate(request(), [ + 'name' => 'required|max:60', + 'description' => 'required', + 'date' => 'required|date|after_or_equal:today', + ]); + + Event::dispatch('settings.marketing.events.update.before', $id); + + $marketingEvent = $this->eventRepository->update($validatedData, $id); + + Event::dispatch('settings.marketing.events.update.after', $marketingEvent); + + return response()->json([ + 'message' => trans('admin::app.settings.marketing.events.index.update-success'), + 'data' => $marketingEvent, + ]); + } + + /** + * Remove the specified marketing event from storage. + */ + public function destroy(int $id): JsonResponse + { + $event = $this->eventRepository->findOrFail($id); + + if ($event->campaigns->isNotEmpty()) { + return response()->json([ + 'message' => trans('admin::app.settings.marketing.events.index.delete-failed-associated-campaigns'), + ], 422); + } + + Event::dispatch('settings.marketing.events.delete.before', $event); + + $this->eventRepository->delete($event->id); + + Event::dispatch('settings.marketing.events.delete.after', $event); + + return response()->json([ + 'message' => trans('admin::app.settings.marketing.events.index.delete-success'), + ]); + } + + /** + * Remove the specified marketing events from storage. + */ + public function massDestroy(MassDestroyRequest $request): JsonResponse + { + try { + $events = $this->eventRepository->findWhereIn('id', $request->input('indices', [])); + + $deletedCount = 0; + + $blockedCount = 0; + + foreach ($events as $event) { + if ( + $event->campaigns + && $event->campaigns->isNotEmpty() + ) { + $blockedCount++; + + continue; + } + + Event::dispatch('settings.marketing.events.delete.before', $event); + + $this->eventRepository->delete($event->id); + + Event::dispatch('settings.marketing.events.delete.after', $event); + + $deletedCount++; + } + + $statusCode = 200; + + switch (true) { + case $deletedCount > 0 && $blockedCount === 0: + $message = trans('admin::app.settings.marketing.events.index.mass-delete-success'); + + break; + + case $deletedCount > 0 && $blockedCount > 0: + $message = trans('admin::app.settings.marketing.events.index.partial-delete-warning'); + + break; + + case $deletedCount === 0 && $blockedCount > 0: + $message = trans('admin::app.settings.marketing.events.index.none-delete-warning'); + + $statusCode = 400; + + break; + + default: + $message = trans('admin::app.settings.marketing.events.index.no-selection'); + + $statusCode = 400; + + break; + } + + return response()->json(['message' => $message], $statusCode); + } catch (Exception $e) { + return response()->json([ + 'message' => trans('admin::app.settings.marketing.events.index.mass-delete-failed'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/PipelineController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/PipelineController.php new file mode 100644 index 0000000..93011f0 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/PipelineController.php @@ -0,0 +1,152 @@ +ajax()) { + return datagrid(PipelineDataGrid::class)->process(); + } + + return view('admin::settings.pipelines.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::settings.pipelines.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(PipelineForm $request): RedirectResponse + { + $request->validated(); + + $request->merge([ + 'is_default' => request()->has('is_default') ? 1 : 0, + ]); + + Event::dispatch('settings.pipeline.create.before'); + + $pipeline = $this->pipelineRepository->create($request->all()); + + Event::dispatch('settings.pipeline.create.after', $pipeline); + + session()->flash('success', trans('admin::app.settings.pipelines.index.create-success')); + + return redirect()->route('admin.settings.pipelines.index'); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View + { + $pipeline = $this->pipelineRepository->findOrFail($id); + + return view('admin::settings.pipelines.edit', compact('pipeline')); + } + + /** + * Update the specified resource in storage. + */ + public function update(PipelineForm $request, int $id): RedirectResponse + { + $request->validated(); + + $isDefault = request()->has('is_default') ? 1 : 0; + + if (! $isDefault) { + $defaultCount = $this->pipelineRepository->findWhere(['is_default' => 1])->count(); + + $pipeline = $this->pipelineRepository->find($id); + + if ( + $defaultCount === 1 + && $pipeline->is_default + ) { + session()->flash('error', trans('admin::app.settings.pipelines.index.default-required')); + + return redirect()->back(); + } + } + + $request->merge(['is_default' => $isDefault]); + + Event::dispatch('settings.pipeline.update.before', $id); + + $pipeline = $this->pipelineRepository->update($request->all(), $id); + + Event::dispatch('settings.pipeline.update.after', $pipeline); + + session()->flash('success', trans('admin::app.settings.pipelines.index.update-success')); + + return redirect()->route('admin.settings.pipelines.index'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy($id): JsonResponse + { + $pipeline = $this->pipelineRepository->findOrFail($id); + + if ($pipeline->is_default) { + return response()->json([ + 'message' => trans('admin::app.settings.pipelines.index.default-delete-error'), + ], 400); + } else { + $defaultPipeline = $this->pipelineRepository->getDefaultPipeline(); + + $pipeline->leads()->update([ + 'lead_pipeline_id' => $defaultPipeline->id, + 'lead_pipeline_stage_id' => $defaultPipeline->stages()->first()->id, + ]); + } + + try { + Event::dispatch('settings.pipeline.delete.before', $id); + + $this->pipelineRepository->delete($id); + + Event::dispatch('settings.pipeline.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.settings.pipelines.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.settings.pipelines.index.delete-failed'), + ], 400); + } + + return response()->json([ + 'message' => trans('admin::app.settings.pipelines.index.delete-failed'), + ], 400); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/RoleController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/RoleController.php new file mode 100755 index 0000000..d01a7be --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/RoleController.php @@ -0,0 +1,283 @@ +ajax()) { + return datagrid(RoleDataGrid::class)->process(); + } + + return view('admin::settings.roles.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::settings.roles.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): RedirectResponse|JsonResponse + { + $this->validate(request(), [ + 'name' => 'required', + 'permission_type' => 'required|in:all,custom', + 'description' => 'required', + ]); + + if (request('permission_type') == 'custom') { + $this->validate(request(), [ + 'permissions' => 'required', + ]); + } + + /** + * A non-administrator may neither create a full-administrator role nor grant permissions + * beyond their own, so role management cannot be used to escalate privileges. The denial is + * surfaced as a flash message rather than a 401 page, since this is a standard form submit. + */ + if (! $this->canManageRole(request('permission_type'), (array) request('permissions'))) { + if (request()->ajax()) { + return response()->json([ + 'message' => trans('admin::app.errors.role-permissions-exceed-own'), + ], 401); + } + + session()->flash('error', trans('admin::app.errors.role-permissions-exceed-own')); + + return redirect()->back()->withInput(); + } + + Event::dispatch('settings.role.create.before'); + + $data = request()->only([ + 'name', + 'description', + 'permission_type', + 'permissions', + ]); + + /** + * Record who created the role so the listing can scope it by the acting user's data scope + * (a user with the `individual` scope sees only the roles they created). + */ + $data['created_by'] = auth()->guard('user')->id(); + + $role = $this->roleRepository->create($data); + + Event::dispatch('settings.role.create.after', $role); + + if (request()->ajax()) { + return response()->json([ + 'data' => $role, + 'message' => trans('admin::app.settings.roles.index.create-success'), + ]); + } + + session()->flash('success', trans('admin::app.settings.roles.index.create-success')); + + return redirect()->route('admin.settings.roles.index'); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View|RedirectResponse + { + $role = $this->roleRepository->findOrFail($id); + + /** + * A user — including a full administrator — may never edit the role currently assigned to + * their own account, otherwise they could grant themselves additional permissions by + * editing their own role. This mirrors the block on deleting your own role. + */ + if (auth()->guard('user')->user()->role_id == $id) { + session()->flash('error', trans('admin::app.settings.roles.index.current-role-edit-error')); + + return redirect()->route('admin.settings.roles.index'); + } + + return view('admin::settings.roles.edit', compact('role')); + } + + /** + * Update the specified resource in storage. + */ + public function update(int $id): RedirectResponse + { + $this->validate(request(), [ + 'name' => 'required', + 'permission_type' => 'required|in:all,custom', + 'description' => 'required', + 'permissions' => 'required_if:permission_type,custom', + ]); + + $role = $this->roleRepository->findOrFail($id); + + /** + * A user — including a full administrator — may never edit the role currently assigned to + * their own account, otherwise they could grant themselves additional permissions by + * editing their own role. This mirrors the block on deleting your own role. + */ + if (auth()->guard('user')->user()->role_id == $id) { + session()->flash('error', trans('admin::app.settings.roles.index.current-role-edit-error')); + + return redirect()->back(); + } + + /** + * A non-administrator may neither modify a role that is broader than their own (such as the + * Administrator role) nor promote a role to full administrator nor grant permissions beyond + * their own, so role management cannot be used to escalate privileges. The denial is + * surfaced as a flash message rather than a 401 page, since this is a standard form submit. + */ + if (! $this->canManageRole(request('permission_type'), (array) request('permissions'), $role)) { + session()->flash('error', trans('admin::app.errors.role-permissions-exceed-own')); + + return redirect()->back()->withInput(); + } + + Event::dispatch('settings.role.update.before', $id); + + $data = array_merge(request()->only([ + 'name', + 'description', + 'permission_type', + ]), [ + 'permissions' => request()->has('permissions') ? request('permissions') : [], + ]); + + $role = $this->roleRepository->update($data, $id); + + Event::dispatch('settings.role.update.after', $role); + + session()->flash('success', trans('admin::app.settings.roles.index.update-success')); + + return redirect()->back(); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $response = [ + 'responseCode' => 400, + ]; + + $role = $this->roleRepository->findOrFail($id); + + if ($role->users && $role->users->count() >= 1) { + $response['message'] = trans('admin::app.settings.roles.index.being-used'); + + session()->flash('error', $response['message']); + } elseif ($this->roleRepository->count() == 1) { + $response['message'] = trans('admin::app.settings.roles.index.last-delete-error'); + + session()->flash('error', $response['message']); + } else { + try { + Event::dispatch('settings.role.delete.before', $id); + + if (auth()->guard('user')->user()->role_id == $id) { + $response['message'] = trans('admin::app.settings.roles.index.current-role-delete-error'); + } else { + $this->roleRepository->delete($id); + + Event::dispatch('settings.role.delete.after', $id); + + $message = trans('admin::app.settings.roles.index.delete-success'); + + $response = [ + 'responseCode' => 200, + 'message' => $message, + ]; + + session()->flash('success', $message); + } + } catch (\Exception $exception) { + $message = $exception->getMessage(); + + $response['message'] = $message; + + session()->flash('error', $message); + } + } + + return response()->json($response, $response['responseCode']); + } + + /** + * Determine whether the acting user may create or edit a role with the given permissions. + * + * A full administrator may manage any role. Everyone else may only manage custom roles whose + * permissions are a subset of their own, may never create or promote a role to full + * administrator, and may never modify a role that already holds privileges beyond their own + * (such as the Administrator role). This matches the "cannot grant or edit a role above your + * own" model used by mainstream CRMs. + * + * @param Role|null $existingRole + */ + protected function canManageRole(?string $permissionType, array $permissions, $existingRole = null): bool + { + $authUser = auth()->guard('user')->user(); + + /** + * Full administrators are trusted to manage any role. + */ + if ($authUser->role?->permission_type === 'all') { + return true; + } + + $ownPermissions = $authUser->role?->permissions ?? []; + + /** + * A non-administrator can never create or promote a role to full administrator, nor grant + * any permission they do not personally hold. + */ + if ($permissionType === 'all' + || ! empty(array_diff($permissions, $ownPermissions)) + ) { + return false; + } + + /** + * The role being edited must not already be broader than the acting user's own role, so a + * full-administrator role (or any role holding extra permissions) cannot be tampered with. + */ + if ($existingRole + && ($existingRole->permission_type === 'all' + || ! empty(array_diff($existingRole->permissions ?? [], $ownPermissions))) + ) { + return false; + } + + return true; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/SettingController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/SettingController.php new file mode 100644 index 0000000..ad1dbe2 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/SettingController.php @@ -0,0 +1,98 @@ +query('query')); + + if (empty($query)) { + return response()->json(['data' => []]); + } + + $results = $this->searchMenuItems($this->getSettingsConfig(), $query); + + return response()->json([ + 'data' => $results->values(), + ]); + } + + /** + * Recursively search through menu items and children. + * + * @param Collection $menuItems + * @return Collection> + */ + protected function searchMenuItems(Collection $menuItems, string $query): Collection + { + $results = collect(); + + foreach ($menuItems as $item) { + if ($this->matchesQuery($item, $query)) { + $results->push([ + 'name' => $item->getName(), + 'url' => $item->getUrl(), + 'icon' => $item->getIcon(), + 'key' => $item->getKey(), + ]); + } + + if ($item->haveChildren()) { + $childResults = $this->searchMenuItems($item->getChildren(), $query); + + $results = $results->merge($childResults); + } + } + + return $results; + } + + /** + * Determine if the menu item matches the query. + */ + protected function matchesQuery(MenuItem $item, string $query): bool + { + $query = strtolower($query); + $url = strtolower($item->getUrl()); + + if ( + ! $url + || ! str_contains($url, $query) + ) { + return false; + } + + return true; + } + + /** + * Get the settings configuration. + */ + protected function getSettingsConfig(): Collection + { + return menu() + ->getItems('admin') + ->filter(fn (MenuItem $item) => $item->getKey() === 'settings'); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/SourceController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/SourceController.php new file mode 100755 index 0000000..70bac19 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/SourceController.php @@ -0,0 +1,117 @@ +ajax()) { + return datagrid(SourceDataGrid::class)->process(); + } + + return view('admin::settings.sources.index'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): JsonResponse + { + $this->validate(request(), [ + 'name' => ['required', 'unique:lead_sources,name'], + ]); + + Event::dispatch('settings.source.create.before'); + + $source = $this->sourceRepository->create(request()->only(['name'])); + + Event::dispatch('settings.source.create.after', $source); + + return new JsonResponse([ + 'data' => $source, + 'message' => trans('admin::app.settings.sources.index.create-success'), + ]); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View|JsonResponse + { + $source = $this->sourceRepository->findOrFail($id); + + return new JsonResponse([ + 'data' => $source, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(int $id): JsonResponse + { + $this->validate(request(), [ + 'name' => 'required|unique:lead_sources,name,'.$id, + ]); + + Event::dispatch('settings.source.update.before', $id); + + $source = $this->sourceRepository->update(request()->only(['name']), $id); + + Event::dispatch('settings.source.update.after', $source); + + return new JsonResponse([ + 'data' => $source, + 'message' => trans('admin::app.settings.sources.index.update-success'), + ]); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $source = $this->sourceRepository->findOrFail($id); + + if ($source->leads()->count() > 0) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.sources.index.delete-failed-associated-leads'), + ], 400); + } + + try { + Event::dispatch('settings.source.delete.before', $id); + + $source->delete(); + + Event::dispatch('settings.source.delete.after', $id); + + return new JsonResponse([ + 'message' => trans('admin::app.settings.sources.index.delete-success'), + ], 200); + } catch (Exception $exception) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.sources.index.delete-failed'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/TagController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/TagController.php new file mode 100755 index 0000000..9b77555 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/TagController.php @@ -0,0 +1,177 @@ +ajax()) { + return datagrid(TagDataGrid::class)->process(); + } + + return view('admin::settings.tags.index'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): JsonResponse + { + $this->validate(request(), [ + 'name' => ['required', 'unique:tags,name', 'max:50'], + ]); + + Event::dispatch('settings.tag.create.before'); + + $tag = $this->tagRepository->create(array_merge(request()->only([ + 'name', + 'color', + ]), [ + 'user_id' => auth()->guard('user')->user()->id, + ])); + + Event::dispatch('settings.tag.create.after', $tag); + + return new JsonResponse([ + 'data' => new TagResource($tag), + 'message' => trans('admin::app.settings.tags.index.create-success'), + ]); + } + + /** + * Show the form for editing the specified tag. + */ + public function edit(int $id): View|JsonResponse + { + $tag = $this->tagRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($tag->user_id); + + return new JsonResponse([ + 'data' => $tag, + ]); + } + + /** + * Update the specified tag in storage. + */ + public function update(int $id): JsonResponse + { + $this->validate(request(), [ + 'name' => 'required|max:50|unique:tags,name,'.$id, + ]); + + $this->preventUnauthorizedAccess($this->tagRepository->findOrFail($id)->user_id); + + Event::dispatch('settings.tag.update.before', $id); + + $tag = $this->tagRepository->update(request()->only([ + 'name', + 'color', + ]), $id); + + Event::dispatch('settings.tag.update.after', $tag); + + return new JsonResponse([ + 'data' => new TagResource($tag), + 'message' => trans('admin::app.settings.tags.index.update-success'), + ]); + } + + /** + * Remove the specified type from storage. + */ + public function destroy(int $id): JsonResponse + { + $tag = $this->tagRepository->findOrFail($id); + + $this->preventUnauthorizedAccess($tag->user_id); + + try { + Event::dispatch('settings.tag.delete.before', $id); + + $tag->delete($id); + + Event::dispatch('settings.tag.delete.after', $id); + + return new JsonResponse([ + 'message' => trans('admin::app.settings.tags.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.tags.index.delete-failed'), + ], 400); + } + } + + /** + * Search tag results + * + * @return Response + */ + public function search() + { + $tagRepository = $this->tagRepository->pushCriteria(app(RequestCriteria::class)); + + /** + * Match the tag datagrid: a user without global visibility only sees tags they own (or that + * their group owns). Without this, search returned every tag and leaked the ids of records + * hidden from the scoped listing, which the edit/update/delete handlers could then act on. + */ + $tags = ($userIds = bouncer()->getAuthorizedUserIds()) + ? $tagRepository->findWhereIn('user_id', $userIds) + : $tagRepository->all(); + + return TagResource::collection($tags); + } + + /** + * Mass Delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $tags = $this->filterAuthorizedRecords( + $this->tagRepository->findWhereIn('id', $massDestroyRequest->input('indices', [])) + ); + + try { + foreach ($tags as $tag) { + Event::dispatch('settings.tag.delete.before', $tag->id); + + $this->tagRepository->delete($tag->id); + + Event::dispatch('settings.tag.delete.after', $tag->id); + } + + return new JsonResponse([ + 'message' => trans('admin::app.customers.reviews.index.datagrid.mass-delete-success'), + ], 200); + } catch (\Exception $e) { + return new JsonResponse([ + 'message' => $e->getMessage(), + ], 500); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/TypeController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/TypeController.php new file mode 100755 index 0000000..1be1abb --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/TypeController.php @@ -0,0 +1,110 @@ +ajax()) { + return datagrid(TypeDataGrid::class)->process(); + } + + return view('admin::settings.types.index'); + } + + /** + * Store a newly created type in storage. + */ + public function store(): JsonResponse + { + $this->validate(request(), [ + 'name' => ['required', 'unique:lead_types,name'], + ]); + + Event::dispatch('settings.type.create.before'); + + $type = $this->typeRepository->create(request()->only(['name'])); + + Event::dispatch('settings.type.create.after', $type); + + return new JsonResponse([ + 'data' => $type, + 'message' => trans('admin::app.settings.types.index.create-success'), + ]); + } + + /** + * Show the form for editing the specified type. + */ + public function edit(int $id): View|JsonResponse + { + $type = $this->typeRepository->findOrFail($id); + + return new JsonResponse([ + 'data' => $type, + ]); + } + + /** + * Update the specified type in storage. + */ + public function update(int $id): JsonResponse + { + $this->validate(request(), [ + 'name' => 'required|unique:lead_types,name,'.$id, + ]); + + Event::dispatch('settings.type.update.before', $id); + + $type = $this->typeRepository->update(request()->only(['name']), $id); + + Event::dispatch('settings.type.update.after', $type); + + return new JsonResponse([ + 'data' => $type, + 'message' => trans('admin::app.settings.types.index.update-success'), + ]); + } + + /** + * Remove the specified type from storage. + */ + public function destroy(int $id): JsonResponse + { + $type = $this->typeRepository->findOrFail($id); + + try { + Event::dispatch('settings.type.delete.before', $id); + + $type->delete($id); + + Event::dispatch('settings.type.delete.after', $id); + + return new JsonResponse([ + 'message' => trans('admin::app.settings.types.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.types.index.delete-failed'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/UserController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/UserController.php new file mode 100755 index 0000000..82228b0 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/UserController.php @@ -0,0 +1,518 @@ +ajax()) { + return datagrid(UserDataGrid::class)->process(); + } + + $roles = $this->assignableRoles(); + + $groups = $this->groupRepository->all(); + + return view('admin::settings.users.index', compact('roles', 'groups')); + } + + /** + * The roles the acting user is allowed to assign. + * + * A full administrator may assign any role. Everyone else — mirroring Salesforce delegated + * administration and HubSpot ("you can't assign permissions you don't have") — only sees roles + * whose privileges are a subset of their own, and never the administrator role. This keeps the + * UI in step with the server-side guard so an un-assignable role is never even offered. + */ + protected function assignableRoles() + { + $roles = $this->roleRepository->all(); + + $authUser = auth()->guard('user')->user(); + + if ($authUser->role?->permission_type === 'all') { + return $roles; + } + + $ownPermissions = $authUser->role?->permissions ?? []; + + return $roles + ->filter(fn ($role) => $role->permission_type !== 'all' + && empty(array_diff($role->permissions ?? [], $ownPermissions))) + ->values(); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): View|JsonResponse + { + $validated = $this->validate(request(), [ + 'email' => 'required|email|unique:users,email', + 'name' => 'required', + 'password' => 'nullable', + 'confirm_password' => 'nullable|required_with:password|same:password', + 'role_id' => 'required|integer|exists:roles,id', + 'status' => 'boolean|in:0,1', + 'view_permission' => 'string|in:global,group,individual', + 'groups' => 'required_if:view_permission,group|array', + 'groups.*' => 'integer|exists:groups,id', + ]); + + /** + * Reject any role that grants privileges beyond the acting user's own, so an administrator + * role (or any broader custom role) can never be granted by a non-administrator. + */ + $this->preventUnauthorizedRoleAssignment($validated['role_id']); + + /** + * Reject any data scope broader than the acting user's own on the new account — the advisory + * abuses `view_permission=global` alongside `role_id`, so both must be constrained on create. + */ + $this->preventUnauthorizedScopeAssignment(request('view_permission')); + + /** + * Build the payload from the validated data only; never mass-assign the raw request. + */ + $data = Arr::only($validated, [ + 'name', 'email', 'password', 'role_id', 'status', 'view_permission', 'groups', + ]); + + if (! empty($data['password'])) { + $data['password'] = bcrypt($data['password']); + } else { + unset($data['password']); + } + + /** + * Record who created the account so the listing can scope it by ownership (a user with the + * `individual` data scope sees only the users they created), mirroring how Krayin scopes + * lead and contact records by their owner. + */ + $data['created_by'] = auth()->guard('user')->id(); + + Event::dispatch('settings.user.create.before'); + + $admin = $this->userRepository->create($data); + + $admin->groups()->sync($data['groups'] ?? []); + + try { + Mail::queue(new UserCreatedNotification($admin)); + } catch (\Exception $e) { + report($e); + } + + Event::dispatch('settings.user.create.after', $admin); + + return new JsonResponse([ + 'data' => $admin, + 'message' => trans('admin::app.settings.users.index.create-success'), + ]); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(int $id): View|JsonResponse + { + $authUser = auth()->guard('user')->user(); + + $admin = $this->userRepository->with(['role', 'groups'])->findOrFail($id); + + /** + * A non-administrator may view their own account or any user that is not above them, but + * never a user whose privileges exceed their own (mirrors the rule enforced in update()). + */ + if ($authUser->id != $id) { + $this->preventManagingHigherPrivilegedUser($admin); + } + + return new JsonResponse([ + 'data' => $admin, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(int $id): JsonResponse + { + $authUser = auth()->guard('user')->user(); + + $isAdministrator = $authUser->role?->permission_type === 'all'; + + $isSelf = $authUser->id == $id; + + $targetUser = $this->userRepository->findOrFail($id); + + /** + * The `settings.user.users.edit` permission is already enforced by the ACL middleware. A + * non-administrator editing another user may only edit users that are not above them, so an + * administrator (or any user holding a broader role) can never be tampered with. + */ + if (! $isSelf) { + $this->preventManagingHigherPrivilegedUser($targetUser); + } + + $validated = $this->validate(request(), [ + 'email' => 'required|email|unique:users,email,'.$id, + 'name' => 'required|string', + 'password' => 'nullable|string|min:6', + 'confirm_password' => 'nullable|required_with:password|same:password', + 'role_id' => 'required|integer|exists:roles,id', + 'status' => 'nullable|boolean|in:0,1', + 'view_permission' => 'required|string|in:global,group,individual', + 'groups' => 'required_if:view_permission,group|array', + 'groups.*' => 'integer|exists:groups,id', + ]); + + /** + * A user may never escalate their own account: reject — with a clear error rather than a + * silent success — any attempt to change their own role or data scope. + */ + if ( + ! $isAdministrator + && $isSelf + && ((int) request('role_id') !== (int) $authUser->role_id + || request('view_permission') !== $authUser->view_permission) + ) { + abort(401, trans('admin::app.errors.own-privileges')); + } + + /** + * A non-administrator may only assign a role, and a data scope, that do not exceed their own. + */ + $this->preventUnauthorizedRoleAssignment($validated['role_id']); + + $this->preventUnauthorizedScopeAssignment($validated['view_permission'], $targetUser->view_permission); + + /** + * Whitelist writable fields. A user editing their OWN account may only touch profile fields + * (never self-escalate); editing another user (delegated management) they may also set the + * role, data scope, status and groups — all already constrained above to their own level. + */ + $data = Arr::only($validated, ($isAdministrator || ! $isSelf) + ? ['name', 'email', 'password', 'role_id', 'status', 'view_permission', 'groups'] + : ['name', 'email', 'password'] + ); + + if (empty($data['password'])) { + unset($data['password']); + } else { + $data['password'] = bcrypt($data['password']); + } + + /** + * The primary administrator (id 1) can never be demoted, rescoped, or disabled. Reject any + * such attempt with a clear error rather than silently ignoring it and reporting success. + */ + if ((int) $id === 1) { + if ( + (int) request('role_id') !== (int) $targetUser->role_id + || request('view_permission') !== $targetUser->view_permission + || (request()->filled('status') && ! (int) request('status')) + ) { + abort(401, trans('admin::app.errors.primary-admin-protected')); + } + + $data['status'] = 1; + } + + if ($isSelf) { + $data['status'] = true; + } + + Event::dispatch('settings.user.update.before', $id); + + $admin = $this->userRepository->update($data, $id); + + /** + * Group membership is only writable when the role/scope fields are (an administrator, or a + * delegated manager editing another user) and never for the primary administrator. + */ + if (($isAdministrator || ! $isSelf) && (int) $id !== 1) { + $admin->groups()->sync(request('groups') ?? []); + } + + Event::dispatch('settings.user.update.after', $admin); + + return new JsonResponse([ + 'data' => $admin, + 'message' => trans('admin::app.settings.users.index.update-success'), + ]); + } + + /** + * Search user results. + */ + public function search(): JsonResource + { + $users = $this->userRepository + ->pushCriteria(app(RequestCriteria::class)) + ->all(); + + return UserResource::collection($users); + } + + /** + * Destroy specified user. + */ + public function destroy(int $id): JsonResponse + { + /** + * Never delete the last remaining user or the primary administrator (id 1). + */ + if ($this->userRepository->count() == 1 || (int) $id === 1) { + return new JsonResponse([ + 'message' => trans('admin::app.settings.users.index.last-delete-error'), + ], 400); + } + + /** + * A non-administrator may not delete a user whose privileges exceed their own. + */ + if (auth()->guard('user')->user()->id != $id) { + $this->preventManagingHigherPrivilegedUser($this->userRepository->findOrFail($id)); + } + + try { + Event::dispatch('user.admin.delete.before', $id); + + $this->userRepository->delete($id); + + Event::dispatch('user.admin.delete.after', $id); + + return new JsonResponse([ + 'message' => trans('admin::app.settings.users.index.delete-success'), + ], 200); + } catch (\Exception $e) { + } + + return new JsonResponse([ + 'message' => trans('admin::app.settings.users.index.delete-failed'), + ], 500); + } + + /** + * Mass Update the specified resources. + */ + public function massUpdate(MassUpdateRequest $massDestroyRequest): JsonResponse + { + $count = 0; + + $users = $this->userRepository->findWhereIn('id', $massDestroyRequest->input('indices')); + + foreach ($users as $users) { + /** + * Skip the acting user, the primary administrator (id 1) and any user whose privileges + * exceed the acting user's own. + */ + if ( + auth()->guard('user')->user()->id == $users->id + || (int) $users->id === 1 + || ! $this->canManageUser($users) + ) { + continue; + } + + Event::dispatch('settings.user.update.before', $users->id); + + $this->userRepository->update([ + 'status' => $massDestroyRequest->input('value'), + ], $users->id); + + Event::dispatch('settings.user.update.after', $users->id); + + $count++; + } + + if (! $count) { + return response()->json([ + 'message' => trans('admin::app.settings.users.index.mass-update-failed'), + ], 400); + } + + return response()->json([ + 'message' => trans('admin::app.settings.users.index.mass-update-success'), + ]); + } + + /** + * Mass Delete the specified resources. + */ + public function massDestroy(MassDestroyRequest $massDestroyRequest): JsonResponse + { + $count = 0; + + $users = $this->userRepository->findWhereIn('id', $massDestroyRequest->input('indices')); + + foreach ($users as $user) { + /** + * Skip the acting user, the primary administrator (id 1) and any user whose privileges + * exceed the acting user's own. + */ + if ( + auth()->guard('user')->user()->id == $user->id + || (int) $user->id === 1 + || ! $this->canManageUser($user) + ) { + continue; + } + + Event::dispatch('settings.user.delete.before', $user->id); + + $this->userRepository->delete($user->id); + + Event::dispatch('settings.user.delete.after', $user->id); + + $count++; + } + + if (! $count) { + return response()->json([ + 'message' => trans('admin::app.settings.users.index.mass-delete-failed'), + ], 400); + } + + return response()->json([ + 'message' => trans('admin::app.settings.users.index.mass-delete-success'), + ]); + } + + /** + * Guard against privilege escalation through role assignment. + * + * A full administrator may assign any role, including administrator. Everyone else may only + * assign a role whose privileges are a subset of their own, and may never assign a role whose + * permission type is `all`. This mirrors the "cannot grant a role above your own" model used by + * mainstream CRMs (Salesforce role hierarchy, HubSpot/SuiteCRM/EspoCRM permission sets). + */ + protected function preventUnauthorizedRoleAssignment(?int $roleId): void + { + $authUser = auth()->guard('user')->user(); + + /** + * Full administrators are trusted to grant any role. + */ + if ($authUser->role?->permission_type === 'all') { + return; + } + + $role = $this->roleRepository->find($roleId); + + /** + * A non-administrator can never grant the administrator (`all`) role. + */ + if (! $role || $role->permission_type === 'all') { + abort(401, trans('admin::app.errors.role-exceeds-own')); + } + + /** + * The requested role must not contain any permission the acting user does not personally + * hold, so a user can never grant privileges beyond their own. + */ + if (! empty(array_diff($role->permissions ?? [], $authUser->role?->permissions ?? []))) { + abort(401, trans('admin::app.errors.role-exceeds-own')); + } + } + + /** + * Guard against widening a data scope beyond the acting user's own. + * + * A full administrator may grant any scope. Everyone else may only assign a scope that is not + * broader than their own (individual < group < global), so a delegated manager cannot grant a + * user — or themselves — more visibility than they hold. + */ + protected function preventUnauthorizedScopeAssignment(?string $scope, ?string $currentScope = null): void + { + $authUser = auth()->guard('user')->user(); + + if ($authUser->role?->permission_type === 'all') { + return; + } + + $rank = ['individual' => 0, 'group' => 1, 'global' => 2]; + + /** + * The scope may be kept at the target's existing level, but never raised beyond the greater + * of the acting user's own scope and the target's current scope — so no extra visibility is + * ever granted. + */ + $ceiling = max($rank[$authUser->view_permission] ?? 0, $rank[$currentScope] ?? 0); + + if (($rank[$scope] ?? 0) > $ceiling) { + abort(401, trans('admin::app.errors.scope-exceeds-own')); + } + } + + /** + * Determine whether the acting user may manage (edit or delete) the given user. + * + * A full administrator may manage anyone. Everyone else may manage only users that are not above + * them — a user holding the administrator role, or any permission the acting user does not + * personally hold, is off-limits. This prevents a delegated manager from tampering with a + * superior's account. + * + * @param User $targetUser + */ + protected function canManageUser($targetUser): bool + { + $authUser = auth()->guard('user')->user(); + + if ($authUser->role?->permission_type === 'all') { + return true; + } + + if ($targetUser->role?->permission_type === 'all') { + return false; + } + + return empty(array_diff($targetUser->role?->permissions ?? [], $authUser->role?->permissions ?? [])); + } + + /** + * Abort when the acting user is not allowed to manage the given user. + * + * @param User $targetUser + */ + protected function preventManagingHigherPrivilegedUser($targetUser): void + { + if (! $this->canManageUser($targetUser)) { + abort(401, trans('admin::app.errors.user-exceeds-own')); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/ActivityController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/ActivityController.php new file mode 100644 index 0000000..112d8f5 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/ActivityController.php @@ -0,0 +1,29 @@ + 'warehouse_activities', 'foreign' => 'warehouse_id']; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/TagController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/TagController.php new file mode 100644 index 0000000..283774c --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/TagController.php @@ -0,0 +1,62 @@ +warehouseRepository->find($id); + + if (! $warehouse->tags->contains(request()->input('tag_id'))) { + $warehouse->tags()->attach(request()->input('tag_id')); + } + + Event::dispatch('warehouse.tag.create.after', $warehouse); + + return response()->json([ + 'message' => trans('admin::app.warehouse.view.tags.create-success'), + ]); + } + + /** + * Remove the specified resource from storage. + * + * @param int $warehouseId + * @return Response + */ + public function detach($warehouseId) + { + Event::dispatch('warehouse.tag.delete.before', $warehouseId); + + $warehouse = $this->warehouseRepository->find($warehouseId); + + $warehouse->tags()->detach(request()->input('tag_id')); + + Event::dispatch('warehouse.tag.delete.after', $warehouse); + + return response()->json([ + 'message' => trans('admin::app.leads.view.tags.destroy-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/WarehouseController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/WarehouseController.php new file mode 100644 index 0000000..6844677 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/Warehouse/WarehouseController.php @@ -0,0 +1,159 @@ +request->add(['entity_type' => 'warehouses']); + } + + /** + * Display a listing of the resource. + */ + public function index(): View|JsonResponse + { + if (request()->ajax()) { + return datagrid(WarehouseDataGrid::class)->process(); + } + + return view('admin::settings.warehouses.index'); + } + + /** + * Search location results + */ + public function search(): JsonResponse + { + $results = $this->warehouseRepository + ->pushCriteria(app(RequestCriteria::class)) + ->all(); + + return response()->json($results); + } + + /** + * Display a listing of the product resource. + */ + public function products(int $id) + { + if (request()->ajax()) { + return datagrid(ProductDataGrid::class)->process(); + } + + $warehouse = $this->warehouseRepository->findOrFail($id); + + return view('admin::settings.warehouses.products', compact('warehouse')); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::settings.warehouses.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(AttributeForm $request): RedirectResponse + { + Event::dispatch('settings.warehouse.create.before'); + + $warehouse = $this->warehouseRepository->create($request->all()); + + Event::dispatch('settings.warehouse.create.after', $warehouse); + + session()->flash('success', trans('admin::app.settings.warehouses.index.create-success')); + + return redirect()->route('admin.settings.warehouses.index'); + } + + /** + * Show the form for viewing the specified resource. + */ + public function view(int $id): View + { + $warehouse = $this->warehouseRepository->findOrFail($id); + + return view('admin::settings.warehouses.view', compact('warehouse')); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return View + */ + public function edit($id) + { + $warehouse = $this->warehouseRepository->findOrFail($id); + + return view('admin::settings.warehouses.edit', compact('warehouse')); + } + + /** + * Update the specified resource in storage. + */ + public function update(AttributeForm $request, int $id): RedirectResponse|JsonResponse + { + Event::dispatch('settings.warehouse.update.before', $id); + + $warehouse = $this->warehouseRepository->update($request->all(), $id); + + Event::dispatch('settings.warehouse.update.after', $warehouse); + + if (request()->ajax()) { + return response()->json([ + 'data' => $warehouse, + 'message' => trans('admin::app.settings.warehouses.index.update-success'), + ]); + } + + session()->flash('success', trans('admin::app.settings.warehouses.index.update-success')); + + return redirect()->route('admin.settings.warehouses.index'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $this->warehouseRepository->findOrFail($id); + + try { + Event::dispatch('settings.warehouse.delete.before', $id); + + $this->warehouseRepository->delete($id); + + Event::dispatch('settings.warehouse.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.settings.warehouses.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.settings.warehouses.index.delete-success'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/WebFormController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/WebFormController.php new file mode 100644 index 0000000..0267242 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/WebFormController.php @@ -0,0 +1,168 @@ +ajax()) { + return datagrid(WebFormDataGrid::class)->process(); + } + + return view('admin::settings.web-forms.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + $tempAttributes = $this->attributeRepository->getWebFormAttributes(); + + $pipelines = $this->pipelineRepository->all(); + + $defaultPipelineId = $this->pipelineRepository->getDefaultPipeline()?->id; + + $attributes = []; + + foreach ($tempAttributes as $attribute) { + if ( + $attribute->entity_type == 'persons' + && in_array($attribute->code, ['name', 'emails', 'contact_numbers']) + ) { + $attributes['default'][] = $attribute; + } else { + $attributes['other'][] = $attribute; + } + } + + return view('admin::settings.web-forms.create', compact('attributes', 'pipelines', 'defaultPipelineId')); + } + + /** + * Store a newly created email templates in storage. + */ + public function store(): RedirectResponse + { + $this->validate(request(), [ + 'title' => 'required', + 'submit_button_label' => 'required', + 'submit_success_action' => 'required', + 'submit_success_content' => 'required', + 'lead_pipeline_id' => 'nullable|integer|exists:lead_pipelines,id', + ]); + + Event::dispatch('settings.web_forms.create.before'); + + $data = request()->all(); + + $webForm = $this->webFormRepository->create($data); + + Event::dispatch('settings.web_forms.create.after', $webForm); + + session()->flash('success', trans('admin::app.settings.webforms.index.create-success')); + + return redirect()->route('admin.settings.web_forms.index'); + } + + /** + * Show the form for editing the specified email template. + */ + public function edit(int $id): View + { + $webForm = $this->webFormRepository->findOrFail($id); + + $attributes = $this->attributeRepository->getWebFormAttributes( + $webForm->attributes()->pluck('attribute_id')->toArray() + ); + + $pipelines = $this->pipelineRepository->all(); + + $defaultPipelineId = $this->pipelineRepository->getDefaultPipeline()?->id; + + return view('admin::settings.web-forms.edit', compact('webForm', 'attributes', 'pipelines', 'defaultPipelineId')); + } + + /** + * Update the specified email template in storage. + */ + public function update(int $id): RedirectResponse + { + $this->validate(request(), [ + 'title' => 'required', + 'submit_button_label' => 'required', + 'submit_success_action' => 'required', + 'submit_success_content' => 'required', + 'lead_pipeline_id' => 'nullable|integer|exists:lead_pipelines,id', + ]); + + Event::dispatch('settings.web_forms.update.before', $id); + + $data = request()->all(); + + $webForm = $this->webFormRepository->update($data, $id); + + Event::dispatch('settings.web_forms.update.after', $webForm); + + session()->flash('success', trans('admin::app.settings.webforms.index.update-success')); + + return redirect()->route('admin.settings.web_forms.index'); + } + + /** + * Remove the specified email template from storage. + */ + public function destroy(int $id): JsonResponse + { + $webForm = $this->webFormRepository->findOrFail($id); + + try { + Event::dispatch('settings.web_forms.delete.before', $id); + + $webForm->delete($id); + + Event::dispatch('settings.web_forms.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.settings.webforms.index.delete-success'), + ]); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.settings.webforms.index.delete-failed'), + ], 400); + } + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/WebhookController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/WebhookController.php new file mode 100644 index 0000000..8806f93 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/WebhookController.php @@ -0,0 +1,97 @@ +ajax()) { + return datagrid(WebhookDataGrid::class)->process(); + } + + return view('admin::settings.webhook.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::settings.webhook.create'); + } + + /** + * Store the newly created resource in storage. + */ + public function store(WebhookRequest $webhookRequest): RedirectResponse + { + Event::dispatch('settings.webhook.create.before'); + + $webhook = $this->webhookRepository->create($webhookRequest->validated()); + + Event::dispatch('settings.webhook.create.after', $webhook); + + session()->flash('success', trans('admin::app.settings.webhooks.index.create-success')); + + return redirect()->route('admin.settings.webhooks.index'); + } + + /** + * Store the newly created resource in storage. + */ + public function edit(int $id): View + { + $webhook = $this->webhookRepository->findOrFail($id); + + return view('admin::settings.webhook.edit', compact('webhook')); + } + + /** + * Update the specified resource in storage. + */ + public function update(WebhookRequest $webhookRequest, int $id): RedirectResponse + { + Event::dispatch('settings.webhook.update.before', $id); + + $webhook = $this->webhookRepository->update($webhookRequest->validated(), $id); + + Event::dispatch('settings.webhook.update.after', $webhook); + + session()->flash('success', trans('admin::app.settings.webhooks.index.update-success')); + + return redirect()->route('admin.settings.webhooks.index'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + $webhook = $this->webhookRepository->findOrFail($id); + + Event::dispatch('settings.webhook.delete.before', $id); + + $webhook?->delete(); + + Event::dispatch('settings.webhook.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.settings.webhooks.index.delete-success'), + ]); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/Settings/WorkflowController.php b/packages/Webkul/Admin/src/Http/Controllers/Settings/WorkflowController.php new file mode 100755 index 0000000..4a64beb --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/Settings/WorkflowController.php @@ -0,0 +1,119 @@ +ajax()) { + return datagrid(WorkflowDataGrid::class)->process(); + } + + return view('admin::settings.workflows.index'); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + return view('admin::settings.workflows.create'); + } + + /** + * Store a newly created workflow in storage. + */ + public function store(): RedirectResponse + { + $this->validate(request(), [ + 'name' => 'required', + ]); + + Event::dispatch('settings.workflow.create.before'); + + $workflow = $this->workflowRepository->create(request()->all()); + + Event::dispatch('settings.workflow.create.after', $workflow); + + session()->flash('success', trans('admin::app.settings.workflows.index.create-success')); + + return redirect()->route('admin.settings.workflows.index'); + } + + /** + * Show the form for editing the specified workflow. + */ + public function edit(int $id): View + { + $workflow = $this->workflowRepository->findOrFail($id); + + return view('admin::settings.workflows.edit', compact('workflow')); + } + + /** + * Update the specified workflow in storage. + */ + public function update(int $id): RedirectResponse + { + $this->validate(request(), [ + 'name' => 'required', + ]); + + Event::dispatch('settings.workflow.update.before', $id); + + $workflow = $this->workflowRepository->update(request()->all(), $id); + + Event::dispatch('settings.workflow.update.after', $workflow); + + session()->flash('success', trans('admin::app.settings.workflows.index.update-success')); + + return redirect()->route('admin.settings.workflows.index'); + } + + /** + * Remove the specified workflow from storage. + */ + public function destroy(int $id): JsonResponse + { + $workflow = $this->workflowRepository->findOrFail($id); + + try { + Event::dispatch('settings.workflow.delete.before', $id); + + $workflow->delete($id); + + Event::dispatch('settings.workflow.delete.after', $id); + + return response()->json([ + 'message' => trans('admin::app.settings.workflows.index.delete-success'), + ], 200); + } catch (\Exception $exception) { + return response()->json([ + 'message' => trans('admin::app.settings.workflows.index.delete-failed'), + ], 400); + } + + return response()->json([ + 'message' => trans('admin::app.settings.workflows.index.delete-failed'), + ], 400); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/TinyMCEController.php b/packages/Webkul/Admin/src/Http/Controllers/TinyMCEController.php new file mode 100644 index 0000000..7bb3555 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/TinyMCEController.php @@ -0,0 +1,77 @@ +validate(request(), [ + 'file' => 'required|file|mimes:'.$this->allowedExtensions.'|max:8192', + ]); + + $media = $this->storeMedia(); + + if (! empty($media)) { + return response()->json([ + 'location' => $media['file_url'], + ]); + } + + return response()->json([]); + } + + /** + * Store media. + */ + public function storeMedia(): array + { + if (! request()->hasFile('file')) { + return []; + } + + $file = request()->file('file'); + + if (! $file instanceof UploadedFile) { + return []; + } + + $extension = $file->extension(); + + if (! in_array($extension, explode(',', $this->allowedExtensions), true)) { + return []; + } + + $filename = md5($file->getClientOriginalName().time()).'.'.$extension; + + $path = $file->storeAs($this->storagePath, $filename); + + $this->sanitizeSVG($path, $file); + + return [ + 'file' => $path, + 'file_name' => $file->getClientOriginalName(), + 'file_url' => Storage::url($path), + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/User/AccountController.php b/packages/Webkul/Admin/src/Http/Controllers/User/AccountController.php new file mode 100755 index 0000000..f0554c3 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/User/AccountController.php @@ -0,0 +1,98 @@ +guard('user')->user(); + + return view('admin::user.account.edit', compact('user')); + } + + /** + * Update the specified resource in storage. + * + * @return Response + */ + public function update() + { + $user = auth()->guard('user')->user(); + + $this->validate(request(), [ + 'name' => 'required', + 'email' => 'email|unique:users,email,'.$user->id, + 'password' => 'nullable|min:6|confirmed', + 'current_password' => 'required|min:6', + 'image.*' => 'nullable|mimes:bmp,jpeg,jpg,png,webp', + ]); + + $data = request()->only([ + 'name', + 'email', + 'password', + 'password_confirmation', + 'current_password', + 'image', + ]); + + if (! Hash::check($data['current_password'], $user->password)) { + session()->flash('warning', trans('admin::app.account.edit.invalid-password')); + + return redirect()->back(); + } + + if (isset($data['role_id']) || isset($data['view_permission'])) { + session()->flash('warning', trans('admin::app.user.account.permission-denied')); + + return redirect()->back(); + } + + $isPasswordChanged = false; + + if (! $data['password']) { + unset($data['password']); + } else { + $isPasswordChanged = true; + + $data['password'] = bcrypt($data['password']); + } + + if (request()->hasFile('image')) { + $data['image'] = current(request()->file('image'))->store('admins/'.$user->id); + } else { + if (! isset($data['image'])) { + if (! empty($data['image'])) { + Storage::delete($user->image); + } + + $data['image'] = null; + } else { + $data['image'] = $user->image; + } + } + + $user->update($data); + + if ($isPasswordChanged) { + Event::dispatch('user.account.update-password', $user); + } + + session()->flash('success', trans('admin::app.account.edit.update-success')); + + return back(); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/User/ForgotPasswordController.php b/packages/Webkul/Admin/src/Http/Controllers/User/ForgotPasswordController.php new file mode 100755 index 0000000..c40cd08 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/User/ForgotPasswordController.php @@ -0,0 +1,76 @@ +guard('user')->check()) { + return redirect()->route('admin.dashboard.index'); + } else { + if (strpos(url()->previous(), 'user') !== false) { + $intendedUrl = url()->previous(); + } else { + $intendedUrl = route('admin.dashboard.index'); + } + + session()->put('url.intended', $intendedUrl); + + return view('admin::sessions.forgot-password'); + } + } + + /** + * Store a newly created resource in storage. + * + * @return Response + */ + public function store() + { + try { + $this->validate(request(), [ + 'email' => 'required|email', + ]); + + $this->broker()->sendResetLink(request(['email']), function ($user, $token) { + $user->notify(new UserResetPassword($token)); + }); + + /** + * The same answer is returned whether or not the address belongs to an + * account. Reporting "this email does not exist" would let anyone probe + * the form to learn which addresses are registered users. + */ + session()->flash('success', trans('admin::app.users.forget-password.create.reset-link-sent')); + + return back(); + } catch (\Exception $exception) { + session()->flash('error', trans($exception->getMessage())); + + return redirect()->back(); + } + } + + /** + * Get the broker to be used during password reset. + * + * @return PasswordBroker + */ + public function broker() + { + return Password::broker('users'); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/User/ResetPasswordController.php b/packages/Webkul/Admin/src/Http/Controllers/User/ResetPasswordController.php new file mode 100755 index 0000000..bfa395d --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/User/ResetPasswordController.php @@ -0,0 +1,110 @@ +with([ + 'token' => $token, + 'email' => request('email'), + ]); + } + + /** + * Store a newly created resource in storage. + * + * @return Response + */ + public function store() + { + try { + $this->validate(request(), [ + 'token' => 'required', + 'email' => 'required|email', + 'password' => 'required|confirmed|min:6', + ]); + + $response = $this->broker()->reset( + request(['email', 'password', 'password_confirmation', 'token']), function ($admin, $password) { + $this->resetPassword($admin, $password); + } + ); + + if ($response == Password::PASSWORD_RESET) { + return redirect()->route('admin.dashboard.index'); + } + + return back() + ->withInput(request(['email'])) + ->withErrors([ + 'email' => trans($response), + ]); + } catch (\Exception $exception) { + session()->flash('error', trans($exception->getMessage())); + + return redirect()->back(); + } + } + + /** + * Reset the given admin's password. + * + * @param CanResetPassword $admin + * @param string $password + * @return void + */ + protected function resetPassword($admin, $password) + { + $admin->password = Hash::make($password); + + $admin->setRememberToken(Str::random(60)); + + $admin->save(); + + event(new PasswordReset($admin)); + + /** + * A deactivated account is not signed in here. Login enforces the same + * check, so resetting a password must not become the way around it. + */ + if (! (bool) $admin->status) { + return; + } + + auth()->guard('user')->login($admin); + } + + /** + * Get the broker to be used during password reset. + * + * @return PasswordBroker + */ + public function broker() + { + return Password::broker('users'); + } +} diff --git a/packages/Webkul/Admin/src/Http/Controllers/User/SessionController.php b/packages/Webkul/Admin/src/Http/Controllers/User/SessionController.php new file mode 100755 index 0000000..9f43e80 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Controllers/User/SessionController.php @@ -0,0 +1,187 @@ +guard('user')->check()) { + return redirect()->route('admin.dashboard.index'); + } + + $previousUrl = url()->previous(); + + $intendedUrl = str_contains($previousUrl, 'admin') + ? $previousUrl + : route('admin.dashboard.index'); + + session()->put('url.intended', $intendedUrl); + + return view('admin::sessions.login'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(): RedirectResponse + { + $this->validate(request(), [ + 'email' => 'required|email', + 'password' => 'required', + ]); + + $this->ensureIsNotRateLimited(); + + if (! auth()->guard('user')->attempt(request(['email', 'password']), request('remember'))) { + $this->recordFailedAttempt(); + + session()->flash('error', trans('admin::app.users.login-error')); + + return redirect()->back(); + } + + $this->clearRateLimit(); + + if (auth()->guard('user')->user()->status == 0) { + session()->flash('warning', trans('admin::app.users.activate-warning')); + + auth()->guard('user')->logout(); + + return redirect()->route('admin.session.create'); + } + + $menus = menu()->getItems('admin'); + + $availableNextMenu = $menus?->first(); + + if (! bouncer()->hasPermission('dashboard')) { + if (is_null($availableNextMenu)) { + session()->flash('error', trans('admin::app.users.not-permission')); + + auth()->guard('user')->logout(); + + return redirect()->route('admin.session.create'); + } + + return redirect()->to($availableNextMenu->getUrl()); + } + + $hasAccessToIntendedUrl = $this->canAccessIntendedUrl($menus, redirect()->getIntendedUrl()); + + if ($hasAccessToIntendedUrl) { + return redirect()->intended(route('admin.dashboard.index')); + } + + return redirect()->to($availableNextMenu->getUrl()); + } + + /** + * Stop the request when too many recent attempts have already failed. + * + * Only failures are counted. Throttling every attempt would also throttle people who sign in + * correctly — a browser test suite signing in for each of its cases, or several staff behind one + * office address — while doing nothing extra against password guessing, which is made up + * entirely of failures. + */ + protected function ensureIsNotRateLimited(): void + { + if ( + RateLimiter::tooManyAttempts($this->credentialRateLimitKey(), self::MAX_ATTEMPTS_PER_CREDENTIAL) + || RateLimiter::tooManyAttempts($this->addressRateLimitKey(), self::MAX_ATTEMPTS_PER_ADDRESS) + ) { + abort(429); + } + } + + /** + * Count a failed attempt against both the credentials used and the address it came from, so + * neither guessing one password nor spraying many accounts from one place goes unchecked. + */ + protected function recordFailedAttempt(): void + { + RateLimiter::hit($this->credentialRateLimitKey()); + + RateLimiter::hit($this->addressRateLimitKey()); + } + + /** + * A successful sign-in clears the record for those credentials. + */ + protected function clearRateLimit(): void + { + RateLimiter::clear($this->credentialRateLimitKey()); + } + + protected function credentialRateLimitKey(): string + { + return 'admin-login|'.sha1(Str::lower((string) request('email')).'|'.request()->ip()); + } + + protected function addressRateLimitKey(): string + { + return 'admin-login-address|'.sha1((string) request()->ip()); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(): RedirectResponse + { + auth()->guard('user')->logout(); + + /** + * Drop every value the session carried and rotate its identifier, so no + * data survives the logout and the id cannot be replayed afterwards. + */ + session()->invalidate(); + + session()->regenerateToken(); + + return redirect()->route('admin.session.create'); + } + + /** + * Find menu item by URL. + */ + protected function canAccessIntendedUrl(Collection $menus, ?string $url): ?MenuItem + { + if (is_null($url)) { + return null; + } + + foreach ($menus as $menu) { + if ($menu->getUrl() === $url) { + return $menu; + } + + if ($menu->haveChildren()) { + $found = $this->canAccessIntendedUrl($menu->getChildren(), $url); + + if ($found) { + return $found; + } + } + } + + return null; + } +} diff --git a/packages/Webkul/Admin/src/Http/Middleware/Bouncer.php b/packages/Webkul/Admin/src/Http/Middleware/Bouncer.php new file mode 100755 index 0000000..3a558d4 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Middleware/Bouncer.php @@ -0,0 +1,191 @@ +guard($guard)->check()) { + return redirect()->route('admin.session.create'); + } + + /** + * If user status is changed by admin. Then session should be + * logged out. + */ + if (! (bool) auth()->guard($guard)->user()->status) { + $this->terminateSession($guard); + + return redirect()->route('admin.session.create'); + } + + /** + * If somehow the user deleted all permissions, then it should be + * auto logged out and need to contact the administrator again. + */ + if ($this->isPermissionsEmpty()) { + $this->terminateSession($guard); + + return redirect()->route('admin.session.create'); + } + + return $next($request); + } + + /** + * Log the user out and tear the session down, then flash the reason. + * + * The session is invalidated and its token rotated so nothing the revoked + * session held survives; the message is flashed afterwards because + * invalidating would otherwise discard it. + */ + protected function terminateSession($guard): void + { + auth()->guard($guard)->logout(); + + session()->invalidate(); + + session()->regenerateToken(); + + session()->flash('error', trans('admin::app.errors.401')); + } + + /** + * Check for user, if they have empty permissions or not except admin. + * + * @return bool + */ + public function isPermissionsEmpty() + { + if (! $role = auth()->guard('user')->user()->role) { + abort(401, 'This action is unauthorized.'); + } + + if ($role->permission_type === 'all') { + return false; + } + + if ($role->permission_type !== 'all' && empty($role->permissions)) { + return true; + } + + $this->checkIfAuthorized(); + + return false; + } + + /** + * Check authorization. + * + * The middleware fails closed: an administrative route that is not covered by the ACL map is + * denied rather than allowed. A route without its own mapping inherits the permission of the + * nearest mapped ancestor feature (so, for example, an unmapped `admin.leads.search` still + * requires the `leads` permission), while a small allow-list covers authentication, self-service + * account management, and generic UI helpers that expose no privileged data. + * + * @return void + */ + public function checkIfAuthorized() + { + $routeName = Route::currentRouteName(); + + if (! $routeName) { + return; + } + + $roles = acl()->getRoles(); + + /** + * Exact ACL mapping. + */ + if (isset($roles[$routeName])) { + bouncer()->allow($roles[$routeName]); + + return; + } + + /** + * Public and self-service routes that any authenticated user may reach. + */ + if ($this->isAllowlistedRoute($routeName)) { + return; + } + + /** + * Fail closed: authorize against the nearest mapped ancestor feature, otherwise deny. + */ + if (! $this->isAuthorizedByFeature($routeName, $roles)) { + abort(401, 'This action is unauthorized.'); + } + } + + /** + * Routes every authenticated user may reach regardless of their role — authentication, + * self-service account management, and generic UI helpers exposing no privileged data. + */ + protected function isAllowlistedRoute(string $routeName): bool + { + return in_array($routeName, [ + 'admin.session.create', + 'admin.session.store', + 'admin.session.destroy', + 'admin.forgot_password.create', + 'admin.forgot_password.store', + 'admin.reset_password.create', + 'admin.reset_password.store', + 'admin.user.account.edit', + 'admin.user.account.update', + 'admin.datagrid.look_up', + 'admin.datagrid.saved_filters.index', + 'admin.datagrid.saved_filters.store', + 'admin.datagrid.saved_filters.update', + 'admin.datagrid.saved_filters.destroy', + 'admin.tinymce.upload', + 'admin.settings.web_forms.form_js', + 'admin.settings.web_forms.form_store', + 'admin.settings.web_forms.preview', + 'admin.mail.inbound_parse', + ], true); + } + + /** + * Authorize an unmapped route against the permission of its nearest mapped ancestor feature. + * Walks up the dotted route name until a mapped feature is found, then requires the user to hold + * at least one permission within that feature. + */ + protected function isAuthorizedByFeature(string $routeName, $roles): bool + { + $segments = explode('.', $routeName); + + while (count($segments) > 2) { + array_pop($segments); + + $prefix = implode('.', $segments).'.'; + + $featureKeys = collect($roles) + ->filter(fn ($key, $route) => str_starts_with($route, $prefix)) + ->values(); + + if ($featureKeys->isEmpty()) { + continue; + } + + $user = auth()->guard('user')->user(); + + return $featureKeys->contains(fn ($key) => $user->hasPermission($key)); + } + + return false; + } +} diff --git a/packages/Webkul/Admin/src/Http/Middleware/Locale.php b/packages/Webkul/Admin/src/Http/Middleware/Locale.php new file mode 100644 index 0000000..438ddbe --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Middleware/Locale.php @@ -0,0 +1,40 @@ +app = $app; + + $this->request = $request; + } + + /** + * Handle an incoming request. + * + * @param Request $request + * @return mixed + */ + public function handle($request, Closure $next) + { + app()->setLocale( + core()->getConfigData('general.general.locale_settings.locale') + ?: app()->getLocale() + ); + + return $next($request); + } +} diff --git a/packages/Webkul/Admin/src/Http/Middleware/SanitizeUrl.php b/packages/Webkul/Admin/src/Http/Middleware/SanitizeUrl.php new file mode 100644 index 0000000..dc78dae --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Middleware/SanitizeUrl.php @@ -0,0 +1,60 @@ +ajax()) { + return $next($request); + } + + $route = $request->route('route'); + + /** + * This middleware only sanitizes and whitelists the mailbox folder parameter. Routes in the + * mail group that do not carry a `route` parameter — such as the attachment download — are not + * folder listings, so they must pass through untouched; validating them against the folder + * whitelist would wrongly reject them (the attachment download link is a plain, non-ajax href). + */ + if (is_null($route)) { + return $next($request); + } + + $sanitizedRoute = Str::of($route)->ascii()->lower()->replaceMatches('/[^a-z0-9_-]/', '')->__toString(); + + $request->route()->setParameter('route', $sanitizedRoute); + + /** + * Whitelist acceptable route values to prevent unexpected input + */ + $allowedRoutes = [ + SupportedFolderEnum::INBOX->value, + SupportedFolderEnum::IMPORTANT->value, + SupportedFolderEnum::STARRED->value, + SupportedFolderEnum::DRAFT->value, + SupportedFolderEnum::OUTBOX->value, + SupportedFolderEnum::SENT->value, + SupportedFolderEnum::SPAM->value, + SupportedFolderEnum::TRASH->value, + ]; + + if (! in_array($route, $allowedRoutes, true)) { + abort(401, trans('admin::app.mail.invalid-route')); + } + + return $next($request); + } +} diff --git a/packages/Webkul/Admin/src/Http/Requests/AttributeForm.php b/packages/Webkul/Admin/src/Http/Requests/AttributeForm.php new file mode 100644 index 0000000..a6d72d5 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Requests/AttributeForm.php @@ -0,0 +1,120 @@ +attributeRepository->scopeQuery(function ($query) { + $query = $query->whereIn('code', array_keys(request()->all())) + ->where('entity_type', request('entity_type')); + + if (request()->has('quick_add')) { + $query = $query->where('quick_add', 1); + } + + return $query; + })->get(); + + foreach ($attributes as $attribute) { + $validations = []; + + if ($attribute->type == 'boolean') { + continue; + } elseif ($attribute->type == 'address') { + if (! $attribute->is_required) { + continue; + } + + $validations = [ + $attribute->code.'.address' => 'required', + $attribute->code.'.country' => 'required', + $attribute->code.'.state' => 'required', + $attribute->code.'.city' => 'required', + $attribute->code.'.postcode' => 'required', + ]; + } elseif ($attribute->type == 'email') { + $validations = [ + $attribute->code => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable', 'email'], + $attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable', + ]; + } elseif ($attribute->type == 'phone') { + $validations = [ + $attribute->code => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable', + ]; + } else { + $validations[$attribute->code] = [$attribute->is_required ? 'required' : 'nullable']; + + if ($attribute->type == 'text' && $attribute->validation) { + array_push($validations[$attribute->code], + $attribute->validation == 'decimal' + ? new Decimal + : $attribute->validation + ); + } + + if ($attribute->type == 'price') { + array_push($validations[$attribute->code], new Decimal); + } + + if ($attribute->type == 'image' && ! request($attribute->code.'.delete')) { + array_push($validations[$attribute->code], 'mimes:bmp,jpeg,jpg,png,webp'); + } + } + + if ($attribute->is_unique) { + array_push($validations[in_array($attribute->type, ['email', 'phone']) + ? $attribute->code.'.*.value' + : $attribute->code + ], function ($field, $value, $fail) use ($attribute) { + if (! $this->attributeValueRepository->isValueUnique($this->id, $attribute->entity_type, $attribute, request($field))) { + $fail('The value has already been taken.'); + } + }); + } + + $this->rules = array_merge($this->rules, $validations); + } + + return $this->rules; + } +} diff --git a/packages/Webkul/Admin/src/Http/Requests/ConfigurationForm.php b/packages/Webkul/Admin/src/Http/Requests/ConfigurationForm.php new file mode 100644 index 0000000..dd63800 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Requests/ConfigurationForm.php @@ -0,0 +1,77 @@ +input('keys', []) as $item) { + $data = json_decode($item, true); + + foreach ($data['fields'] ?? [] as $field) { + $key = $data['key'].'.'.$field['name']; + + if ($this->has($key.'.delete')) { + continue; + } + + $rules[$key] = $this->resolveValidation($key); + } + } + + /** + * Every uploaded file must also be validated against its trusted rule, even when the client + * omits the field from `keys[]`. Files for keys that are not registered configuration fields + * are rejected outright. + */ + foreach (array_keys(Arr::dot(request()->allFiles())) as $key) { + $configField = core()->getConfigField($key); + + $rules[$key] = $configField['validation'] ?? 'prohibited'; + } + + return $rules; + } + + /** + * Resolve the validation rule for a configuration field from its registered definition. + */ + protected function resolveValidation(string $key): string + { + $configField = core()->getConfigField($key); + + if ($configField === null) { + return $this->hasFile($key) ? 'prohibited' : 'nullable'; + } + + return $configField['validation'] ?? 'nullable'; + } +} diff --git a/packages/Webkul/Admin/src/Http/Requests/LeadForm.php b/packages/Webkul/Admin/src/Http/Requests/LeadForm.php new file mode 100644 index 0000000..e39df78 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Requests/LeadForm.php @@ -0,0 +1,152 @@ + $entityType) { + $attributes = $this->attributeRepository->scopeQuery(function ($query) use ($entityType) { + $attributeCodes = $entityType == 'persons' + ? array_keys(request('person') ?? []) + : array_keys(request()->all()); + + $query = $query->whereIn('code', $attributeCodes) + ->where('entity_type', $entityType); + + if (request()->has('quick_add')) { + $query = $query->where('quick_add', 1); + } + + return $query; + })->get(); + + foreach ($attributes as $attribute) { + if ($entityType == 'persons') { + $attribute->code = 'person.'.$attribute->code; + } + + $validations = []; + + if ($attribute->type == 'boolean') { + continue; + } elseif ($attribute->type == 'address') { + if (! $attribute->is_required) { + continue; + } + + $validations = [ + $attribute->code.'.address' => 'required', + $attribute->code.'.country' => 'required', + $attribute->code.'.state' => 'required', + $attribute->code.'.city' => 'required', + $attribute->code.'.postcode' => 'required', + ]; + } elseif ($attribute->type == 'email') { + $validations = [ + $attribute->code => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable', 'email'], + $attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable', + ]; + } elseif ($attribute->type == 'phone') { + $validations = [ + $attribute->code => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable', + ]; + } else { + $validations[$attribute->code] = [$attribute->is_required ? 'required' : 'nullable']; + + if ($attribute->type == 'text' && $attribute->validation) { + array_push($validations[$attribute->code], + $attribute->validation == 'decimal' + ? new Decimal + : $attribute->validation + ); + } + + if ($attribute->type == 'price') { + array_push($validations[$attribute->code], new Decimal); + } + } + + if ($attribute->is_unique) { + array_push($validations[in_array($attribute->type, ['email', 'phone']) + ? $attribute->code.'.*.value' + : $attribute->code + ], function ($field, $value, $fail) use ($attribute, $entityType) { + if (! $this->attributeValueRepository->isValueUnique( + $entityType == 'persons' ? request('person.id') : $this->id, + $attribute->entity_type, + $attribute, + request($field) + ) + ) { + $fail('The value has already been taken.'); + } + }); + } + + $this->rules = array_merge($this->rules, $validations); + } + } + + return [ + ...$this->rules, + 'products' => 'array', + 'products.*.product_id' => 'sometimes|required|exists:products,id', + 'products.*.name' => 'required_with:products.*.product_id', + 'products.*.price' => 'required_with:products.*.product_id', + 'products.*.quantity' => 'required_with:products.*.product_id', + ]; + } + + /** + * Get the validation messages that apply to the request. + */ + public function messages(): array + { + return [ + 'products.*.product_id.exists' => trans('admin::app.leads.selected-product-not-exist'), + 'products.*.name.required_with' => trans('admin::app.leads.product-name-required'), + 'products.*.price.required_with' => trans('admin::app.leads.product-price-required'), + 'products.*.quantity.required_with' => trans('admin::app.leads.product-quantity-required'), + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Requests/MassDestroyRequest.php b/packages/Webkul/Admin/src/Http/Requests/MassDestroyRequest.php new file mode 100644 index 0000000..86f360c --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Requests/MassDestroyRequest.php @@ -0,0 +1,31 @@ + ['required', 'array'], + 'indices.*' => ['integer'], + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Requests/MassUpdateRequest.php b/packages/Webkul/Admin/src/Http/Requests/MassUpdateRequest.php new file mode 100644 index 0000000..a411e0d --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Requests/MassUpdateRequest.php @@ -0,0 +1,32 @@ + ['required', 'array'], + 'indices.*' => ['integer'], + 'value' => ['required'], + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Requests/PipelineForm.php b/packages/Webkul/Admin/src/Http/Requests/PipelineForm.php new file mode 100755 index 0000000..d575f21 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Requests/PipelineForm.php @@ -0,0 +1,89 @@ +validatorExtensions($validationFactory); + } + + /** + * Determine if the user is authorized to make this request. + * + * @return bool + */ + public function authorize() + { + return true; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + if (request('id')) { + return [ + 'name' => 'required|unique:lead_pipelines,name,'.request('id'), + 'stages.*.name' => 'unique_key', + 'stages.*.code' => 'unique_key', + ]; + } + + return [ + 'name' => 'required|unique:lead_pipelines,name', + 'rotten_days' => 'required', + 'stages.*.name' => 'unique_key', + 'stages.*.code' => 'unique_key', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages() + { + return [ + 'stages.*.name.unique_key' => trans('admin::app.settings.pipelines.duplicate-name'), + ]; + } + + /** + * Place all your validator extensions here. + * + * @return void + */ + public function validatorExtensions(ValidationFactory $validationFactory) + { + $validationFactory->extend( + 'unique_key', + function ($attribute, $value, $parameters) { + $key = last(explode('.', $attribute)); + + $stages = collect(request()->get('stages'))->filter(function ($stage) use ($key, $value) { + return $stage[$key] === $value; + }); + + if ($stages->count() > 1) { + return false; + } + + return true; + } + ); + } +} diff --git a/packages/Webkul/Admin/src/Http/Requests/UserForm.php b/packages/Webkul/Admin/src/Http/Requests/UserForm.php new file mode 100755 index 0000000..f65485f --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Requests/UserForm.php @@ -0,0 +1,43 @@ +rules = [ + 'name' => 'required', + 'email' => 'email|unique:users,email', + 'password' => 'nullable', + 'password_confirmation' => 'nullable|required_with:password|same:password', + 'status' => 'sometimes', + 'role_id' => 'required', + ]; + + if ($this->method() == 'PUT') { + $this->rules['email'] = 'email|unique:users,email,'.$this->route('id'); + } + + return $this->rules; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/ActivityFileResource.php b/packages/Webkul/Admin/src/Http/Resources/ActivityFileResource.php new file mode 100644 index 0000000..ee14fb4 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/ActivityFileResource.php @@ -0,0 +1,27 @@ + $this->id, + 'name' => $this->name, + 'path' => $this->path, + 'url' => $this->url, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/ActivityParticipantResource.php b/packages/Webkul/Admin/src/Http/Resources/ActivityParticipantResource.php new file mode 100644 index 0000000..065d582 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/ActivityParticipantResource.php @@ -0,0 +1,23 @@ + $this->when($this->user, new UserResource($this->user)), + 'person' => $this->when($this->person, new PersonResource($this->person)), + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/ActivityResource.php b/packages/Webkul/Admin/src/Http/Resources/ActivityResource.php new file mode 100644 index 0000000..38ea785 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/ActivityResource.php @@ -0,0 +1,36 @@ + $this->id, + 'parent_id' => $this->parent_id ?? null, + 'title' => $this->title, + 'type' => $this->type, + 'comment' => $this->comment, + 'additional' => is_array($this->resource->additional) ? $this->resource->additional : json_decode($this->resource->additional, true), + 'schedule_from' => $this->schedule_from, + 'schedule_to' => $this->schedule_to, + 'is_done' => $this->is_done, + 'user' => new UserResource($this->user), + 'files' => ActivityFileResource::collection($this->files), + 'participants' => ActivityParticipantResource::collection($this->participants), + 'location' => $this->location, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/EmailAttachmentResource.php b/packages/Webkul/Admin/src/Http/Resources/EmailAttachmentResource.php new file mode 100644 index 0000000..e38cd30 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/EmailAttachmentResource.php @@ -0,0 +1,30 @@ + $this->id, + 'name' => $this->name, + 'path' => $this->path, + 'url' => $this->url, + 'size' => $this->size, + 'content_type' => $this->content_type, + 'content_id' => $this->content_id, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/EmailResource.php b/packages/Webkul/Admin/src/Http/Resources/EmailResource.php new file mode 100644 index 0000000..dc1cdda --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/EmailResource.php @@ -0,0 +1,44 @@ + $this->id, + 'subject' => $this->subject, + 'source' => $this->source, + 'user_type' => $this->user_type, + 'name' => $this->name, + 'reply' => $this->reply, + 'is_read' => $this->is_read, + 'folders' => $this->folders, + 'from' => $this->from, + 'sender' => $this->sender, + 'reply_to' => $this->reply_to, + 'cc' => $this->cc, + 'bcc' => $this->bcc, + 'unique_id' => $this->unique_id, + 'message_id' => $this->message_id, + 'reference_ids' => $this->reference_ids, + 'person' => new PersonResource($this->person), + 'lead' => new LeadResource($this->lead), + 'parent_id' => $this->parent_id, + 'parent' => new EmailResource($this->parent), + 'attachments' => EmailAttachmentResource::collection($this->attachments), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/LeadResource.php b/packages/Webkul/Admin/src/Http/Resources/LeadResource.php new file mode 100644 index 0000000..81fe04c --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/LeadResource.php @@ -0,0 +1,38 @@ + $this->id, + 'title' => $this->title, + 'lead_value' => $this->lead_value, + 'formatted_lead_value' => core()->formatBasePrice($this->lead_value), + 'status' => $this->status, + 'expected_close_date' => $this->expected_close_date, + 'rotten_days' => $this->rotten_days, + 'closed_at' => $this->closed_at, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'person' => $this->person ? new PersonResource($this->person) : null, + 'user' => $this->user ? new UserResource($this->user) : null, + 'type' => $this->type ? new TypeResource($this->type) : null, + 'source' => $this->source ? new SourceResource($this->source) : null, + 'pipeline' => $this->pipeline ? new PipelineResource($this->pipeline) : null, + 'stage' => $this->stage ? new StageResource($this->stage) : null, + 'tags' => TagResource::collection($this->tags), + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/OrganizationResource.php b/packages/Webkul/Admin/src/Http/Resources/OrganizationResource.php new file mode 100644 index 0000000..a8a3f97 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/OrganizationResource.php @@ -0,0 +1,26 @@ + $this->id, + 'name' => $this->name, + 'address' => $this->address, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/PersonResource.php b/packages/Webkul/Admin/src/Http/Resources/PersonResource.php new file mode 100644 index 0000000..5989b43 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/PersonResource.php @@ -0,0 +1,28 @@ + $this->id, + 'name' => $this->name, + 'emails' => $this->emails, + 'contact_numbers' => $this->contact_numbers, + 'organization' => $this->organization ? new OrganizationResource($this->organization) : null, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/PipelineResource.php b/packages/Webkul/Admin/src/Http/Resources/PipelineResource.php new file mode 100644 index 0000000..646fb2f --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/PipelineResource.php @@ -0,0 +1,28 @@ + $this->id, + 'name' => $this->name, + 'is_default' => $this->is_default, + 'rotten_days' => $this->rotten_days, + 'stages' => StageResource::collection($this->stages), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/ProductResource.php b/packages/Webkul/Admin/src/Http/Resources/ProductResource.php new file mode 100644 index 0000000..5da099a --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/ProductResource.php @@ -0,0 +1,28 @@ + $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'sku' => $this->sku, + 'price' => $this->price, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/QuoteResource.php b/packages/Webkul/Admin/src/Http/Resources/QuoteResource.php new file mode 100644 index 0000000..5ca63aa --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/QuoteResource.php @@ -0,0 +1,33 @@ + $this->id, + 'subject' => $this->subject, + 'description' => $this->description, + 'billing_address' => $this->billing_address, + 'shipping_address' => $this->shipping_address, + 'discount_percent' => $this->discount_percent, + 'discount_amount' => $this->discount_amount, + 'tax_amount' => $this->tax_amount, + 'adjustment_amount' => $this->adjustment_amount, + 'sub_total' => $this->sub_total, + 'grand_total' => $this->grand_total, + 'expired_at' => $this->expired_at, + 'user' => new UserResource($this->user), + 'person' => new PersonResource($this->person), + 'leads' => LeadResource::collection($this->leads), + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/SourceResource.php b/packages/Webkul/Admin/src/Http/Resources/SourceResource.php new file mode 100644 index 0000000..13f8421 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/SourceResource.php @@ -0,0 +1,25 @@ + $this->id, + 'name' => $this->name, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/StageResource.php b/packages/Webkul/Admin/src/Http/Resources/StageResource.php new file mode 100644 index 0000000..83c58b6 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/StageResource.php @@ -0,0 +1,31 @@ + $this->id, + 'code' => $this->code, + 'name' => $this->name, + 'lead_value' => $this->lead_value, + 'formatted_lead_value' => core()->formatBasePrice($this->lead_value), + 'is_user_defined' => $this->is_user_defined, + 'sort_order' => $this->sort_order, + 'lead_pipeline_id' => $this->lead_pipeline_id, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/TagResource.php b/packages/Webkul/Admin/src/Http/Resources/TagResource.php new file mode 100644 index 0000000..5d845f0 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/TagResource.php @@ -0,0 +1,27 @@ + $this->id, + 'name' => $this->name, + 'color' => $this->color, + 'user' => new UserResource($this->user), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/TypeResource.php b/packages/Webkul/Admin/src/Http/Resources/TypeResource.php new file mode 100644 index 0000000..fb62f97 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/TypeResource.php @@ -0,0 +1,25 @@ + $this->id, + 'name' => $this->name, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/Resources/UserResource.php b/packages/Webkul/Admin/src/Http/Resources/UserResource.php new file mode 100644 index 0000000..a79f593 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/Resources/UserResource.php @@ -0,0 +1,27 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'image' => $this->image, + 'created_at' => $this->created_at, + 'updated_at' => $this->name, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Http/helpers.php b/packages/Webkul/Admin/src/Http/helpers.php new file mode 100755 index 0000000..019e620 --- /dev/null +++ b/packages/Webkul/Admin/src/Http/helpers.php @@ -0,0 +1,8 @@ +make('bouncer'); + } +} diff --git a/packages/Webkul/Admin/src/Listeners/Activity.php b/packages/Webkul/Admin/src/Listeners/Activity.php new file mode 100644 index 0000000..d2ba5ed --- /dev/null +++ b/packages/Webkul/Admin/src/Listeners/Activity.php @@ -0,0 +1,56 @@ +input('lead_id')) { + $lead = $this->leadRepository->find(request()->input('lead_id')); + + if (! $lead->activities->contains($activity->id)) { + $lead->activities()->attach($activity->id); + } + } elseif (request()->input('person_id')) { + $person = $this->personRepository->find(request()->input('person_id')); + + if (! $person->activities->contains($activity->id)) { + $person->activities()->attach($activity->id); + } + } elseif (request()->input('warehouse_id')) { + $warehouse = $this->warehouseRepository->find(request()->input('warehouse_id')); + + if (! $warehouse->activities->contains($activity->id)) { + $warehouse->activities()->attach($activity->id); + } + } elseif (request()->input('product_id')) { + $product = $this->productRepository->find(request()->input('product_id')); + + if (! $product->activities->contains($activity->id)) { + $product->activities()->attach($activity->id); + } + } + } +} diff --git a/packages/Webkul/Admin/src/Listeners/Lead.php b/packages/Webkul/Admin/src/Listeners/Lead.php new file mode 100644 index 0000000..20a40fa --- /dev/null +++ b/packages/Webkul/Admin/src/Listeners/Lead.php @@ -0,0 +1,30 @@ +emailRepository->update([ + 'lead_id' => $lead->id, + ], request('email_id')); + } +} diff --git a/packages/Webkul/Admin/src/Listeners/Person.php b/packages/Webkul/Admin/src/Listeners/Person.php new file mode 100644 index 0000000..b8c4378 --- /dev/null +++ b/packages/Webkul/Admin/src/Listeners/Person.php @@ -0,0 +1,30 @@ +emailRepository->update([ + 'person_id' => $person->id, + ], request('email_id')); + } +} diff --git a/packages/Webkul/Admin/src/Notifications/Common.php b/packages/Webkul/Admin/src/Notifications/Common.php new file mode 100644 index 0000000..ae2ff2a --- /dev/null +++ b/packages/Webkul/Admin/src/Notifications/Common.php @@ -0,0 +1,38 @@ +to($this->data['to']) + ->subject($this->data['subject']) + ->view('admin::emails.common.index', [ + 'body' => $this->data['body'], + ]); + + if (isset($this->data['attachments'])) { + foreach ($this->data['attachments'] as $attachment) { + $message->attachData($attachment['content'], $attachment['name'], [ + 'mime' => $attachment['mime'], + ]); + } + } + + return $message; + } +} diff --git a/packages/Webkul/Admin/src/Notifications/User/Create.php b/packages/Webkul/Admin/src/Notifications/User/Create.php new file mode 100644 index 0000000..9f44ed2 --- /dev/null +++ b/packages/Webkul/Admin/src/Notifications/User/Create.php @@ -0,0 +1,27 @@ +to($this->user->email) + ->subject(trans('admin::app.emails.common.user.create-subject')) + ->view('admin::emails.users.create', [ + 'user_name' => $this->user->name, + ]); + } +} diff --git a/packages/Webkul/Admin/src/Notifications/User/UserResetPassword.php b/packages/Webkul/Admin/src/Notifications/User/UserResetPassword.php new file mode 100755 index 0000000..4ddc284 --- /dev/null +++ b/packages/Webkul/Admin/src/Notifications/User/UserResetPassword.php @@ -0,0 +1,28 @@ +token); + } + + return (new MailMessage) + ->view('admin::emails.users.forget-password', [ + 'user_name' => $notifiable->name, + 'token' => $this->token, + ]); + } +} diff --git a/packages/Webkul/Admin/src/Notifications/User/UserUpdatePassword.php b/packages/Webkul/Admin/src/Notifications/User/UserUpdatePassword.php new file mode 100644 index 0000000..b54b6be --- /dev/null +++ b/packages/Webkul/Admin/src/Notifications/User/UserUpdatePassword.php @@ -0,0 +1,34 @@ +from(core()->getSenderEmailDetails()['email'], core()->getSenderEmailDetails()['name']) + ->to($this->user->email, $this->user->name) + ->subject(trans('shop::app.mail.update-password.subject')) + ->view('shop::emails.users.update-password', ['user' => $this->user]); + } +} diff --git a/packages/Webkul/Admin/src/Providers/AdminServiceProvider.php b/packages/Webkul/Admin/src/Providers/AdminServiceProvider.php new file mode 100644 index 0000000..6fb1c37 --- /dev/null +++ b/packages/Webkul/Admin/src/Providers/AdminServiceProvider.php @@ -0,0 +1,111 @@ +aliasMiddleware('user', BouncerMiddleware::class); + + $router->aliasMiddleware('admin_locale', Locale::class); + + $router->aliasMiddleware('sanitize_url', SanitizeUrl::class); + + include __DIR__.'/../Http/helpers.php'; + + Route::middleware(['web', 'admin_locale', 'user']) + ->prefix(config('app.admin_path')) + ->group(__DIR__.'/../Routes/Admin/web.php'); + + Route::middleware(['web', 'admin_locale']) + ->group(__DIR__.'/../Routes/Front/web.php'); + + $this->loadMigrationsFrom(__DIR__.'/../Database/Migrations'); + + $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'admin'); + + $this->loadViewsFrom(__DIR__.'/../Resources/views', 'admin'); + + Blade::anonymousComponentPath(__DIR__.'/../Resources/views/components', 'admin'); + + $this->app->bind(ExceptionHandler::class, Handler::class); + + Relation::morphMap([ + 'leads' => Lead::class, + 'organizations' => Organization::class, + 'persons' => Person::class, + 'products' => Product::class, + 'quotes' => Quote::class, + 'warehouses' => Warehouse::class, + ]); + + $this->app->register(EventServiceProvider::class); + } + + /** + * Register services. + * + * @return void + */ + public function register() + { + $this->registerFacades(); + + $this->registerConfig(); + } + + /** + * Register Bouncer as a singleton. + */ + protected function registerFacades(): void + { + $loader = AliasLoader::getInstance(); + + $loader->alias('Bouncer', \Webkul\Admin\Facades\Bouncer::class); + + $this->app->singleton('bouncer', function () { + return new Bouncer; + }); + } + + /** + * Register package config. + */ + protected function registerConfig(): void + { + $this->mergeConfigFrom(dirname(__DIR__).'/Config/acl.php', 'acl'); + + $this->mergeConfigFrom(dirname(__DIR__).'/Config/menu.php', 'menu.admin'); + + $this->mergeConfigFrom(dirname(__DIR__).'/Config/core_config.php', 'core_config'); + + $this->mergeConfigFrom(dirname(__DIR__).'/Config/attribute_lookups.php', 'attribute_lookups'); + + $this->mergeConfigFrom(dirname(__DIR__).'/Config/attribute_entity_types.php', 'attribute_entity_types'); + + $this->mergeConfigFrom(dirname(__DIR__).'/Config/support.php', 'support'); + } +} diff --git a/packages/Webkul/Admin/src/Providers/EventServiceProvider.php b/packages/Webkul/Admin/src/Providers/EventServiceProvider.php new file mode 100644 index 0000000..d5410b5 --- /dev/null +++ b/packages/Webkul/Admin/src/Providers/EventServiceProvider.php @@ -0,0 +1,31 @@ + [ + 'Webkul\Admin\Listeners\Person@linkToEmail', + ], + + 'lead.create.after' => [ + 'Webkul\Admin\Listeners\Lead@linkToEmail', + ], + + 'activity.create.after' => [ + 'Webkul\Admin\Listeners\Activity@afterUpdateOrCreate', + ], + + 'activity.update.after' => [ + 'Webkul\Admin\Listeners\Activity@afterUpdateOrCreate', + ], + ]; +} diff --git a/packages/Webkul/Admin/src/Providers/ModuleServiceProvider.php b/packages/Webkul/Admin/src/Providers/ModuleServiceProvider.php new file mode 100644 index 0000000..b9d2e2f --- /dev/null +++ b/packages/Webkul/Admin/src/Providers/ModuleServiceProvider.php @@ -0,0 +1,10 @@ +|string> + */ + public function rules() + { + return [ + 'name' => 'required|string|max:255', + 'entity_type' => 'required|string|max:255', + 'description' => 'nullable|string|max:255', + 'method' => 'required|string|max:255', + 'end_point' => [ + 'required', + 'string', + 'max:255', + fn ($attribute, $value, $fail) => $this->validateEndpoint($value, $fail), + ], + 'query_params' => 'nullable', + 'headers' => 'nullable', + 'payload_type' => [ + 'required', + 'string', + 'max:255', + Rule::in(['default', 'x-www-form-urlencoded', 'raw']), + ], + 'raw_payload_type' => [ + 'string', + 'max:255', + Rule::in(['json', 'text']), + ], + 'payload' => 'nullable', + ]; + } + + /** + * Reject webhook endpoints that could be used for Server-Side Request Forgery. Only plain + * HTTP(S) URLs whose host resolves exclusively to public addresses are accepted. Endpoints that + * contain workflow placeholders (e.g. `{%leads.id%}`) are only resolved when the webhook fires, + * so their final host is validated by the webhook service at trigger time instead. + */ + protected function validateEndpoint(string $value, callable $fail): void + { + if (str_contains($value, '{%')) { + return; + } + + $scheme = strtolower((string) parse_url($value, PHP_URL_SCHEME)); + + $host = parse_url($value, PHP_URL_HOST); + + if ( + ! in_array($scheme, ['http', 'https']) + || empty($host) + ) { + $fail(trans('admin::app.settings.webhooks.index.invalid-endpoint')); + + return; + } + + $host = trim($host, '[]'); + + if (filter_var($host, FILTER_VALIDATE_IP)) { + $ips = [$host]; + } else { + $records = array_merge( + @dns_get_record($host, DNS_A) ?: [], + @dns_get_record($host, DNS_AAAA) ?: [], + ); + + $ips = array_values(array_filter(array_map( + fn ($record) => $record['ip'] ?? $record['ipv6'] ?? null, + $records, + ))); + } + + if (empty($ips)) { + $fail(trans('admin::app.settings.webhooks.index.invalid-endpoint')); + + return; + } + + foreach ($ips as $ip) { + if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + $fail(trans('admin::app.settings.webhooks.index.invalid-endpoint')); + + return; + } + } + } +} diff --git a/packages/Webkul/Admin/src/Resources/assets/css/app.css b/packages/Webkul/Admin/src/Resources/assets/css/app.css new file mode 100644 index 0000000..96b5991 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/css/app.css @@ -0,0 +1,675 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* -------------------------------- new css -------------------------------- */ + +@font-face { + font-family: 'icomoon'; + src: url('../fonts/icomoon.woff?w2trdd') format('woff'); + font-weight: normal; + font-style: normal; + font-display: block; + } + + [class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'icomoon' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + /* Help menu glyph rendered as an SVG mask (no icomoon glyph available). */ + .icon-help { + display: inline-block; + width: 1em; + height: 1em; + background-color: currentColor; + -webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iIzAwMCI+PHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNOS44NzkgNy41MTljMS4xNzEtMS4wMjUgMy4wNzEtMS4wMjUgNC4yNDIgMCAxLjE3MiAxLjAyNSAxLjE3MiAyLjY4NyAwIDMuNzEyLS4yMDMuMTc5LS40My4zMjYtLjY3LjQ0Mi0uNzQ1LjM2MS0xLjQ1Ljk5OS0xLjQ1IDEuODI3di43NU0yMSAxMmE5IDkgMCAxIDEtMTggMCA5IDkgMCAwIDEgMTggMFptLTkgNS4yNWguMDA4di4wMDhIMTJ2LS4wMDhaIi8+PC9zdmc+") no-repeat center / contain; + mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iIzAwMCI+PHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNOS44NzkgNy41MTljMS4xNzEtMS4wMjUgMy4wNzEtMS4wMjUgNC4yNDIgMCAxLjE3MiAxLjAyNSAxLjE3MiAyLjY4NyAwIDMuNzEyLS4yMDMuMTc5LS40My4zMjYtLjY3LjQ0Mi0uNzQ1LjM2MS0xLjQ1Ljk5OS0xLjQ1IDEuODI3di43NU0yMSAxMmE5IDkgMCAxIDEtMTggMCA5IDkgMCAwIDEgMTggMFptLTkgNS4yNWguMDA4di4wMDhIMTJ2LS4wMDhaIi8+PC9zdmc+") no-repeat center / contain; + } + +@layer components { + .icon-image:before { + content: "\e956"; + } + + .icon-bookmark:before { + content: "\e92c"; + } + + .icon-bookmark-active:before { + content: "\e92d"; + } + + .icon-stats-down:before { + content: "\e94d"; + } + + .icon-error:before { + content: "\e951"; + } + + .icon-info:before { + content: "\e952"; + } + + .icon-success:before { + content: "\e953"; + } + + .icon-warning:before { + content: "\e954"; + } + + .icon-stats-down:before { + content: "\e94d"; + } + + .icon-stats-up:before { + content: "\e94f"; + } + + .icon-attribute:before { + content: "\e947"; + } + + .icon-download:before { + content: "\e948"; + } + + .icon-settings-warehouse:before { + content: "\e94e"; + } + + .icon-move:before { + content: "\e949"; + } + + .icon-organization:before { + content: "\e94a"; + } + + .icon-role:before { + content: "\e94b"; + } + + .icon-user:before { + content: "\e94c"; + } + + .icon-system-generate:before { + content: "\e950"; + } + + .icon-settings-attributes:before { + content: "\e93b"; + } + + .icon-settings-flow:before { + content: "\e93c"; + } + + .icon-settings-group:before { + content: "\e93d"; + } + + .icon-settings-mail:before { + content: "\e93e"; + } + + .icon-settings-pipeline:before { + content: "\e93f"; + } + + .icon-settings-roles:before { + content: "\e940"; + } + + .icon-settings-sources:before { + content: "\e941"; + } + + .icon-settings-tag:before { + content: "\e942"; + } + + .icon-settings-type:before { + content: "\e943"; + } + + .icon-settings-user:before { + content: "\e944"; + } + + .icon-settings-webforms:before { + content: "\e945"; + } + + .icon-settings-webhooks:before { + content: "\e946"; + } + + .icon-attached-file:before { + content: "\e932"; + } + + .icon-forward:before { + content: "\e933"; + } + + .icon-location:before { + content: "\e934"; + } + + .icon-pin:before { + content: "\e935"; + } + + .icon-print:before { + content: "\e936"; + } + + .icon-reply-all:before { + content: "\e937"; + } + + .icon-reply:before { + content: "\e938"; + } + + .icon-rotten:before { + content: "\e939"; + } + + .icon-tag:before { + content: "\e93a"; + } + + .icon-list:before { + content: "\e92e"; + } + + .icon-enter:before { + content: "\e92f"; + } + + .icon-kanban:before { + content: "\e930"; + } + + .icon-tick:before { + content: "\e931"; + } + + .icon-eye-hide:before { + content: "\e929"; + } + + .icon-percentage:before { + content: "\e92a"; + } + + .icon-dollar:before { + content: "\e92b"; + } + + .icon-radio-selected:before { + content: "\e924"; + } + + .icon-radio-normal:before { + content: "\e925"; + } + + .icon-folder:before { + content: "\e926"; + } + + .icon-file:before { + content: "\e927"; + } + + .icon-eye:before { + content: "\e928"; + } + + .icon-notification:before { + content: "\e900"; + } + + .icon-configuration:before { + content: "\e901"; + } + + .icon-note:before { + content: "\e902"; + } + + .icon-edit:before { + content: "\e903"; + } + + .icon-menu:before { + content: "\e955"; + } + + .icon-calendar:before { + content: "\e904"; + } + + .icon-delete:before { + content: "\e905"; + } + + .icon-more:before { + content: "\e906"; + } + + .icon-checkbox-multiple:before { + content: "\e907"; + } + + .icon-checkbox-select:before { + content: "\e908"; + } + + .icon-checkbox-outline:before { + content: "\e909"; + } + + .icon-message:before { + content: "\e90a"; + } + + .icon-video:before { + content: "\e90b"; + } + + .icon-attachment:before { + content: "\e90c"; + } + + .icon-sent:before { + content: "\e90d"; + } + + .icon-call:before { + content: "\e90e"; + } + + .icon-meeting:before { + content: "\e90f"; + } + + .icon-light:before { + content: "\e910"; + } + + .icon-dark:before { + content: "\e911"; + } + + .icon-mail:before { + content: "\e912"; + } + + .icon-leads:before { + content: "\e913"; + } + + .icon-filter:before { + content: "\e914"; + } + + .icon-setting:before { + content: "\e915"; + } + + .icon-product:before { + content: "\e916"; + } + + .icon-contact:before { + content: "\e917"; + } + + .icon-activity:before { + content: "\e918"; + } + + .icon-perosnal:before { + content: "\e919"; + } + + .icon-quote:before { + content: "\e91a"; + } + + .icon-dashboard:before { + content: "\e91b"; + } + + .icon-cross-large:before { + content: "\e91c"; + } + + .icon-left-arrow:before { + content: "\e91d"; + } + + .icon-right-arrow:before { + content: "\e91e"; + } + + .icon-up-arrow:before { + content: "\e91f"; + } + + .icon-down-arrow:before { + content: "\e920"; + } + + .icon-search:before { + content: "\e921"; + } + + .icon-add:before { + content: "\e922"; + } + + .icon-add-2:before { + content: "\e923"; + } + + .label-active { + @apply max-w-max rounded-md bg-emerald-200 px-3 py-1.5 text-xs font-medium text-emerald-900; + } + + .label-inactive { + @apply max-w-max rounded-md bg-red-200 px-3 py-1.5 text-xs font-medium text-red-800; + } + + [dir="ltr"] .sidebar-rounded::after { + @apply content-[''] w-[30px] h-[30px] top-0 right-[-30px] absolute bg-no-repeat pointer-events-none bg-[url("../images/corner-clip.svg")] transition-all; + } + + [dir="rtl"] .sidebar-rounded::before { + @apply content-[''] w-[30px] h-[30px] top-0 left-[-30px] absolute bg-no-repeat pointer-events-none bg-[url("../images/corner-clip.svg")] transition-all rotate-90; + } + + .dark .sidebar-rounded::after { + @apply bg-[url("../images/dark-corner-clip.svg")] + } + + .dark .sidebar-rounded::before { + @apply bg-[url("../images/dark-corner-clip.svg")] + } + + .tox.tox-silver-sink.tox-tinymce-aux { + z-index: 99999; + } + + .stage::after { + content: ''; + position: absolute; + top: 50%; + right: -10px; + width: 24px; + height: 24px; + z-index: 1; + border-radius: 0 0 0 25px; + transform: translateY(-50%) rotate(45deg); + border-right: 4px solid #f3f4f6; + border-top: 4px solid #f3f4f6; + } + + .dark .stage::after { + content: ''; + position: absolute; + top: 50%; + right: -10px; + width: 24px; + height: 24px; + z-index: 1; + border-radius: 0 0 0 25px; + transform: translateY(-50%) rotate(45deg); + border-right: 4px solid #030712; + border-top: 4px solid #030712; + } + + [dir="rtl"] .stage::before { + content: ''; + position: absolute; + top: 50%; + left: -10px; + width: 24px; + height: 24px; + z-index: 1; + border-radius: 0 0 0 25px; + transform: translateY(-50%) rotate(225deg); + border-right: 4px solid #f3f4f6; + border-top: 4px solid #f3f4f6; + } + + [dir="rtl"] .stage::after { + display: none; + } + + .dark\:bg-gray-400:is(.dark *) { + --tw-bg-opacity: 1 !important; + background-color: #1f2937 !important; + border: #111827 solid !important; + color: #e5e7eb !important; + } + + .dark\:bg-gray-800:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(16 24 39); + } + + .shimmer { + animation-duration: 2.2s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: skeleton; + animation-timing-function: linear; + background: linear-gradient( + to right, + #f6f6f6 8%, + #f0f0f0 18%, + #f6f6f6 33% + ); + background-size: 1250px 100%; + } + + .light-shimmer-bg { + background: linear-gradient( + to right, + #fafafa 8%, + #f5f5f5 18%, + #fafafa 33% + ); + background-size: 1250px 100%; + } + + .dark .shimmer { + background: linear-gradient( + to right, + #1f2937 8%, + #1a2232 18%, + #1f2937 33% + ); + } + + .primary-button { + @apply bg-brandColor border border-brandColor cursor-pointer flex focus:opacity-[0.9] font-semibold gap-x-1 hover:opacity-[0.9] items-center place-content-center px-3 py-1.5 rounded-md text-gray-50 transition-all; + } + + .secondary-button { + @apply flex cursor-pointer place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border-2 border-brandColor bg-white px-3 py-1.5 font-semibold text-brandColor transition-all hover:bg-[#eff6ff61] focus:bg-[#eff6ff61] dark:border-gray-400 dark:bg-gray-800 dark:text-white dark:hover:opacity-80; + } + + .transparent-button { + @apply flex cursor-pointer appearance-none place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border-2 border-transparent px-3 py-1.5 font-semibold text-gray-600 transition-all marker:shadow hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-950 dark:focus:bg-gray-950; + } + + ::-webkit-scrollbar { + width: 12px; + } + + ::-webkit-scrollbar-track { + background: #f1f1f1; + } + + ::-webkit-scrollbar-thumb { + background: #888; + border-radius: 6px; + } + + ::-webkit-scrollbar-thumb:hover { + background: #555; + } + + /* Firefox */ + * { + scrollbar-width: thin; + scrollbar-color: #888 #f1f1f1; + } + + ::selection { + background-color: rgba(0, 68, 242, .2); + } + + body { + @apply bg-gray-100 text-sm text-gray-800; + } + + button:disabled { + @apply cursor-not-allowed opacity-50; + } + + button:disabled:hover { + @apply cursor-not-allowed opacity-50; + } + + .draggable-ghost { + opacity: 0.5; + background: #e0e7ff; + } + + html.dark [class^="icon-"], + html.dark [class*=" icon-"]{ + color: #d1d5db; + } + + p { + @apply text-[14px] !leading-[17px]; + } + + input, + textarea, + select { + @apply outline-none; + } + + .journal-scroll::-webkit-scrollbar { + width: 14px; + cursor: pointer; + display: none; + } + + .journal-scroll::-webkit-scrollbar-track { + background-color: #fff; + cursor: pointer; + border-radius: 12px; + border: 1px solid #e9e9e9; + } + + .journal-scroll::-webkit-scrollbar-thumb { + cursor: pointer; + background-color: #e9e9e9; + border-radius: 12px; + border: 3px solid transparent; + background-clip: content-box; + } + + .custom-select { + -webkit-appearance: none; + -moz-appearance: none; + background: transparent; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position-x: calc(100% - 10px); + background-position-y: 50%; + } + + .dark .custom-select{ + background-image: url("data:image/svg+xml;utf8,"); + } + + [dir="rtl"] .custom-select { + background-position-x: calc(100% - (100% - 10px)); + } + + .draggable-ghost { + @apply border border-dashed !border-gray-300; + } + + @keyframes skeleton { + 0% { + @apply bg-[-1250px_0]; + } + + 100% { + @apply bg-[1250px_0]; + } + } + + .required:after { + @apply content-['*']; + } + + body.is-scrolling-down .scroll-reactive-sticky { + width: calc(100% + 2rem); + margin-left: -1rem; + border-radius: 0px; + padding-left: 2rem; + padding-right: 2rem; + border-right: none !important; + } +} + +.tox .tox-toolbar__group:last-child button { + padding: 6px 8px; + background: #eff6ff; + color: #2563EB; +} + +.tox .tox-toolbar__group:last-child button:hover { + background: #dbeafe; +} + +.tox .tox-toolbar__group:last-child button[aria-disabled="true"] { + @apply cursor-not-allowed opacity-50; +} + +.tox .tox-toolbar__group:last-child button { + @apply !bg-transparent; +} + +.tox .tox-toolbar__group:last-child button:hover { + @apply border-0 !bg-[#3389ec] text-white shadow-none; +} + +.tox.tox-silver-sink.tox-tinymce-aux { + z-index: 99999; +} \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/assets/fonts/icomoon.woff b/packages/Webkul/Admin/src/Resources/assets/fonts/icomoon.woff new file mode 100644 index 0000000000000000000000000000000000000000..bedf008609c593ccf2e7c0d4d34c7c9fc4bf2fb2 GIT binary patch literal 37964 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 + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/dark-corner-clip.svg b/packages/Webkul/Admin/src/Resources/assets/images/dark-corner-clip.svg new file mode 100644 index 0000000..9c36b81 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/dark-corner-clip.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/assets/images/dark-logo.svg b/packages/Webkul/Admin/src/Resources/assets/images/dark-logo.svg new file mode 100644 index 0000000..47f0509 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/dark-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/activities.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/activities.svg new file mode 100644 index 0000000..890d0ea --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/activities.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/calls.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/calls.svg new file mode 100644 index 0000000..6ade01a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/calls.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/default.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/default.svg new file mode 100644 index 0000000..fa4e8d9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/default.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/description.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/description.svg new file mode 100644 index 0000000..d367df7 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/description.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/emails.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/emails.svg new file mode 100644 index 0000000..24a893f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/emails.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/files.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/files.svg new file mode 100644 index 0000000..e75d4fc --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/files.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/lunches.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/lunches.svg new file mode 100644 index 0000000..9eef171 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/lunches.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/meetings.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/meetings.svg new file mode 100644 index 0000000..6aec1c6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/meetings.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/notes.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/notes.svg new file mode 100644 index 0000000..d27d66c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/notes.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/organizations.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/organizations.svg new file mode 100644 index 0000000..a41bd86 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/organizations.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/pipedrive.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/pipedrive.svg new file mode 100644 index 0000000..fc9d411 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/pipedrive.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/plans.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/plans.svg new file mode 100644 index 0000000..669e469 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/plans.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/products.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/products.svg new file mode 100644 index 0000000..6c1fab3 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/products.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/quotes.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/quotes.svg new file mode 100644 index 0000000..db5d86d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/quotes.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/users.svg b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/users.svg new file mode 100644 index 0000000..1a90f23 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/empty-placeholders/users.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/error.svg b/packages/Webkul/Admin/src/Resources/assets/images/error.svg new file mode 100644 index 0000000..6fe0d2a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/error.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/favicon.ico b/packages/Webkul/Admin/src/Resources/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8fd69159384857b582cc8e427588faba37285153 GIT binary patch literal 1150 zcmd7OOG_J37{>888ZRK33WDIul#sCtsU*P*QtC=*loqloF1(de)Lj?qR?xkpg)Utc zKSu?@uizMks03O;XbSx+dd4__jl7pPb)>x7flw+Hh|T?SZl%@e5^KVj8R^|5+o` z=iBITQuzgxpsce--v&~D&xe6$hnrp${clDR9|lwYcb6SV`Gt4m@!aX>*mL$pwDDm}-wpzAJ^Lc@o*HLlPbNm6&K_)d+pHCk4TC4zUn!v$E_+VDj4K4MUnZs|U&W{L z8{QYj*R{1{+IkK8jnKI+tGpD+rNLhBXKl`{sm9oT5YySc2=A<`mTKx=%~(PSF0XbM zU5H{1KcM;BS=E;&EW*_&($9rKtnw}C>p!T*5oV!t>z`PCsKzq#$c3L;E48(WvFM&Y yw6C6Z9nGN?N$5Pc^r&sQY$n$uX1}}5E@}!bW?!+=_~&|&6|>bHv%S-5M!6fYLjX_! literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/src/Resources/assets/images/logo.svg b/packages/Webkul/Admin/src/Resources/assets/images/logo.svg new file mode 100644 index 0000000..50755ec --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/mobile-dark-logo.svg b/packages/Webkul/Admin/src/Resources/assets/images/mobile-dark-logo.svg new file mode 100644 index 0000000..1c24c93 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/mobile-dark-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/images/mobile-light-logo.svg b/packages/Webkul/Admin/src/Resources/assets/images/mobile-light-logo.svg new file mode 100644 index 0000000..3d0761d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/images/mobile-light-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/Webkul/Admin/src/Resources/assets/js/app.js b/packages/Webkul/Admin/src/Resources/assets/js/app.js new file mode 100644 index 0000000..4075af7 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/app.js @@ -0,0 +1,141 @@ +/** + * This will track all the images and fonts for publishing. + */ +import.meta.glob(["../images/**", "../fonts/**"]); + +/** + * Main vue bundler. + */ +import { createApp } from "vue/dist/vue.esm-bundler"; + +/** + * Main root application registry. + */ +window.app = createApp({ + data() { + return { + isMenuActive: false, + + hoveringMenu: '', + + lastScrollY: 0, + }; + }, + + created() { + window.addEventListener('click', this.handleFocusOut); + this.lastScrollY = Math.max((window.scrollY ?? window.pageYOffset ?? 0), 0); + document.body.classList.remove('is-scrolling-down'); + window.addEventListener('scroll', this.handleScrollDirection, { passive: true }); + + // Keep initial load in normal state when page is at/near top. + this.handleScrollDirection(); + }, + + beforeDestroy() { + window.removeEventListener('click', this.handleFocusOut); + window.removeEventListener('scroll', this.handleScrollDirection); + document.body.classList.remove('is-scrolling-down'); + }, + + beforeUnmount() { + window.removeEventListener('click', this.handleFocusOut); + window.removeEventListener('scroll', this.handleScrollDirection); + document.body.classList.remove('is-scrolling-down'); + }, + + methods: { + onSubmit() {}, + + onInvalidSubmit({ values, errors, results }) { + setTimeout(() => { + const errorKeys = Object.entries(errors) + .map(([key, value]) => ({ key, value })) + .filter(error => error["value"].length); + + let firstErrorElement = document.querySelector('[name="' + errorKeys[0]["key"] + '"]'); + + firstErrorElement.scrollIntoView({ + behavior: "smooth", + block: "center" + }); + }, 100); + }, + + /** + * Closes an open submenu flyout when the click lands outside the + * sidebar. The sidebar width itself is owned solely by the collapse + * toggle, so nothing here touches `sidebar-collapsed`. + */ + handleFocusOut(event) { + const sidebar = this.$refs.sidebar; + + if ( + sidebar && + !sidebar.contains(event.target) + ) { + this.isMenuActive = false; + } + }, + + handleScrollDirection() { + const topThreshold = 0; + const currentScrollY = Math.max((window.scrollY ?? window.pageYOffset ?? 0), 0); + const scrollDelta = currentScrollY - this.lastScrollY; + + if (currentScrollY == topThreshold) { + document.body.classList.remove('is-scrolling-down'); + this.lastScrollY = currentScrollY; + + return; + } + + if (Math.abs(scrollDelta) < 2) { + return; + } + + if (scrollDelta > 0) { + document.body.classList.add('is-scrolling-down'); + } + + this.lastScrollY = currentScrollY; + }, + }, +}); + +/** + * Global plugins registration. + */ +import Admin from "./plugins/admin"; +import Axios from "./plugins/axios"; +import Emitter from "./plugins/emitter"; +import Flatpickr from "./plugins/flatpickr"; +import VeeValidate from "./plugins/vee-validate"; +import CreateElement from "./plugins/createElement"; +import Draggable from "./plugins/draggable"; +import VueCal from "./plugins/vue-cal"; + +[ + Admin, + Axios, + Emitter, + CreateElement, + Draggable, + Flatpickr, + VeeValidate, + VueCal, +].forEach((plugin) => app.use(plugin)); + +/** + * Global directives. + */ +import Debounce from "./directives/debounce"; +import DOMPurify from "./directives/dompurify"; +import ToolTip from "./directives/tooltip"; + +app.directive("debounce", Debounce); +app.directive("safe-html", DOMPurify); +app.directive("tooltip", ToolTip); + +export default app; + diff --git a/packages/Webkul/Admin/src/Resources/assets/js/directives/debounce.js b/packages/Webkul/Admin/src/Resources/assets/js/directives/debounce.js new file mode 100644 index 0000000..5961581 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/directives/debounce.js @@ -0,0 +1,17 @@ +var timeoutID = null; + +export default { + mounted(el, binding) { + let handler = function (e) { + if (binding.value !== binding.oldValue) { + clearTimeout(timeoutID) + + timeoutID = setTimeout(function () { + el.dispatchEvent(new Event('change')) + }, binding.value || 500) + } + }; + + el.addEventListener('input', handler); + } +} \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/assets/js/directives/dompurify.js b/packages/Webkul/Admin/src/Resources/assets/js/directives/dompurify.js new file mode 100644 index 0000000..e990eba --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/directives/dompurify.js @@ -0,0 +1,10 @@ +import DOMPurify from 'dompurify'; + +export default { + beforeMount(el, binding) { + el.innerHTML = DOMPurify.sanitize(binding.value); + }, + updated(el, binding) { + el.innerHTML = DOMPurify.sanitize(binding.value); + } +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/js/directives/tooltip.js b/packages/Webkul/Admin/src/Resources/assets/js/directives/tooltip.js new file mode 100644 index 0000000..e6bf232 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/directives/tooltip.js @@ -0,0 +1,143 @@ +export default { + mounted(el, binding) { + initTooltip(el, binding); + }, + updated(el, binding) { + initTooltip(el, binding); + } +}; + +const initTooltip = (el, binding) => { + const defaultOptions = { + placement: 'top', + trigger: 'hover', + html: false, + content: '', + delay: { show: 200, hide: 100 } + }; + + const options = { + ...defaultOptions, + ...(typeof binding.value === 'object' ? binding.value : { content: binding.value }) + }; + + let tooltip = document.getElementById(`tooltip-${el.tooltipId}`); + + if (! tooltip) { + el.tooltipId = Math.random().toString(36).substring(2, 9); + tooltip = document.createElement('div'); + tooltip.id = `tooltip-${el.tooltipId}`; + tooltip.className = 'max-w-[250px] break-words rounded-lg bg-gray-800 px-4 py-3 text-sm leading-snug text-white shadow-lg transition-opacity transition-transform duration-200'; + tooltip.style.display = 'none'; + tooltip.style.position = 'absolute'; + tooltip.style.zIndex = '10000'; + + const inner = document.createElement('div'); + inner.className = 'tooltip-inner'; + + const arrow = document.createElement('div'); + arrow.className = 'absolute h-0 w-0 border-solid'; + + tooltip.appendChild(inner); + tooltip.appendChild(arrow); + document.body.appendChild(tooltip); + + if (options.html) { + inner.innerHTML = options.content; + } else { + inner.textContent = options.content; + } + + el._tooltip = tooltip; + + const showTooltip = () => { + tooltip.style.display = 'block'; + + const rect = el.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + + let top, left; + + switch (options.placement) { + case 'top': + top = rect.top - tooltipRect.height - 10; + left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); + arrow.style.top = 'auto'; + arrow.style.bottom = '-5px'; + arrow.style.left = '50%'; + arrow.style.transform = 'translateX(-50%)'; + break; + case 'bottom': + top = rect.bottom + 10; + left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); + arrow.style.bottom = 'auto'; + arrow.style.top = '-5px'; + arrow.style.left = '50%'; + arrow.style.transform = 'translateX(-50%) rotate(180deg)'; + break; + case 'left': + top = rect.top + (rect.height / 2) - (tooltipRect.height / 2); + left = rect.left - tooltipRect.width - 10; + arrow.style.top = '50%'; + arrow.style.left = 'auto'; + arrow.style.right = '-5px'; + arrow.style.transform = 'translateY(-50%) rotate(-90deg)'; + break; + case 'right': + top = rect.top + (rect.height / 2) - (tooltipRect.height / 2); + left = rect.right + 10; + arrow.style.top = '50%'; + arrow.style.right = 'auto'; + arrow.style.left = '-5px'; + arrow.style.transform = 'translateY(-50%) rotate(90deg)'; + break; + } + + if (top < 0) { + top = 0; + } + + if (left < 0) { + left = 0; + } + + if (left + tooltipRect.width > window.innerWidth) { + left = window.innerWidth - tooltipRect.width; + } + + tooltip.style.top = `${top + window.scrollY}px`; + tooltip.style.left = `${left + window.scrollX}px`; + }; + + const hideTooltip = () => { + tooltip.style.display = 'none'; + }; + + if (options.trigger === 'hover') { + el.addEventListener('mouseenter', () => { + el._showTimeout = setTimeout(showTooltip, options.delay.show); + }); + + el.addEventListener('mouseleave', () => { + clearTimeout(el._showTimeout); + el._hideTimeout = setTimeout(hideTooltip, options.delay.hide); + }); + } else if (options.trigger === 'click') { + el.addEventListener('click', showTooltip); + + document.addEventListener('click', (e) => { + if (e.target !== el && !el.contains(e.target)) { + hideTooltip(); + } + }); + } + } else { + const inner = tooltip.querySelector('.tooltip-inner'); + + if (options.html) { + inner.innerHTML = options.content; + } else { + inner.textContent = options.content; + } + } +} diff --git a/packages/Webkul/Admin/src/Resources/assets/js/plugins/admin.js b/packages/Webkul/Admin/src/Resources/assets/js/plugins/admin.js new file mode 100644 index 0000000..6d1aa23 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/plugins/admin.js @@ -0,0 +1,148 @@ +export default { + install(app) { + app.config.globalProperties.$admin = { + /** + * Generates a formatted price. + * + * @param {number} price - The price value to be formatted. + * @returns {string} - The formatted price string. + */ + formatPrice: (price) => { + let locale = document.querySelector( + 'meta[http-equiv="content-language"]' + ).content; + + locale = locale.replace(/([a-z]{2})_([A-Z]{2})/g, "$1-$2"); + + const currency = JSON.parse( + document.querySelector('meta[name="currency"]').content + ); + + const symbol = + currency.symbol !== "" ? currency.symbol : currency.code; + + if (!currency.currency_position) { + return new Intl.NumberFormat(locale, { + style: "currency", + currency: currency.code, + }).format(price); + } + + const formatter = new Intl.NumberFormat(locale, { + style: "currency", + currency: currency.code, + minimumFractionDigits: currency.decimal ?? 2, + }); + + const formattedCurrency = formatter + .formatToParts(price) + .map((part) => { + switch (part.type) { + case "currency": + return ""; + + case "group": + return currency.group_separator === "" + ? part.value + : currency.group_separator; + + case "decimal": + return currency.decimal_separator === "" + ? part.value + : currency.decimal_separator; + + default: + return part.value; + } + }) + .join(""); + + switch (currency.currency_position) { + case "left": + return symbol + formattedCurrency; + + case "left_with_space": + return symbol + " " + formattedCurrency; + + case "right": + return formattedCurrency + symbol; + + case "right_with_space": + return formattedCurrency + " " + symbol; + + default: + return formattedCurrency; + } + }, + + /** + * Generates a formatted date based on specified timezone. + * + * @param {string} dateString - The date value to be formatted. + * @param {string} format - The format to be used for formatting the date. + * @param {string} timezone - The timezone to use (e.g., 'America/New_York'). + * @returns {string} - The formatted date string. + */ + formatDate: (dateString, format, timezone) => { + const date = new Date(dateString); + + const options = { timeZone: timezone }; + + const formatter = new Intl.DateTimeFormat("en-US", { + ...options, + hour12: false, + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }); + + const parts = formatter.formatToParts(date); + const dateParts = {}; + + parts.forEach((part) => { + if (part.type !== "literal") { + dateParts[part.type] = part.value; + } + }); + + const tzDay = parseInt(dateParts.day, 10); + const tzMonth = parseInt(dateParts.month, 10); + const tzYear = parseInt(dateParts.year, 10); + const tzHour = parseInt(dateParts.hour, 10); + const tzMinute = parseInt(dateParts.minute, 10); + + const formatters = { + d: tzDay, + DD: tzDay.toString().padStart(2, "0"), + M: tzMonth, + MM: tzMonth.toString().padStart(2, "0"), + MMM: new Date(tzYear, tzMonth - 1, 1).toLocaleString( + "en-US", + { month: "short" } + ), + MMMM: new Date(tzYear, tzMonth - 1, 1).toLocaleString( + "en-US", + { month: "long" } + ), + yy: tzYear.toString().slice(-2), + yyyy: tzYear, + H: tzHour, + HH: tzHour.toString().padStart(2, "0"), + h: tzHour % 12 || 12, + hh: (tzHour % 12 || 12).toString().padStart(2, "0"), + m: tzMinute, + mm: tzMinute.toString().padStart(2, "0"), + A: tzHour < 12 ? "AM" : "PM", + }; + + return format.replace( + /\b(?:d|DD|M|MM|MMM|MMMM|yy|yyyy|H|HH|h|hh|m|mm|A)\b/g, + (match) => formatters[match] + ); + }, + }; + }, +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/js/plugins/axios.js b/packages/Webkul/Admin/src/Resources/assets/js/plugins/axios.js new file mode 100644 index 0000000..d96e755 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/plugins/axios.js @@ -0,0 +1,14 @@ +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ +import axios from "axios"; +window.axios = axios; +window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; + +export default { + install(app) { + app.config.globalProperties.$axios = axios; + }, +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/js/plugins/createElement.js b/packages/Webkul/Admin/src/Resources/assets/js/plugins/createElement.js new file mode 100644 index 0000000..714c11e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/plugins/createElement.js @@ -0,0 +1,15 @@ +import { h, resolveComponent } from "vue/dist/vue.esm-bundler"; + +export default { + install(app) { + /** + * Create the virtual dom element + */ + app.config.globalProperties.$h = h; + + /** + * Resolve the component which is globally registered + */ + app.config.globalProperties.$resolveComponent = resolveComponent; + }, +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/js/plugins/draggable.js b/packages/Webkul/Admin/src/Resources/assets/js/plugins/draggable.js new file mode 100644 index 0000000..a8fc1fa --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/plugins/draggable.js @@ -0,0 +1,10 @@ +import Draggable from 'vuedraggable'; + +export default { + install: (app) => { + /** + * Global component registration; + */ + app.component("draggable", Draggable); + }, +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/js/plugins/emitter.js b/packages/Webkul/Admin/src/Resources/assets/js/plugins/emitter.js new file mode 100644 index 0000000..eb6a588 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/plugins/emitter.js @@ -0,0 +1,11 @@ +import mitt from "mitt"; + +const emitter = mitt(); + +window.emitter = emitter; + +export default { + install: (app, options) => { + app.config.globalProperties.$emitter = emitter; + }, +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/js/plugins/flatpickr.js b/packages/Webkul/Admin/src/Resources/assets/js/plugins/flatpickr.js new file mode 100644 index 0000000..835ec6d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/plugins/flatpickr.js @@ -0,0 +1,56 @@ +import Flatpickr from "flatpickr"; +import "flatpickr/dist/flatpickr.css"; +import { Spanish } from "flatpickr/dist/l10n/es.js"; +import { Arabic } from "flatpickr/dist/l10n/ar.js"; +import { Persian } from "flatpickr/dist/l10n/fa.js"; +import { Turkish } from "flatpickr/dist/l10n/tr.js"; + +export default { + install: (app) => { + window.Flatpickr = Flatpickr; + + const setLocaleFromLang = () => { + const lang = document.documentElement.lang || "en"; + + const localeMap = { + es: Spanish, + ar: Arabic, + fa: Persian, + tr: Turkish + }; + + const locale = localeMap[lang] || null; + + if (locale) { + window.Flatpickr.localize(locale); + } + }; + + setLocaleFromLang(); + + const changeTheme = (theme) => { + document.getElementById('flatpickr')?.remove(); + + if (theme === 'light') { + return; + } + + const linkElement = document.createElement("link"); + + linkElement.rel = "stylesheet"; + linkElement.type = "text/css"; + linkElement.href = `https://npmcdn.com/flatpickr/dist/themes/${theme}.css`; + linkElement.id = 'flatpickr'; + + document.head.appendChild(linkElement); + }; + + const currentTheme = document.documentElement.classList.contains("dark") + ? "dark" + : "light"; + + changeTheme(currentTheme); + + window.emitter.on("change-theme", (theme) => changeTheme(theme)); + }, +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/js/plugins/vee-validate.js b/packages/Webkul/Admin/src/Resources/assets/js/plugins/vee-validate.js new file mode 100644 index 0000000..018bf05 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/plugins/vee-validate.js @@ -0,0 +1,194 @@ +/** + * We are defining all the global rules here and configuring + * all the `vee-validate` settings. + */ +import { configure, defineRule, Field, Form, ErrorMessage } from "vee-validate"; +import { localize, setLocale } from "@vee-validate/i18n"; +import ar from "@vee-validate/i18n/dist/locale/ar.json"; +import bn from "@vee-validate/i18n/dist/locale/bn.json"; +import de from "@vee-validate/i18n/dist/locale/de.json"; +import en from "@vee-validate/i18n/dist/locale/en.json"; +import es from "@vee-validate/i18n/dist/locale/es.json"; +import fa from "@vee-validate/i18n/dist/locale/fa.json"; +import fr from "@vee-validate/i18n/dist/locale/fr.json"; +import he from "@vee-validate/i18n/dist/locale/he.json"; +import hi_IN from "../../locales/hi_IN.json"; +import it from "@vee-validate/i18n/dist/locale/it.json"; +import ja from "@vee-validate/i18n/dist/locale/ja.json"; +import nl from "@vee-validate/i18n/dist/locale/nl.json"; +import pl from "@vee-validate/i18n/dist/locale/pl.json"; +import pt_BR from "@vee-validate/i18n/dist/locale/pt_BR.json"; +import ru from "@vee-validate/i18n/dist/locale/ru.json"; +import sin from "../../locales/sin.json"; +import tr from "@vee-validate/i18n/dist/locale/tr.json"; +import uk from "@vee-validate/i18n/dist/locale/uk.json"; +import zh_CN from "@vee-validate/i18n/dist/locale/zh_CN.json"; +import { all } from '@vee-validate/rules'; + +window.defineRule = defineRule; + +export default { + install: (app) => { + /** + * Global components registration; + */ + app.component("VForm", Form); + app.component("VField", Field); + app.component("VErrorMessage", ErrorMessage); + + window.addEventListener("load", () => setLocale(document.documentElement.attributes.lang.value)); + + /** + * Registration of all global validators. + */ + Object.entries(all).forEach(([name, rule]) => defineRule(name, rule)); + + /** + * This regular expression allows phone numbers with the following conditions: + * - The phone number can start with an optional "+" sign. + * - After the "+" sign, there should be one or more digits. + * + * This validation is sufficient for global-level phone number validation. If + * someone wants to customize it, they can override this rule. + */ + defineRule("phone", (value) => { + if (! value || ! value.length) { + return true; + } + + if (! /^\+?\d+$/.test(value)) { + return false; + } + + return true; + }); + + defineRule("address", (value) => { + if (!value || !value.length) { + return true; + } + + if ( + !/^[a-zA-Z0-9\s.\/*'\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF\u3040-\u309F\u30A0-\u30FF\u0400-\u04FF\u0D80-\u0DFF\u3400-\u4DBF\u2000-\u2A6D\u00C0-\u017F\u0980-\u09FF\u0900-\u097F\u4E00-\u9FFF,\(\)-]{1,60}$/iu.test( + value + ) + ) { + return false; + } + + return true; + }); + + defineRule("postcode", (value) => { + if (! value || ! value.length) { + return true; + } + + if (! /^[a-zA-Z0-9][a-zA-Z0-9\s-]*[a-zA-Z0-9]$/.test(value)) { + return false; + } + + return true; + }); + + defineRule("decimal", (value, { decimals = '*', separator = '.' } = {}) => { + if (value === null || value === undefined || value === '') { + return true; + } + + if (Number(decimals) === 0) { + return /^-?\d*$/.test(value); + } + + const regexPart = decimals === '*' ? '+' : `{1,${decimals}}`; + const regex = new RegExp(`^[-+]?\\d*(\\${separator}\\d${regexPart})?([eE]{1}[-]?\\d+)?$`); + + return regex.test(value); + }); + + defineRule("required_if", (value, { condition = true } = {}) => { + if (condition) { + if (value === null || value === undefined || value === '') { + return false; + } + } + + return true; + }); + + defineRule("", () => true); + + defineRule("date_format", (value) => { + const regex = /^\d{4}-\d{2}-\d{2}$/; + + return regex.test(value); + }); + + defineRule("after", (value) => { + const today = new Date(); + const inputDate = new Date(value); + + today.setHours(0, 0, 0, 0); + inputDate.setHours(0, 0, 0, 0); + + return inputDate >= today; + }); + + configure({ + /** + * Built-in error messages and custom error messages are available. Multiple + * locales can be added in the same way. + */ + generateMessage: localize({ + ar: { + ...ar, + messages: { + ...ar.messages, + phone: "يجب أن يكون هذا {field} رقم هاتف صالحًا", + after: "يجب أن يكون {field} تاريخًا في المستقبل أو اليوم.", + }, + }, + + en: { + ...en, + messages: { + ...en.messages, + phone: "This {field} must be a valid phone number", + after: "The {field} must be a date in the future or today.", + }, + }, + + es: { + ...es, + messages: { + ...es.messages, + phone: "Este {field} debe ser un número de teléfono válido.", + after: "El {field} debe ser una fecha en el futuro o hoy.", + }, + }, + + fa: { + ...fa, + messages: { + ...fa.messages, + phone: "این {field} باید یک شماره تلفن معتبر باشد.", + after: "{field} باید یک تاریخ در آینده یا امروز باشد.", + }, + }, + + tr: { + ...tr, + messages: { + ...tr.messages, + phone: "Bu {field} geçerli bir telefon numarası olmalıdır.", + after: "{field} gelecekte veya bugün olmalıdır.", + }, + }, + }), + + validateOnBlur: true, + validateOnInput: true, + validateOnChange: true, + }); + }, +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/js/plugins/vue-cal.js b/packages/Webkul/Admin/src/Resources/assets/js/plugins/vue-cal.js new file mode 100644 index 0000000..9aa6209 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/js/plugins/vue-cal.js @@ -0,0 +1,16 @@ + +import VueCal from 'vue-cal'; + +import 'vue-cal/dist/vuecal.css'; + +import 'vue-cal/dist/i18n/ar.es.js'; +import 'vue-cal/dist/i18n/en.es.js'; +import 'vue-cal/dist/i18n/es.es.js'; +import 'vue-cal/dist/i18n/fa.es.js'; +import 'vue-cal/dist/i18n/tr.es.js'; +import 'vue-cal/dist/i18n/pt-br.es.js'; +import 'vue-cal/dist/i18n/vi.es.js'; + +export default { + install: (app, options) => app.component('v-vue-cal', VueCal), +}; diff --git a/packages/Webkul/Admin/src/Resources/assets/locales/hi_IN.json b/packages/Webkul/Admin/src/Resources/assets/locales/hi_IN.json new file mode 100644 index 0000000..bb7bb23 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/locales/hi_IN.json @@ -0,0 +1,32 @@ +{ + "code": "hi_IN", + "messages": { + "_default": "यह {field} मान्य नहीं है", + "alpha": "{field} फ़ील्ड में केवल वर्णात्मक अक्षर हो सकते हैं", + "alpha_num": "{field} फ़ील्ड में केवल वर्णात्मक और संख्यात्मक अक्षर हो सकते हैं", + "alpha_dash": "{field} फ़ील्ड में वर्णात्मक और संख्यात्मक अक्षरों के साथ डैश और अंडरस्कोर हो सकते हैं", + "alpha_spaces": "{field} फ़ील्ड में केवल वर्णात्मक अक्षर और अंतर हो सकते हैं", + "between": "{field} फ़ील्ड 0:{min} और 1:{max} के बीच होना चाहिए", + "confirmed": "{field} फ़ील्ड की पुष्टि मेल नहीं खाती", + "digits": "{field} फ़ील्ड संख्यात्मक होनी चाहिए और बिल्कुल 0:{length} अंक होने चाहिए", + "dimensions": "{field} फ़ील्ड 0:{width} पिक्सेल और 1:{height} पिक्सेल होना चाहिए", + "email": "{field} फ़ील्ड में एक मान्य ईमेल होना चाहिए", + "not_one_of": "{field} फ़ील्ड मान्य मूल्य नहीं है", + "ext": "{field} फ़ील्ड में मान्य फ़ाइल नहीं है", + "image": "{field} फ़ील्ड एक छवि होनी चाहिए", + "integer": "{field} फ़ील्ड एक पूर्णांक होना चाहिए", + "length": "{field} फ़ील्ड 0:{length} लंबा होना चाहिए", + "max_value": "{field} फ़ील्ड 0:{max} या उससे कम होना चाहिए", + "max": "{field} फ़ील्ड 0:{length} अक्षरों से अधिक नहीं हो सकता", + "mimes": "{field} फ़ील्ड को मान्य फ़ाइल प्रकार होना चाहिए", + "min_value": "{field} फ़ील्ड 0:{min} या उससे अधिक होना चाहिए", + "min": "{field} फ़ील्ड कम से कम 0:{length} अक्षरों का होना चाहिए", + "numeric": "{field} फ़ील्ड में केवल संख्याएँ हो सकती हैं", + "one_of": "{field} फ़ील्ड मान्य मूल्य नहीं है", + "regex": "{field} फ़ील्ड का प्रारूप अवैध है", + "required_if": "{field} फ़ील्ड आवश्यक है", + "required": "{field} फ़ील्ड आवश्यक है", + "size": "{field} फ़ील्ड का आकार 0:{size}KB से कम होना चाहिए", + "url": "{field} फ़ील्ड में एक मान्य URL नहीं है" + } +} diff --git a/packages/Webkul/Admin/src/Resources/assets/locales/sin.json b/packages/Webkul/Admin/src/Resources/assets/locales/sin.json new file mode 100644 index 0000000..d0f8a85 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/assets/locales/sin.json @@ -0,0 +1,32 @@ +{ + "code": "sin", + "messages": { + "_default": "මේ {field} වල වලංගු නොවේ", + "alpha": "{field} ක්ෂණික සංඛ්‍යාවක් පිළිබඳව සියල්ල සියල්ල සහිතව හැකිය", + "alpha_num": "{field} ක්ෂණික සහ සංඛ්‍යාවක් පිළිබඳව සියල්ල සහිතව හැකිය", + "alpha_dash": "{field} ක්ෂණික සහ සංඛ්‍යාවක් සමග දැහැ හෝ පරිදි ලොව සහිතව හැකිය", + "alpha_spaces": "{field} ක්ෂණික සංඛ්‍යාවක් සහිතව හැකිය, සහ වීඩියෝ හෝම්හෝ සහිතව හැකිය", + "between": "{field} ක්ෂණික 0:{min} සහ 1:{max} අතර විය යුතුය", + "confirmed": "{field} ක්ෂණික තහවුරු නොගත් බව තහවුරු කර නොයාය", + "digits": "{field} ක්ෂණික සෂ්යෝගයක් හා සියලුමේ විය 0:{length} දිගු විය යුතුය", + "dimensions": "{field} ක්ෂණික 0:{width} පික්සල සහ 1:{height} පික්සල විය යුතුය", + "email": "{field} ක්ෂණික වලංගු ඊමේල් එක හෝ යුක්ත විය යුතුය", + "not_one_of": "{field} ක්ෂණික වලංගු අගය නොවේ", + "ext": "{field} ක්ෂණික වලංගු ගොනුව නොවේ", + "image": "{field} ක්ෂණික වලංගු ඡායාරූපය යුතුය", + "integer": "{field} ක්ෂණික වලංගු නික්මෙර වර්ගයේ යුතුය", + "length": "{field} ක්ෂණික වලංගු 0:{length} හෝමාව යුතුය", + "max_value": "{field} ක්ෂණික 0:{max} හෝමා හෝමා හෝමා යුතුය", + "max": "{field} ක්ෂණික 0:{length} අකුරු වලංගු වී නොයාය", + "mimes": "{field} ක්ෂණික ගොනුවේ වලංගු ගොනු වර්ගය හෝ හෝ හෝ යුතුය", + "min_value": "{field} ක්ෂණික 0:{min} හෝමාව හෝමාව හෝමාව හෝමාව හෝමාව යුතුය", + "min": "{field} ක්ෂණික 0:{length} හෝමාවක් හෝමාවක් හෝමාවක් හෝමාවක් යුතුය", + "numeric": "{field} ක්ෂණික වලංගු සංඛ්‍යාවෙන් වයස්ක්‍ර සංඛ්‍යාවෙන් වයස්ක්‍ර විය ෺", + "one_of": "{field} ක්ෂණික වලංගු අගය නොවේ", + "regex": "{field} ක්ෂණික වලංගු ආකාරය අවලංගුය", + "required_if": "{field} ක්ෂණිකයෙන් හෝයි", + "required": "{field} ක්ෂණිකයෙන් හෝයි", + "size": "{field} ක්ෂණික වලංගු විය හැකි ආකාරය 0:{size}KB හෝ හොයා යුතුයි", + "url": "{field} ක්ෂණික වලංගු වර්ගවල URL නොවේ" + } +} \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/lang/en/app.php b/packages/Webkul/Admin/src/Resources/lang/en/app.php new file mode 100644 index 0000000..5ffec5f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/lang/en/app.php @@ -0,0 +1,2483 @@ + [ + 'help' => 'Help', + 'leads' => 'Leads', + 'lead' => 'Lead', + 'quotes' => 'Quotes', + 'mail' => 'Mail', + 'inbox' => 'Inbox', + 'draft' => 'Draft', + 'outbox' => 'Outbox', + 'sent' => 'Sent', + 'trash' => 'Trash', + 'activities' => 'Activities', + 'webhook' => 'Webhook', + 'contacts' => 'Contacts', + 'persons' => 'Persons', + 'organizations' => 'Organizations', + 'products' => 'Products', + 'settings' => 'Settings', + 'groups' => 'Groups', + 'roles' => 'Roles', + 'users' => 'Users', + 'user' => 'User', + 'automation' => 'Automation', + 'attributes' => 'Attributes', + 'pipelines' => 'Pipelines', + 'sources' => 'Sources', + 'types' => 'Types', + 'email-templates' => 'Email Templates', + 'workflows' => 'Workflows', + 'other-settings' => 'Other Settings', + 'tags' => 'Tags', + 'configuration' => 'Configuration', + 'create' => 'Create', + 'quick_add' => 'Quick Add', + 'edit' => 'Edit', + 'view' => 'View', + 'print' => 'Print', + 'delete' => 'Delete', + 'export' => 'Export', + 'mass-delete' => 'Mass Delete', + 'data-transfer' => 'Data Transfer', + 'imports' => 'Imports', + 'import' => 'Import', + 'event' => 'Event', + 'campaigns' => 'Campaigns', + 'warehouses' => 'Warehouses', + 'inventory' => 'Inventory', + ], + 'users' => [ + 'activate-warning' => 'Your account is not activated yet. Please contact the administrator.', + 'login-error' => 'The credentials do not match our records.', + 'not-permission' => 'You do not have permission to access the admin panel.', + 'login' => [ + 'email' => 'Email Address', + 'forget-password-link' => 'Forget Password ?', + 'password' => 'Password', + 'submit-btn' => 'Sign In', + 'title' => 'Sign In', + ], + 'forget-password' => [ + 'create' => [ + 'email' => 'Registered Email', + 'email-not-exist' => 'Email Not Exists', + 'page-title' => 'Forget Password', + 'reset-link-sent' => 'Reset Password link sent', + 'sign-in-link' => 'Back to Sign In ?', + 'submit-btn' => 'Reset', + 'title' => 'Recover Password', + ], + ], + 'reset-password' => [ + 'back-link-title' => 'Back to Sign In ?', + 'confirm-password' => 'Confirm Password', + 'email' => 'Registered Email', + 'password' => 'Password', + 'submit-btn' => 'Reset Password', + 'title' => 'Reset Password', + ], + ], + 'account' => [ + 'edit' => [ + 'back-btn' => 'Back', + 'change-password' => 'Change Password', + 'confirm-password' => 'Confirm Password', + 'current-password' => 'Current Password', + 'email' => 'Email', + 'general' => 'General', + 'invalid-password' => 'The current password you entered is incorrect.', + 'name' => 'Name', + 'password' => 'Password', + 'profile-image' => 'Profile Image', + 'save-btn' => 'Save Account', + 'title' => 'My Account', + 'update-success' => 'Account updated successfully', + 'upload-image-info' => 'Upload a Profile Image (110px X 110px) in PNG or JPG Format', + ], + ], + 'components' => [ + 'activities' => [ + 'actions' => [ + 'mail' => [ + 'btn' => 'Mail', + 'title' => 'Compose Mail', + 'to' => 'To', + 'enter-emails' => 'Press enter to add emails', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Subject', + 'send-btn' => 'Send', + 'message' => 'Message', + ], + 'file' => [ + 'btn' => 'File', + 'title' => 'Add File', + 'title-control' => 'Title', + 'name' => 'Name', + 'description' => 'Description', + 'file' => 'File', + 'save-btn' => 'Save File', + ], + 'note' => [ + 'btn' => 'Note', + 'title' => 'Add Note', + 'comment' => 'Comment', + 'save-btn' => 'Save Note', + ], + 'activity' => [ + 'btn' => 'Activity', + 'title' => 'Add Activity', + 'title-control' => 'Title', + 'description' => 'Description', + 'schedule-from' => 'Schedule From', + 'schedule-to' => 'Schedule To', + 'location' => 'Location', + 'call' => 'Call', + 'meeting' => 'Meeting', + 'lunch' => 'Lunch', + 'save-btn' => 'Save Activity', + 'participants' => [ + 'title' => 'Participants', + 'placeholder' => 'Type to search participants', + 'users' => 'Users', + 'persons' => 'Persons', + 'no-results' => 'No result found...', + ], + ], + ], + 'index' => [ + 'all' => 'All', + 'bcc' => 'Bcc', + 'by-user' => 'By :user', + 'calls' => 'Calls', + 'cc' => 'Cc', + 'change-log' => 'Changelogs', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'emails' => 'Emails', + 'empty' => 'Empty', + 'files' => 'Files', + 'from' => 'From', + 'location' => 'Location', + 'lunches' => 'Lunches', + 'mark-as-done' => 'Mark as Done', + 'meetings' => 'Meetings', + 'notes' => 'Notes', + 'participants' => 'Participants', + 'planned' => 'Planned', + 'quotes' => 'Quotes', + 'scheduled-on' => 'Scheduled on', + 'system' => 'System', + 'to' => 'To', + 'unlink' => 'Unlink', + 'view' => 'View', + 'empty-placeholders' => [ + 'all' => [ + 'title' => 'No Activities Found', + 'description' => 'No activities found for this. You can add activities by clicking on the Activity button on the left panel.', + ], + 'planned' => [ + 'title' => 'No Planned Activities Found', + 'description' => 'No planned activities found for this. You can add planned activities by clicking on the Activity button on the left panel.', + ], + 'notes' => [ + 'title' => 'No Notes Found', + 'description' => 'No notes found for this. You can add notes by clicking on the Note button on the left panel.', + ], + 'calls' => [ + 'title' => 'No Calls Found', + 'description' => 'No calls found for this. You can add calls by clicking on the Activity button on the left panel and selecting the Call type.', + ], + 'meetings' => [ + 'title' => 'No Meetings Found', + 'description' => 'No meetings found for this. You can add meetings by clicking on the Activity button on the left panel and selecting the Meeting type.', + ], + 'lunches' => [ + 'title' => 'No Lunches Found', + 'description' => 'No lunches found for this. You can add lunches by clicking on the Activity button on the left panel and selecting the Lunch type.', + ], + 'files' => [ + 'title' => 'No Files Found', + 'description' => 'No files found for this. You can add files by clicking on the File button on the left panel.', + ], + 'emails' => [ + 'title' => 'No Emails Found', + 'description' => 'No emails found for this. You can add emails by clicking on the Mail button on the left panel.', + ], + 'system' => [ + 'title' => 'No Changelogs Found', + 'description' => 'No changelogs found for this.', + ], + ], + ], + ], + 'media' => [ + 'images' => [ + 'add-image-btn' => 'Add Image', + 'ai-add-image-btn' => 'Magic AI', + 'allowed-types' => 'png, jpeg, jpg', + 'not-allowed-error' => 'Only images files (.jpeg, .jpg, .png, ..) are allowed.', + 'placeholders' => [ + 'front' => 'Front', + 'next' => 'Next', + 'size' => 'Size', + 'use-cases' => 'Use Cases', + 'zoom' => 'Zoom', + ], + ], + 'videos' => [ + 'add-video-btn' => 'Add Video', + 'allowed-types' => 'mp4, webm, mkv', + 'not-allowed-error' => 'Only videos files (.mp4, .mov, .ogg ..) are allowed.', + ], + ], + 'datagrid' => [ + 'index' => [ + 'no-records-selected' => 'No records have been selected.', + 'must-select-a-mass-action-option' => 'You must select a mass action\'s option.', + 'must-select-a-mass-action' => 'You must select a mass action.', + ], + 'toolbar' => [ + 'length-of' => ':length of', + 'of' => 'of', + 'per-page' => 'Per Page', + 'results' => ':total Results', + 'delete' => 'Delete', + 'selected' => ':total Items Selected', + 'mass-actions' => [ + 'submit' => 'Submit', + 'select-option' => 'Select Option', + 'select-action' => 'Select Action', + ], + 'filter' => [ + 'apply-filters-btn' => 'Apply Filters', + 'back-btn' => 'Back', + 'create-new-filter' => 'Create New Filter', + 'custom-filters' => 'Custom Filters', + 'delete-error' => 'Something went wrong while deleting the filter, please try again.', + 'delete-success' => 'Filter has been deleted successfully.', + 'empty-description' => 'There is no selected filters available to save. Please select filters to save.', + 'empty-title' => 'Add Filters to Save', + 'name' => 'Name', + 'quick-filters' => 'Quick Filters', + 'save-btn' => 'Save', + 'save-filter' => 'Save Filter', + 'saved-success' => 'Filter has been saved successfully.', + 'selected-filters' => 'Selected Filters', + 'title' => 'Filter', + 'update' => 'Update', + 'update-filter' => 'Update Filter', + 'updated-success' => 'Filter has been updated successfully.', + ], + 'search' => [ + 'title' => 'Search', + ], + 'column-settings' => [ + 'title' => 'Column Settings', + ], + ], + 'filters' => [ + 'select' => 'Select', + 'title' => 'Filters', + 'dropdown' => [ + 'searchable' => [ + 'at-least-two-chars' => 'Type at least 2 characters...', + 'no-results' => 'No result found...', + ], + ], + 'custom-filters' => [ + 'clear-all' => 'Clear All', + 'title' => 'Custom Filters', + ], + 'boolean-options' => [ + 'false' => 'False', + 'true' => 'True', + ], + 'date-options' => [ + 'last-month' => 'Last Month', + 'last-six-months' => 'Last 6 Months', + 'last-three-months' => 'Last 3 Months', + 'this-month' => 'This Month', + 'this-week' => 'This Week', + 'this-year' => 'This Year', + 'today' => 'Today', + 'yesterday' => 'Yesterday', + ], + ], + 'table' => [ + 'actions' => 'Actions', + 'no-records-available' => 'No Records Available.', + ], + ], + 'modal' => [ + 'confirm' => [ + 'agree-btn' => 'Agree', + 'disagree-btn' => 'Disagree', + 'message' => 'Are you sure you want to perform this action?', + 'title' => 'Are you sure?', + ], + ], + 'tags' => [ + 'index' => [ + 'title' => 'Tags', + 'added-tags' => 'Added Tags', + 'save-btn' => 'Save Tag', + 'placeholder' => 'Type to search tags', + 'add-tag' => 'Add \":term\"...', + 'aquarelle-red' => 'Aquarelle Red', + 'crushed-cashew' => 'Crushed Cashew', + 'beeswax' => 'Beeswax', + 'lemon-chiffon' => 'Lemon Chiffon', + 'snow-flurry' => 'Snow Flurry', + 'honeydew' => 'Honeydew', + ], + ], + 'layouts' => [ + 'powered-by' => [ + 'description' => 'Powered by :krayin, an open-source project by :webkul.', + ], + 'header' => [ + 'mega-search' => [ + 'title' => 'Mega Search', + 'tabs' => [ + 'leads' => 'Leads', + 'quotes' => 'Quotes', + 'persons' => 'Persons', + 'products' => 'Products', + 'settings' => 'Settings', + 'configurations' => 'Configurations', + ], + + 'explore-all-products' => 'Explore all Products', + 'explore-all-leads' => 'Explore all Leads', + 'explore-all-contacts' => 'Explore all Contacts', + 'explore-all-quotes' => 'Explore all Quotes', + 'explore-all-settings' => 'Explore all Settings', + 'explore-all-configurations' => 'Explore all Configurations', + 'explore-all-matching-products' => 'Explore all products matching ":query" (:count)', + 'explore-all-matching-leads' => 'Explore all leads matching ":query" (:count)', + 'explore-all-matching-contacts' => 'Explore all contacts matching ":query" (:count)', + 'explore-all-matching-quotes' => 'Explore all quotes matching ":query" (:count)', + ], + ], + ], + 'attributes' => [ + 'edit' => [ + 'delete' => 'Delete', + ], + 'lookup' => [ + 'click-to-add' => 'Click to add', + 'search' => 'Search...', + 'no-result-found' => 'No result found', + ], + ], + 'lookup' => [ + 'click-to-add' => 'Click to Add', + 'no-results' => 'No Results Found', + 'add-as-new' => 'Add as New', + 'search' => 'Search...', + ], + 'flash-group' => [ + 'success' => 'Success', + 'error' => 'Error', + 'warning' => 'Warning', + 'info' => 'Info', + ], + 'tiny-mce' => [ + 'http-error' => 'HTTP Error', + 'invalid-json' => 'Invalid JSON response from the server.', + 'upload-failed' => 'File upload failed. Please try again.', + ], + ], + 'quotes' => [ + 'index' => [ + 'title' => 'Quotes', + 'create-btn' => 'Create Quote', + 'create-success' => 'Quote created successfully.', + 'update-success' => 'Quote updated successfully.', + 'delete-success' => 'Quote deleted successfully.', + 'delete-failed' => 'Quote can not be deleted.', + 'datagrid' => [ + 'subject' => 'Subject', + 'sales-person' => 'Sales Person', + 'expired-at' => 'Expired At', + 'created-at' => 'Created At', + 'person' => 'Person', + 'subtotal' => 'Subtotal', + 'discount' => 'Discount', + 'tax' => 'Tax', + 'adjustment' => 'Adjustment', + 'grand-total' => 'Grand Total', + 'edit' => 'Edit', + 'delete' => 'Delete', + 'print' => 'Print', + ], + 'pdf' => [ + 'adjustment' => 'Adjustment', + 'amount' => 'Amount', + 'billing-address' => 'Billing Address', + 'date' => 'Date', + 'discount' => 'Discount', + 'description' => 'Description', + 'expired-at' => 'Expired At', + 'grand-total' => 'Grand Total', + 'person' => 'Person', + 'price' => 'Price', + 'product-name' => 'Product Name', + 'quantity' => 'Quantity', + 'quote-id' => 'Quote ID', + 'sales-person' => 'Sales Person', + 'shipping-address' => 'Shipping Address', + 'sku' => 'SKU', + 'sub-total' => 'Sub Total', + 'subject' => 'Subject', + 'tax' => 'Tax', + 'title' => 'Quote', + ], + ], + 'create' => [ + 'title' => 'Create Quote', + 'save-btn' => 'Save Quote', + 'quote-info' => 'Quote Information', + 'quote-info-info' => 'Put the basic information of the quote.', + 'address-info' => 'Address Information', + 'address-info-info' => 'Information about the address related to quote.', + 'same-as-billing' => 'Shipping address same as billing address', + 'quote-items' => 'Quote Items', + 'search-products' => 'Search Products', + 'link-to-lead' => 'Link to lead', + 'quote-item-info' => 'Add Product Request for this quote.', + 'quote-name' => 'Quote name', + 'quantity' => 'Quantity', + 'price' => 'Price', + 'discount' => 'Discount', + 'tax' => 'Tax', + 'total' => 'Total', + 'amount' => 'Amount', + 'add-item' => '+ Add Item', + 'sub-total' => 'Sub Total (:symbol)', + 'total-discount' => 'Discount (:symbol)', + 'total-tax' => 'Tax (:symbol)', + 'total-adjustment' => 'Adjustment (:symbol)', + 'grand-total' => 'Grand Total (:symbol)', + 'discount-amount' => 'Discount amount', + 'tax-amount' => 'Tax amount', + 'adjustment-amount' => 'Adjustment amount', + 'product-name' => 'Product Name', + 'action' => 'Action', + ], + 'edit' => [ + 'title' => 'Edit Quote', + 'save-btn' => 'Save Quote', + 'quote-info' => 'Quote Information', + 'quote-info-info' => 'Put the basic information of the quote.', + 'address-info' => 'Address Information', + 'address-info-info' => 'Information about the address related to quote.', + 'quote-items' => 'Quote Items', + 'link-to-lead' => 'Link to lead', + 'quote-item-info' => 'Add Product Request for this quote.', + 'quote-name' => 'Quote name', + 'quantity' => 'Quantity', + 'price' => 'Price', + 'search-products' => 'Search Products', + 'discount' => 'Discount', + 'tax' => 'Tax', + 'total' => 'Total', + 'amount' => 'Amount', + 'add-item' => '+ Add Item', + 'sub-total' => 'Sub Total (:symbol)', + 'total-discount' => 'Discount (:symbol)', + 'total-tax' => 'Tax (:symbol)', + 'total-adjustment' => 'Adjustment (:symbol)', + 'grand-total' => 'Grand Total (:symbol)', + 'discount-amount' => 'Discount amount', + 'tax-amount' => 'Tax amount', + 'adjustment-amount' => 'Adjustment amount', + 'product-name' => 'Product Name', + 'action' => 'Action', + ], + ], + 'contacts' => [ + 'persons' => [ + 'index' => [ + 'title' => 'Persons', + 'create-btn' => 'Create Person', + 'create-success' => 'Person created successfully.', + 'update-success' => 'Person updated successfully.', + 'all-delete-success' => 'All selected persons were deleted successfully.', + 'partial-delete-warning' => 'Some persons were deleted successfully. Others could not be deleted because they are linked to leads.', + 'none-delete-warning' => 'None of the selected persons could be deleted because they are linked to leads.', + 'no-selection' => 'No persons were selected for deletion.', + 'delete-failed' => 'Failed to delete selected persons.', + 'datagrid' => [ + 'contact-numbers' => 'Contact Numbers', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'emails' => 'Emails', + 'id' => 'ID', + 'view' => 'View', + 'name' => 'Name', + 'organization-name' => 'Organization Name', + 'tag-name' => 'Tag Name', + 'google-export-not-connected' => 'Please connect your Google account first from Settings > Other Settings > Google Contacts.', + 'google-export-started' => 'Export to Google Contacts started.', + ], + ], + 'view' => [ + 'title' => ':name', + 'about-person' => 'About Person', + 'about-organization' => 'About Organization', + 'activities' => [ + 'index' => [ + 'all' => 'All', + 'calls' => 'Calls', + 'meetings' => 'Meetings', + 'lunches' => 'Lunches', + 'files' => 'Files', + 'quotes' => 'Quotes', + 'notes' => 'Notes', + 'emails' => 'Emails', + 'by-user' => 'By :user', + 'scheduled-on' => 'Scheduled on', + 'location' => 'Location', + 'participants' => 'Participants', + 'mark-as-done' => 'Mark as Done', + 'delete' => 'Delete', + 'edit' => 'Edit', + ], + 'actions' => [ + 'mail' => [ + 'btn' => 'Mail', + 'title' => 'Compose Mail', + 'to' => 'To', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Subject', + 'send-btn' => 'Send', + 'message' => 'Message', + ], + 'file' => [ + 'btn' => 'File', + 'title' => 'Add File', + 'title-control' => 'Title', + 'name' => 'File Name', + 'description' => 'Description', + 'file' => 'File', + 'save-btn' => 'Save File', + ], + 'note' => [ + 'btn' => 'Note', + 'title' => 'Add Note', + 'comment' => 'Comment', + 'save-btn' => 'Save Note', + ], + 'activity' => [ + 'btn' => 'Activity', + 'title' => 'Add Activity', + 'title-control' => 'Title', + 'description' => 'Description', + 'schedule-from' => 'Schedule From', + 'schedule-to' => 'Schedule To', + 'location' => 'Location', + 'call' => 'Call', + 'meeting' => 'Meeting', + 'lunch' => 'Lunch', + 'save-btn' => 'Save Activity', + ], + ], + ], + 'tags' => [ + 'create-success' => 'Tag created successfully.', + 'destroy-success' => 'Tag deleted successfully.', + ], + ], + 'create' => [ + 'title' => 'Create Person', + 'save-btn' => 'Save Person', + ], + 'edit' => [ + 'title' => 'Edit Person', + 'save-btn' => 'Save Person', + ], + ], + 'organizations' => [ + 'index' => [ + 'title' => 'Organizations', + 'create-btn' => 'Create Organization', + 'create-success' => 'Organization created successfully.', + 'update-success' => 'Organization updated successfully.', + 'delete-success' => 'Organization deleted successfully.', + 'delete-failed' => 'Organization can not be deleted.', + 'datagrid' => [ + 'delete' => 'Delete', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + 'persons-count' => 'Person Count', + ], + ], + 'create' => [ + 'title' => 'Create Organization', + 'save-btn' => 'Save Organization', + ], + 'edit' => [ + 'title' => 'Edit Organization', + 'save-btn' => 'Save Organization', + ], + ], + ], + 'products' => [ + 'index' => [ + 'title' => 'Products', + 'create-btn' => 'Create Product', + 'create-success' => 'Product created successfully.', + 'update-success' => 'Product updated successfully.', + 'delete-success' => 'Product deleted successfully.', + 'delete-failed' => 'Product can not be deleted.', + 'datagrid' => [ + 'allocated' => 'Allocated', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'id' => 'ID', + 'in-stock' => 'In Stock', + 'name' => 'Name', + 'on-hand' => 'On Hand', + 'tag-name' => 'Tag Name', + 'price' => 'Price', + 'sku' => 'SKU', + 'view' => 'View', + ], + ], + 'create' => [ + 'save-btn' => 'Save Products', + 'title' => 'Create Products', + 'general' => 'General', + 'price' => 'Price', + ], + 'edit' => [ + 'title' => 'Edit Products', + 'save-btn' => 'Save Products', + 'general' => 'General', + 'price' => 'Price', + ], + 'view' => [ + 'sku' => 'SKU', + 'all' => 'All', + 'notes' => 'Notes', + 'files' => 'Files', + 'inventories' => 'Inventory', + 'change-logs' => 'Changelogs', + 'attributes' => [ + 'about-product' => 'About Product', + ], + 'inventory' => [ + 'warehouse' => 'Warehouse', + 'in-stock' => 'In Stock', + 'allocated' => 'Allocated', + 'on-hand' => 'On Hand', + 'actions' => 'Actions', + 'assign' => 'Assign', + 'add-warehouse' => 'Add Warehouse', + 'location' => 'Location', + 'add-more' => 'Add More', + 'save' => 'Save', + ], + ], + ], + 'settings' => [ + 'title' => 'Settings', + 'groups' => [ + 'index' => [ + 'create-btn' => 'Create Group', + 'title' => 'Groups', + 'create-success' => 'Group created successfully.', + 'update-success' => 'Group updated successfully.', + 'destroy-success' => 'Group deleted successfully.', + 'delete-failed' => 'Group can not be deleted.', + 'delete-failed-associated-users' => 'Group can not be deleted, as this is being used by users.', + 'datagrid' => [ + 'delete' => 'Delete', + 'description' => 'Description', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + ], + 'edit' => [ + 'title' => 'Edit Group', + ], + 'create' => [ + 'name' => 'Name', + 'title' => 'Create Group', + 'description' => 'Description', + 'save-btn' => 'Save Group', + ], + ], + ], + 'roles' => [ + 'index' => [ + 'being-used' => 'Role can not be deleted, as this is being used in admin user.', + 'create-btn' => 'Create Roles', + 'create-success' => 'Role created successfully.', + 'current-role-delete-error' => 'Can not delete role assigned to the current user.', + 'current-role-edit-error' => 'Can not edit the role assigned to your own account.', + 'delete-failed' => 'Role can not be deleted.', + 'delete-success' => 'Role deleted successfully.', + 'last-delete-error' => 'At least one role is required.', + 'settings' => 'Settings', + 'title' => 'Roles', + 'update-success' => 'Role updated successfully.', + 'user-define-error' => 'Can not delete system role.', + 'datagrid' => [ + 'all' => 'All', + 'custom' => 'Custom', + 'delete' => 'Delete', + 'description' => 'Description', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + 'permission-type' => 'Permission Type', + ], + ], + 'create' => [ + 'access-control' => 'Access Control', + 'all' => 'All', + 'back-btn' => 'Back', + 'custom' => 'Custom', + 'description' => 'Description', + 'general' => 'General', + 'name' => 'Name', + 'permissions' => 'Permissions', + 'save-btn' => 'Save Role', + 'title' => 'Create Role', + ], + 'edit' => [ + 'access-control' => 'Access Control', + 'all' => 'All', + 'back-btn' => 'Back', + 'custom' => 'Custom', + 'description' => 'Description', + 'general' => 'General', + 'name' => 'Name', + 'permissions' => 'Permissions', + 'save-btn' => 'Save Role', + 'title' => 'Edit Role', + ], + ], + 'types' => [ + 'index' => [ + 'create-btn' => 'Create Type', + 'create-success' => 'Type created successfully.', + 'delete-failed' => 'Type can not be deleted.', + 'delete-success' => 'Type deleted successfully.', + 'title' => 'Types', + 'update-success' => 'Type updated successfully.', + 'datagrid' => [ + 'delete' => 'Delete', + 'description' => 'Description', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + ], + 'create' => [ + 'name' => 'Name', + 'save-btn' => 'Save Type', + 'title' => 'Create Type', + ], + 'edit' => [ + 'title' => 'Edit Type', + ], + ], + ], + 'sources' => [ + 'index' => [ + 'title' => 'Sources', + 'create-btn' => 'Create Source', + 'create-success' => 'Source created successfully.', + 'delete-failed' => 'Source can not be deleted.', + 'delete-success' => 'Source deleted successfully.', + 'update-success' => 'Source updated successfully.', + 'delete-failed-associated-leads' => 'Source cannot be deleted because it is associated with existing leads. Please detach or update those leads before deletion.', + 'datagrid' => [ + 'delete' => 'Delete', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + ], + 'create' => [ + 'name' => 'Name', + 'save-btn' => 'Save Source', + 'title' => 'Create Source', + ], + 'edit' => [ + 'title' => 'Edit Source', + ], + ], + ], + 'workflows' => [ + 'index' => [ + 'title' => 'Workflows', + 'create-btn' => 'Create Workflow', + 'create-success' => 'Workflow created successfully.', + 'update-success' => 'Workflow updated successfully.', + 'delete-success' => 'Workflow deleted successfully.', + 'delete-failed' => 'Workflow can not be deleted.', + 'datagrid' => [ + 'delete' => 'Delete', + 'description' => 'Description', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + ], + ], + 'helpers' => [ + 'update-related-leads' => 'Update related leads', + 'send-email-to-sales-owner' => 'Send email to sales owner', + 'send-email-to-participants' => 'Send email to participants', + 'add-webhook' => 'Add Webhook', + 'update-lead' => 'Update Lead', + 'update-person' => 'Update Person', + 'send-email-to-person' => 'Send email to person', + 'add-tag' => 'Add Tag', + 'add-note-as-activity' => 'Add Note as Activity', + 'update-quote' => 'Update Quote', + ], + 'create' => [ + 'title' => 'Create Workflow', + 'event' => 'Event', + 'back-btn' => 'Back', + 'save-btn' => 'Save Workflow', + 'name' => 'Name', + 'basic-details' => 'Basic Details', + 'description' => 'Description', + 'actions' => 'Actions', + 'basic-details-info' => 'Put the basic information of the workflow.', + 'event-info' => 'An event triggers, checks, conditions, and performs predefined actions.', + 'conditions' => 'Conditions', + 'conditions-info' => 'Conditions are rules checking scenarios, triggered on specific occasions.', + 'actions-info' => 'An action not only reduces the workload but also make it quite easier for CRM automation', + 'value' => 'Value', + 'condition-type' => 'Condition Type', + 'all-condition-are-true' => 'All condition are true', + 'any-condition-are-true' => 'Any condition are true', + 'add-condition' => 'Add Condition', + 'add-action' => 'Add Action', + 'no-webhook-found' => 'No webhooks found.', + 'create-webhook' => 'Create a new webhook.', + 'yes' => 'Yes', + 'no' => 'No', + 'email' => 'Email', + 'is-equal-to' => 'Is equal to', + 'is-not-equal-to' => 'Is not equal to', + 'equals-or-greater-than' => 'Equals or greater than', + 'equals-or-less-than' => 'Equals or less than', + 'greater-than' => 'Greater than', + 'less-than' => 'Less than', + 'type' => 'Type', + 'contain' => 'Contain', + 'contains' => 'Contains', + 'does-not-contain' => 'Does not contain', + ], + 'edit' => [ + 'title' => 'Edit Workflow', + 'event' => 'Event', + 'back-btn' => 'Back', + 'save-btn' => 'Save Workflow', + 'name' => 'Name', + 'basic-details' => 'Basic Details', + 'description' => 'Description', + 'actions' => 'Actions', + 'type' => 'Type', + 'basic-details-info' => 'Put the basic information of the workflow.', + 'event-info' => 'An event triggers, checks, conditions, and performs predefined actions.', + 'conditions' => 'Conditions', + 'conditions-info' => 'Conditions are rules checking scenarios, triggered on specific occasions.', + 'actions-info' => 'An action not only reduces the workload but also make it quite easier for CRM automation', + 'value' => 'Value', + 'condition-type' => 'Condition Type', + 'all-condition-are-true' => 'All condition are true', + 'any-condition-are-true' => 'Any condition are true', + 'add-condition' => 'Add Condition', + 'add-action' => 'Add Action', + 'yes' => 'Yes', + 'no' => 'No', + 'email' => 'Email', + 'is-equal-to' => 'Is equal to', + 'is-not-equal-to' => 'Is not equal to', + 'equals-or-greater-than' => 'Equals or greater than', + 'equals-or-less-than' => 'Equals or less than', + 'greater-than' => 'Greater than', + 'less-than' => 'Less than', + 'contain' => 'Contain', + 'contains' => 'Contains', + 'does-not-contain' => 'Does not contain', + ], + ], + 'google-contacts' => [ + 'index' => [ + 'title' => 'Google Contacts', + 'not-connected-info' => 'Connect a Google account to export CRM contacts to Google Contacts.', + 'connected-info' => 'Connected as :email.', + 'connect-btn' => 'Connect Google Account', + 'disconnect-btn' => 'Disconnect', + 'connect-success' => 'Google account connected successfully.', + 'connect-failed' => 'Unable to connect your Google account. Please try again.', + 'not-configured' => 'Google OAuth is not configured. Please set the required environment variables first.', + 'disconnect-success' => 'Google account disconnected successfully.', + ], + ], + + 'webforms' => [ + 'index' => [ + 'title' => 'Webforms', + 'create-btn' => 'Create Webform', + 'create-success' => 'Webform created successfully.', + 'update-success' => 'Webform updated successfully.', + 'delete-success' => 'Webform deleted successfully.', + 'delete-failed' => 'Webform can not be deleted.', + 'datagrid' => [ + 'id' => 'ID', + 'title' => 'Title', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], + ], + 'create' => [ + 'title' => 'Create Webform', + 'add-attribute-btn' => 'Add Attribute Button', + 'attribute-label-color' => 'Attribute Label Color', + 'attributes' => 'Attributes', + 'attributes-info' => 'Add custom attributes to the form.', + 'background-color' => 'Background Color', + 'create-lead' => 'Create Lead', + 'customize-webform' => 'Customize Webform', + 'customize-webform-info' => 'Customize your web form with element colors of your choosing.', + 'description' => 'Description', + 'display-custom-message' => 'Display custom message', + 'form-background-color' => 'Form Background Color', + 'form-submit-btn-color' => 'Form Submit Button Color', + 'form-submit-button-color' => 'Form Submit Button Color', + 'form-title-color' => 'Form Title Color', + 'general' => 'General', + 'leads' => 'Leads', + 'person' => 'Person', + 'pipeline' => 'Pipeline', + 'save-btn' => 'Save Webform', + 'submit-button-label' => 'Submit Button Label', + 'submit-success-action' => 'Submit Success Action', + 'redirect-to-url' => 'Redirect To Url', + 'choose-value' => 'Choose Value', + 'select-file' => 'Select File', + 'select-image' => 'Select Image', + 'enter-value' => 'Enter Value', + ], + 'edit' => [ + 'add-attribute-btn' => 'Add Attribute Button', + 'attribute-label-color' => 'Attribute Label Color', + 'attributes' => 'Attributes', + 'attributes-info' => 'Add custom attributes to the form.', + 'background-color' => 'Background Color', + 'choose-value' => 'Choose Value', + 'code-snippet' => 'Code Snippet', + 'copied' => 'Copied', + 'copy' => 'Copy', + 'create-lead' => 'Create Lead', + 'customize-webform' => 'Customize Webform', + 'customize-webform-info' => 'Customize your web form with element colors of your choosing.', + 'description' => 'Description', + 'display-custom-message' => 'Display custom message', + 'embed' => 'Embed', + 'enter-value' => 'Enter Value', + 'form-background-color' => 'Form Background Color', + 'form-submit-btn-color' => 'Form Submit Button Color', + 'form-submit-button-color' => 'Form Submit Button Color', + 'form-title-color' => 'Form Title Color', + 'general' => 'General', + 'leads' => 'Leads', + 'person' => 'Person', + 'pipeline' => 'Pipeline', + 'preview' => 'Preview', + 'public-url' => 'Public URL', + 'redirect-to-url' => 'Redirect To URL', + 'save-btn' => 'Save Webform', + 'select-file' => 'Select File', + 'select-image' => 'Select Image', + 'submit-button-label' => 'Submit Button Label', + 'submit-success-action' => 'Submit Success Action', + 'title' => 'Edit Webform', + ], + ], + 'email-template' => [ + 'index' => [ + 'create-btn' => 'Create Email Template', + 'title' => 'Email Templates', + 'create-success' => 'Email Template created successfully.', + 'update-success' => 'Email Template updated successfully.', + 'delete-success' => 'Email Template deleted successfully.', + 'delete-failed' => 'Email Template can not be deleted.', + 'datagrid' => [ + 'delete' => 'Delete', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + 'subject' => 'Subject', + ], + ], + 'create' => [ + 'title' => 'Create Email Template', + 'save-btn' => 'Save Email Template', + 'email-template' => 'Email Template', + 'subject' => 'Subject', + 'content' => 'Content', + 'subject-placeholders' => 'Subject Placeholders', + 'general' => 'General', + 'name' => 'Name', + ], + 'edit' => [ + 'title' => 'Edit Email Template', + 'save-btn' => 'Save Email Template', + 'email-template' => 'Email Template', + 'subject' => 'Subject', + 'content' => 'Content', + 'subject-placeholders' => 'Subject Placeholders', + 'general' => 'General', + 'name' => 'Name', + ], + ], + 'marketing' => [ + 'events' => [ + 'index' => [ + 'create-btn' => 'Create Event', + 'title' => 'Events', + 'create-success' => 'Event created successfully.', + 'update-success' => 'Event updated successfully.', + 'delete-success' => 'Event deleted successfully.', + 'delete-failed' => 'Event can not be deleted.', + 'mass-delete-success' => 'Events deleted successfully', + 'delete-failed-associated-campaigns' => 'Event can not be deleted as it is associated with existing campaigns. Please detach or update those campaigns before deletion.', + 'datagrid' => [ + 'delete' => 'Delete', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + 'description' => 'Description', + 'date' => 'Date', + ], + 'create' => [ + 'title' => 'Create Event', + 'name' => 'Name', + 'date' => 'Date', + 'description' => 'Description', + 'save-btn' => 'Save Event', + ], + 'edit' => [ + 'title' => 'Edit Event', + ], + ], + ], + 'campaigns' => [ + 'index' => [ + 'create-btn' => 'Create Campaigns', + 'title' => 'Campaigns', + 'create-success' => 'Campaign created successfully.', + 'update-success' => 'Campaign updated successfully.', + 'delete-success' => 'Campaign deleted successfully.', + 'delete-failed' => 'Campaign can not be deleted.', + 'mass-delete-success' => 'Campaigns deleted successfully.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Name', + 'subject' => 'Subject', + 'status' => 'Status', + 'active' => 'Active', + 'inactive' => 'Inactive', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], + 'create' => [ + 'title' => 'Create Campaign', + 'name' => 'Name', + 'type' => 'Type', + 'subject' => 'Subject', + 'event' => 'Event', + 'email-template' => 'Email Template', + 'status' => 'Status', + ], + 'edit' => [ + 'title' => 'Edit Campaign', + ], + ], + ], + ], + 'tags' => [ + 'index' => [ + 'create-btn' => 'Create Tag', + 'title' => 'Tags', + 'create-success' => 'Tag created successfully.', + 'update-success' => 'Tag updated successfully.', + 'delete-success' => 'Tag deleted successfully.', + 'delete-failed' => 'Tag can not be deleted.', + 'datagrid' => [ + 'delete' => 'Delete', + 'edit' => 'Edit', + 'id' => 'ID', + 'name' => 'Name', + 'users' => 'Users', + 'created-at' => 'Created At', + ], + 'create' => [ + 'name' => 'Name', + 'save-btn' => 'Save Tag', + 'title' => 'Create Tag', + 'color' => 'Color', + ], + 'edit' => [ + 'title' => 'Edit Tag', + ], + ], + ], + 'users' => [ + 'index' => [ + 'create-btn' => 'Create User', + 'create-success' => 'User created successfully.', + 'delete-failed' => 'User can not be deleted.', + 'delete-success' => 'User deleted successfully.', + 'last-delete-error' => 'At least one user is required.', + 'mass-delete-failed' => 'Users can not be deleted.', + 'mass-delete-success' => 'Users deleted successfully.', + 'mass-update-failed' => 'Users can not be updated.', + 'mass-update-success' => 'Users updated successfully.', + 'title' => 'Users', + 'update-success' => 'User updated successfully.', + 'user-define-error' => 'Can not delete system user.', + 'active' => 'Active', + 'inactive' => 'Inactive', + 'datagrid' => [ + 'active' => 'Active', + 'created-at' => 'Created At', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'email' => 'Email', + 'group' => 'Associated Group', + 'id' => 'ID', + 'inactive' => 'Inactive', + 'name' => 'Name', + 'no-group' => 'No Group', + 'status' => 'Status', + 'update-status' => 'Update Status', + 'users' => 'Users', + ], + 'create' => [ + 'confirm-password' => 'Confirm Password', + 'email' => 'Email', + 'general' => 'General', + 'global' => 'Global', + 'group' => 'Group', + 'individual' => 'Individual', + 'name' => 'Name', + 'password' => 'Password', + 'permission' => 'Permission', + 'role' => 'Role', + 'save-btn' => 'Save User', + 'status' => 'Status', + 'title' => 'Create User', + 'view-permission' => 'View Permission', + 'select-at-lest-one-group' => 'Select at least one group', + ], + 'edit' => [ + 'title' => 'Edit User', + ], + ], + ], + 'pipelines' => [ + 'index' => [ + 'title' => 'Pipelines', + 'create-btn' => 'Create Pipeline', + 'create-success' => 'Pipeline created successfully.', + 'update-success' => 'Pipeline updated successfully.', + 'default-required' => 'At least one default pipeline is required.', + 'delete-success' => 'Pipeline deleted successfully.', + 'delete-failed' => 'Pipeline can not be deleted.', + 'default-delete-error' => 'Default pipeline can not be deleted.', + 'datagrid' => [ + 'delete' => 'Delete', + 'edit' => 'Edit', + 'id' => 'ID', + 'is-default' => 'Is Default', + 'name' => 'Name', + 'no' => 'No', + 'rotten-days' => 'Rotten Days', + 'yes' => 'Yes', + ], + ], + 'create' => [ + 'title' => 'Create Pipeline', + 'save-btn' => 'Save Pipeline', + 'name' => 'Name', + 'rotten-days' => 'Rotten Days', + 'mark-as-default' => 'Mark as Default', + 'general' => 'General', + 'probability' => 'Probability(%)', + 'new-stage' => 'New', + 'won-stage' => 'Won', + 'lost-stage' => 'Lost', + 'stage-btn' => 'Add Stage', + 'stages' => 'Stages', + 'duplicate-name' => 'The Name field cannot be duplicate', + 'delete-stage' => 'Delete Stage', + 'add-new-stages' => 'Add New Stages', + 'add-stage-info' => 'Add new stage for your Pipeline', + 'newly-added' => 'Newly Added', + 'stage-delete-success' => 'Stage Deleted Successfully', + ], + 'edit' => [ + 'title' => 'Edit Pipeline', + 'save-btn' => 'Save Pipeline', + 'name' => 'Name', + 'rotten-days' => 'Rotten Days', + 'mark-as-default' => 'Mark as Default', + 'general' => 'General', + 'probability' => 'Probability(%)', + 'new-stage' => 'New', + 'won-stage' => 'Won', + 'lost-stage' => 'Lost', + 'stage-btn' => 'Add Stage', + 'stages' => 'Stages', + 'duplicate-name' => 'The Name field cannot be duplicate', + 'delete-stage' => 'Delete Stage', + 'add-new-stages' => 'Add New Stages', + 'add-stage-info' => 'Add new stage for your Pipeline', + 'stage-delete-success' => 'Stage Deleted Successfully', + ], + ], + 'webhooks' => [ + 'index' => [ + 'title' => 'Webhooks', + 'create-btn' => 'Create Webhook', + 'create-success' => 'Webhook created successfully.', + 'update-success' => 'Webhook updated successfully.', + 'delete-success' => 'Webhook deleted successfully.', + 'delete-failed' => 'Webhook can not be deleted.', + 'invalid-endpoint' => 'The webhook endpoint URL must be a public HTTP or HTTPS address.', + 'datagrid' => [ + 'id' => 'ID', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'name' => 'Name', + 'entity-type' => 'Entity Type', + 'end-point' => 'End Point', + ], + ], + 'create' => [ + 'title' => 'Create Webhook', + 'save-btn' => 'Save Webhook', + 'info' => 'Enter the details of webhooks', + 'url-and-parameters' => 'URL And Parameters', + 'method' => 'Method', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'Url Endpoint', + 'parameters' => 'Parameters', + 'add-new-parameter' => 'Add New Parameter', + 'url-preview' => 'Url Preview:', + 'headers' => 'Headers', + 'add-new-header' => 'Add New Header', + 'body' => 'Body', + 'default' => 'Default', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Key and Value', + 'add-new-payload' => 'Add new payload', + 'raw' => 'Raw', + 'general' => 'General', + 'name' => 'Name', + 'entity-type' => 'Entity Type', + 'insert-placeholder' => 'Insert Placeholder', + 'description' => 'Description', + 'json' => 'Json', + 'text' => 'Text', + ], + 'edit' => [ + 'title' => 'Edit Webhook', + 'edit-btn' => 'Save Webhook', + 'save-btn' => 'Save Webhook', + 'info' => 'Enter the details of webhooks', + 'url-and-parameters' => 'URL And Parameters', + 'method' => 'Method', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'Url Endpoint', + 'parameters' => 'Parameters', + 'add-new-parameter' => 'Add New Parameter', + 'url-preview' => 'Url Preview:', + 'headers' => 'Headers', + 'add-new-header' => 'Add New Header', + 'body' => 'Body', + 'default' => 'Default', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Key and Value', + 'add-new-payload' => 'Add new payload', + 'raw' => 'Raw', + 'general' => 'General', + 'name' => 'Name', + 'entity-type' => 'Entity Type', + 'insert-placeholder' => 'Insert Placeholder', + 'description' => 'Description', + 'json' => 'Json', + 'text' => 'Text', + ], + ], + 'warehouses' => [ + 'index' => [ + 'title' => 'Warehouses', + 'create-btn' => 'Create Warehouse', + 'create-success' => 'Warehouse created successfully.', + 'name-exists' => 'Warehouse name already exists.', + 'update-success' => 'Warehouse updated successfully.', + 'delete-success' => 'Warehouse deleted successfully.', + 'delete-failed' => 'Warehouse can not be deleted.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Name', + 'contact-name' => 'Contact Name', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'view' => 'View', + 'created-at' => 'Created At', + 'products' => 'Products', + 'contact-emails' => 'Contact Emails', + 'contact-numbers' => 'Contact Numbers', + ], + ], + 'create' => [ + 'title' => 'Create Warehouse', + 'save-btn' => 'Save Warehouse', + 'contact-info' => 'Contact Information', + ], + 'edit' => [ + 'title' => 'Edit Warehouse', + 'save-btn' => 'Save Warehouse', + 'contact-info' => 'Contact Information', + ], + 'view' => [ + 'all' => 'All', + 'notes' => 'Notes', + 'files' => 'Files', + 'location' => 'Location', + 'change-logs' => 'Changelogs', + 'locations' => [ + 'action' => 'Action', + 'add-location' => 'Add Location', + 'create-success' => 'Location created successfully.', + 'delete' => 'Delete', + 'delete-failed' => 'Location can not be deleted.', + 'delete-success' => 'Location deleted successfully.', + 'name' => 'Name', + 'save-btn' => 'Save', + ], + 'general-information' => [ + 'title' => 'General Information', + ], + 'contact-information' => [ + 'title' => 'Contact Information', + ], + ], + ], + 'attributes' => [ + 'index' => [ + 'title' => 'Attributes', + 'create-btn' => 'Create Attribute', + 'create-success' => 'Attribute created successfully.', + 'update-success' => 'Attribute updated successfully.', + 'delete-success' => 'Attribute deleted successfully.', + 'delete-failed' => 'Attribute can not be deleted.', + 'user-define-error' => 'Can not delete system attribute.', + 'mass-delete-failed' => 'System attributes can not be deleted.', + 'datagrid' => [ + 'yes' => 'Yes', + 'no' => 'No', + 'id' => 'ID', + 'code' => 'Code', + 'name' => 'Name', + 'entity-type' => 'Entity Type', + 'type' => 'Type', + 'is-default' => 'Is Default', + 'edit' => 'Edit', + 'delete' => 'Delete', + 'quick-add' => 'Quick Add', + 'entity-types' => [ + 'leads' => 'Leads', + 'organizations' => 'Organizations', + 'persons' => 'Persons', + 'products' => 'Products', + 'quotes' => 'Quotes', + 'warehouses' => 'Warehouses', + ], + 'types' => [ + 'text' => 'Text', + 'textarea' => 'Textarea', + 'price' => 'Price', + 'boolean' => 'Boolean', + 'select' => 'Select', + 'multiselect' => 'Multiselect', + 'checkbox' => 'Checkbox', + 'email' => 'Email', + 'address' => 'Address', + 'phone' => 'Phone', + 'lookup' => 'Lookup', + 'datetime' => 'Datetime', + 'date' => 'Date', + 'image' => 'Image', + 'file' => 'File', + ], + ], + ], + 'create' => [ + 'title' => 'Create Attribute', + 'save-btn' => 'Save Attribute', + 'code' => 'Code', + 'name' => 'Name', + 'quick_add' => 'Quick Add', + 'entity-type' => 'Entity Type', + 'type' => 'Type', + 'validations' => 'Validations', + 'is-required' => 'Is Required', + 'input-validation' => 'Input Validation', + 'is-unique' => 'Is Unique', + 'labels' => 'Labels', + 'general' => 'General', + 'none' => 'None', + 'numeric' => 'Numeric', + 'decimal' => 'Decimal', + 'url' => 'Url', + 'options' => 'Options', + 'option-type' => 'Option Type', + 'lookup-type' => 'Lookup Type', + 'add-option' => 'Add Option', + 'save-option' => 'Save Option', + 'option-name' => 'Option Name', + 'add-attribute-options' => 'Add Attribute Options', + 'text' => 'Text', + 'textarea' => 'Textarea', + 'price' => 'Price', + 'boolean' => 'Boolean', + 'select' => 'Select', + 'multiselect' => 'Multiselect', + 'email' => 'Email', + 'address' => 'Address', + 'phone' => 'Phone', + 'datetime' => 'Datetime', + 'date' => 'Date', + 'image' => 'Image', + 'file' => 'File', + 'lookup' => 'Lookup', + 'entity_type' => 'Entity type', + 'checkbox' => 'Checkbox', + 'is_required' => 'Is Required', + 'is_unique' => 'Is Unique', + 'actions' => 'Actions', + ], + 'edit' => [ + 'actions' => 'Actions', + 'add-attribute-options' => 'Add Attribute Options', + 'add-option' => 'Add Option', + 'address' => 'Address', + 'boolean' => 'Boolean', + 'checkbox' => 'Checkbox', + 'code' => 'Code', + 'date' => 'Date', + 'datetime' => 'Datetime', + 'decimal' => 'Decimal', + 'email' => 'Email', + 'entity-type' => 'Entity Type', + 'entity_type' => 'Entity type', + 'file' => 'File', + 'general' => 'General', + 'image' => 'Image', + 'input-validation' => 'Input Validation', + 'is-required' => 'Is Required', + 'is-unique' => 'Is Unique', + 'is_required' => 'Is Required', + 'is_unique' => 'Is Unique', + 'labels' => 'Labels', + 'lookup' => 'Lookup', + 'lookup-type' => 'Lookup Type', + 'multiselect' => 'Multiselect', + 'name' => 'Name', + 'quick_add' => 'Quick Add', + 'none' => 'None', + 'numeric' => 'Numeric', + 'option-deleted' => 'Attribute Option is deleted successfully', + 'option-name' => 'Option Name', + 'option-type' => 'Option Type', + 'options' => 'Options', + 'phone' => 'Phone', + 'price' => 'Price', + 'save-btn' => 'Save Attribute', + 'save-option' => 'Save Option', + 'select' => 'Select', + 'text' => 'Text', + 'textarea' => 'Textarea', + 'title' => 'Edit Attribute', + 'type' => 'Type', + 'url' => 'Url', + 'validations' => 'Validations', + ], + ], + 'data-transfer' => [ + 'importers' => [ + 'persons' => [ + 'title' => 'Persons', + 'validation' => [ + 'errors' => [ + 'duplicate-email' => 'Email : \'%s\' is found more than once in the import file.', + 'duplicate-phone' => 'Phone : \'%s\' is found more than once in the import file.', + 'email-not-found' => 'Email : \'%s\' not found in the system.', + 'invalid-date' => 'Date : \'%s\' is not a valid date.', + ], + ], + ], + 'products' => [ + 'title' => 'Products', + 'validation' => [ + 'errors' => [ + 'sku-not-found' => 'Product with specified SKU not found', + ], + ], + ], + 'leads' => [ + 'title' => 'Leads', + 'validation' => [ + 'errors' => [ + 'id-not-found' => 'ID : \'%s\' not found in the system.', + ], + ], + ], + ], + 'validation' => [ + 'errors' => [ + 'column-empty-headers' => 'Columns number "%s" have empty headers.', + 'column-name-invalid' => 'Invalid column names: "%s".', + 'column-not-found' => 'Required columns not found: %s.', + 'column-numbers' => 'Number of columns does not correspond to the number of rows in the header.', + 'invalid-attribute' => 'Header contains invalid attribute(s): "%s".', + 'system' => 'An unexpected system error occurred.', + 'wrong-quotes' => 'Curly quotes used instead of straight quotes.', + 'already-exists' => 'The :attribute already exists.', + ], + ], + 'imports' => [ + 'create' => [ + 'action' => 'Action', + 'allowed-errors' => 'Allowed Errors', + 'back-btn' => 'Back', + 'create-update' => 'Create/Update', + 'delete' => 'Delete', + 'download-sample' => 'Download Sample', + 'field-separator' => 'Field Separator', + 'file' => 'File', + 'general' => 'General', + 'images-directory' => 'Images Directory Path', + 'process-in-queue' => 'Process In Queue', + 'results' => 'Results', + 'save-btn' => 'Save Import', + 'settings' => 'Settings', + 'skip-errors' => 'Skip Errors', + 'stop-on-errors' => 'Stop on Errors', + 'title' => 'Create Import', + 'type' => 'Type', + 'validation-strategy' => 'Validation Strategy', + ], + 'edit' => [ + 'action' => 'Action', + 'allowed-errors' => 'Allowed Errors', + 'back-btn' => 'Back', + 'create-update' => 'Create/Update', + 'delete' => 'Delete', + 'download-sample' => 'Download Sample', + 'field-separator' => 'Field Separator', + 'file' => 'File', + 'general' => 'General', + 'images-directory' => 'Images Directory Path', + 'process-in-queue' => 'Process In Queue', + 'results' => 'Results', + 'save-btn' => 'Save Import', + 'settings' => 'Settings', + 'skip-errors' => 'Skip Errors', + 'stop-on-errors' => 'Stop on Errors', + 'title' => 'Edit Import', + 'type' => 'Type', + 'validation-strategy' => 'Validation Strategy', + ], + 'index' => [ + 'button-title' => 'Create Import', + 'title' => 'Imports', + 'datagrid' => [ + 'actions' => 'Actions', + 'completed-at' => 'Completed At', + 'created' => 'Created', + 'delete' => 'Delete', + 'deleted' => 'Deleted', + 'edit' => 'Edit', + 'error-file' => 'Error File', + 'id' => 'ID', + 'started-at' => 'Started At', + 'state' => 'State', + 'summary' => 'Summary', + 'type' => 'Type', + 'updated' => 'Updated', + 'uploaded-file' => 'Uploaded File', + ], + ], + 'import' => [ + 'back-btn' => 'Back', + 'completed-batches' => 'Total Batches Completed:', + 'download-error-report' => 'Download Full Report', + 'edit-btn' => 'Edit', + 'imported-info' => 'Congratulations! Your import was successful.', + 'importing-info' => 'Import In Process', + 'importing-queue-hint' => 'If this import stays at 0%, the queue worker may not be running. Ask your administrator to run \'php artisan queue:work\', or turn off \'Process In Queue\' and import again.', + 'indexing-info' => 'Resources Indexing (Price, Inventory and Elastic Search) In Progress', + 'linking-info' => 'Resources Linking In Progress', + 'progress' => 'Progress:', + 'title' => 'Import', + 'total-batches' => 'Total Batches:', + 'total-created' => 'Total Records Created:', + 'total-deleted' => 'Total Records Deleted:', + 'total-errors' => 'Total Errors:', + 'total-invalid-rows' => 'Total Invalid Rows:', + 'total-rows-processed' => 'Total Rows Processed:', + 'total-updated' => 'Total Records Updated:', + 'validate' => 'Validate', + 'validate-info' => 'Click on Validate Data to check your import.', + 'validating-info' => 'The data started reading and Validating', + 'validation-failed-info' => 'Your import is invalid. Please fix the following errors and try again.', + 'validation-success-info' => 'Your import is valid. Click on Import to start the import process.', + ], + 'create-success' => 'Import created successfully.', + 'delete-failed' => 'Import deletion failed unexpectedly.', + 'delete-success' => 'Import deleted successfully.', + 'not-valid' => 'Import is invalid', + 'nothing-to-import' => 'There are no resources to import.', + 'queue-worker-not-running' => 'Queue worker is not running. Run php artisan queue:work to process this import, or turn off Process In Queue and try again.', + 'setup-queue-error' => 'Please change your queue driver to "database" or "redis" to start the import process.', + 'update-success' => 'Import updated successfully.', + ], + ], + ], + 'activities' => [ + 'index' => [ + 'title' => 'Activities', + 'datagrid' => [ + 'comment' => 'Comment', + 'created_at' => 'Created At', + 'created_by' => 'Created By', + 'edit' => 'Edit', + 'id' => 'ID', + 'done' => 'Is Done', + 'not-done' => 'Not Done', + 'lead' => 'Lead', + 'mass-delete' => 'Mass Delete', + 'mass-update' => 'Mass Update', + 'schedule-from' => 'Schedule From', + 'schedule-to' => 'Schedule To', + 'schedule_from' => 'Schedule From', + 'schedule_to' => 'Schedule To', + 'title' => 'Title', + 'is_done' => 'Is Done', + 'type' => 'Type', + 'update' => 'Update', + 'call' => 'Call', + 'meeting' => 'Meeting', + 'lunch' => 'Lunch', + ], + ], + 'edit' => [ + 'title' => 'Edit Activity', + 'back-btn' => 'Back', + 'save-btn' => 'Save Activity', + 'type' => 'Activity Type', + 'call' => 'Call', + 'meeting' => 'Meeting', + 'lunch' => 'Lunch', + 'schedule_to' => 'Schedule To', + 'schedule_from' => 'Schedule From', + 'location' => 'Location', + 'comment' => 'Comment', + 'lead' => 'Lead', + 'participants' => 'Participants', + 'general' => 'General', + 'persons' => 'Persons', + 'no-result-found' => 'Records not found.', + 'users' => 'Users', + ], + 'updated' => 'Updated :attribute', + 'created' => 'Created', + 'duration-overlapping' => 'Participants have another meeting at this time. Do you want to continue?', + 'create-success' => 'Activity created successfully.', + 'update-success' => 'Activity updated successfully.', + 'overlapping-error' => 'Participants have another meeting at this time.', + 'destroy-success' => 'Activity deleted successfully.', + 'delete-failed' => 'Activity can not be deleted.', + 'mass-update-success' => 'Activities updated successfully.', + 'mass-destroy-success' => 'Activities deleted successfully.', + 'mass-delete-failed' => 'Activities can not be deleted.', + ], + 'mail' => [ + 'index' => [ + 'compose' => 'Compose', + 'draft' => 'Draft', + 'inbox' => 'Inbox', + 'outbox' => 'Outbox', + 'sent' => 'Sent', + 'trash' => 'Trash', + 'compose-mail-btn' => 'Compose Mail', + 'btn' => 'Mail', + 'mail' => [ + 'title' => 'Compose Mail', + 'to' => 'To', + 'enter-emails' => 'Press enter to add emails', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Subject', + 'send-btn' => 'Send', + 'message' => 'Message', + 'draft' => 'Draft', + ], + 'datagrid' => [ + 'id' => 'ID', + 'from' => 'From', + 'to' => 'To', + 'subject' => 'Subject', + 'tags' => 'Tags', + 'content' => 'Content', + 'attachments' => 'Attachments', + 'date' => 'Date', + 'move-to-inbox' => 'Moved To Inbox', + 'move-to-trash' => 'Moved To Trash', + 'edit' => 'Edit', + 'view' => 'View', + 'delete' => 'Delete', + ], + ], + 'create-success' => 'Email sent successfully.', + 'update-success' => 'Email updated successfully.', + 'mass-update-success' => 'Emails updated successfully.', + 'delete-success' => 'Email deleted successfully.', + 'delete-failed' => 'Email can not be deleted.', + 'invalid-route' => 'Invalid route for mail.', + 'unauthorized' => 'This action is unauthorized.', + 'view' => [ + 'title' => 'Mails', + 'subject' => ':subject', + 'link-mail' => 'Link Mail', + 'to' => 'To', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'reply' => 'Reply', + 'reply-all' => 'Reply All', + 'forward' => 'Forward', + 'delete' => 'Delete', + 'enter-mails' => 'Enter email id', + 'rotten-days' => 'Lead is rotten for :days days', + 'search-an-existing-lead' => 'Search an existing lead', + 'search-an-existing-contact' => 'Search an existing contact', + 'message' => 'Message', + 'add-attachments' => 'Add Attachments', + 'discard' => 'Discard', + 'send' => 'Send', + 'no-result-found' => 'No Results found', + 'add-new-contact' => 'Add New Contact', + 'description' => 'Description', + 'search' => 'Search...', + 'add-new-lead' => 'Add New Lead', + 'create-new-contact' => 'Create New Contact', + 'save-contact' => 'Save Contact', + 'create-lead' => 'Create Lead', + 'linked-contact' => 'Linked Contact', + 'link-to-contact' => 'Link To Contact', + 'link-to-lead' => 'Link To Lead', + 'linked-lead' => 'Linked Lead', + 'lead-details' => 'Lead Details', + 'contact-person' => 'Contact Person', + 'product' => 'Product', + 'tags' => [ + 'create-success' => 'Tag created successfully.', + 'destroy-success' => 'Tag deleted successfully.', + ], + ], + ], + 'common' => [ + 'custom-attributes' => [ + 'add-more' => 'Add More', + 'address' => 'Address', + 'city' => 'City', + 'contact' => 'Contact Numbers', + 'country' => 'Country', + 'email' => 'Email', + 'home' => 'Home', + 'postcode' => 'Postcode', + 'save' => 'Save', + 'select' => 'Select', + 'select-country' => 'Select Country', + 'select-state' => 'Select State', + 'state' => 'State', + 'update-contact-title' => 'Update Contact Numbers', + 'update-emails-title' => 'Update Contact Emails', + 'work' => 'Work', + ], + ], + 'leads' => [ + 'create-success' => 'Lead created successfully.', + 'update-success' => 'Leads updated successfully.', + 'update-failed' => 'Leads can not be deleted.', + 'destroy-success' => 'Lead deleted successfully.', + 'destroy-failed' => 'Lead can not be deleted.', + 'file' => [ + 'data-not-found' => 'Data not found.', + 'empty-content' => 'PDF content is empty or could not be extracted.', + 'failed-extract' => 'Failed to extract text from file.', + 'insufficient-info' => 'Due to insufficient data, we are unable to process your request at the moment.', + 'invalid-base64' => 'Invalid base64 format.', + 'invalid-format' => 'Invalid JSON format.', + 'invalid-response' => 'Invalid AI response format.', + 'missing-api-key' => 'Missing API key or model configuration.', + 'not-found' => 'File not found.', + 'recursive-call' => 'Recursive call detected.', + 'text-generation-failed' => 'Text extraction failed. The file might be empty or unreadable.', + ], + 'index' => [ + 'title' => 'Leads', + 'create-btn' => 'Create Lead', + 'datagrid' => [ + 'id' => 'ID', + 'sales-person' => 'Sales Person', + 'subject' => 'Subject', + 'source' => 'Source', + 'lead-value' => 'Estimated Lead Value', + 'lead-type' => 'Lead Type', + 'tag-name' => 'Tag Name', + 'contact-person' => 'Contact Person', + 'stage' => 'Stage', + 'rotten-lead' => 'Rotten Lead', + 'date-to' => 'Date To', + 'created-at' => 'Created At', + 'no' => 'No', + 'yes' => 'Yes', + 'delete' => 'Delete', + 'mass-delete' => 'Mass Delete', + 'mass-update' => 'Mass Update', + ], + 'kanban' => [ + 'rotten-days' => 'Lead is rotten for :days days', + 'empty-list' => 'Your Leads List is Empty', + 'empty-list-description' => 'Create a lead to organize your goals.', + 'create-lead-btn' => 'Create Lead', + 'columns' => [ + 'contact-person' => 'Contact Person', + 'id' => 'ID', + 'lead-type' => 'Lead Type', + 'lead-value' => 'Estimated Lead Value', + 'sales-person' => 'Sales Person', + 'source' => 'Source', + 'title' => 'Title', + 'tags' => 'Tags', + 'expected-close-date' => 'Expected Close Date', + 'date-to' => 'Date To', + 'created-at' => 'Created At', + ], + 'toolbar' => [ + 'search' => [ + 'title' => 'Search by Title', + ], + 'filters' => [ + 'apply-filters' => 'Apply Filters', + 'clear-all' => 'Clear All', + 'filter' => 'Filter', + 'filters' => 'Filters', + 'from' => 'From', + 'select' => 'Select', + 'to' => 'To', + ], + 'card-settings' => [ + 'title' => 'Card Settings', + 'contact-person' => 'Contact Person', + 'rotten-indicator' => 'Rotten Indicator', + 'sales-person' => 'Sales Person', + 'estimated-lead-value' => 'Estimated Lead Value', + 'source' => 'Source', + 'lead-type' => 'Lead Type', + 'tags' => 'Tags', + ], + ], + 'stages' => [ + 'won' => 'Won', + 'lost' => 'Lost', + 'need-more-info' => 'Need More Details', + 'closed-at' => 'Closed At', + 'won-value' => 'Won Value', + 'lost-reason' => 'Lost Reason', + 'save-btn' => 'Save', + ], + ], + 'view-switcher' => [ + 'all-pipelines' => 'All Pipelines', + 'create-new-pipeline' => 'Create New Pipeline', + ], + 'upload' => [ + 'create-lead' => 'Create Lead Using AI', + 'file' => 'File Upload', + 'file-info' => 'Only pdf,bmp,jpg,jpeg,png format files are accepted.', + 'file-required' => 'Please select at least one valid file to proceed.', + 'save-btn' => 'Save', + 'upload-file' => 'Upload File', + ], + ], + 'create' => [ + 'title' => 'Create Lead', + 'save-btn' => 'Save', + 'details' => 'Details', + 'details-info' => 'Put The Basic Information of the Lead', + 'contact-person' => 'Contact Person', + 'contact-info' => 'Information About the Contact Person', + 'products' => 'Products', + 'products-info' => 'Information About the Products', + ], + 'edit' => [ + 'title' => 'Edit Lead', + 'save-btn' => 'Save', + 'details' => 'Details', + 'details-info' => 'Put The Basic Information of the Lead', + 'contact-person' => 'Contact Person', + 'contact-info' => 'Information About the Contact Person', + 'products' => 'Products', + 'products-info' => 'Information About the Products', + ], + 'common' => [ + 'contact' => [ + 'name' => 'Name', + 'email' => 'Email', + 'name-search-placeholder' => 'Search by name, email and number', + 'contact-number' => 'Contact Number', + 'organization' => 'Organization', + ], + 'products' => [ + 'product-name' => 'Product Name', + 'quantity' => 'Quantity', + 'price' => 'Price', + 'amount' => 'Amount', + 'action' => 'Action', + 'add-more' => 'Add More', + 'total' => 'Total', + ], + ], + 'view' => [ + 'title' => 'Lead: :title', + 'rotten-days' => ':days Days', + 'tabs' => [ + 'description' => 'Description', + 'products' => 'Products', + 'quotes' => 'Quotes', + ], + 'attributes' => [ + 'title' => 'About Lead', + ], + 'quotes' => [ + 'id' => 'ID', + 'subject' => 'Subject', + 'expired-at' => 'Expired At', + 'sub-total' => 'Sub Total', + 'discount' => 'Discount', + 'tax' => 'Tax', + 'adjustment' => 'Adjustment', + 'grand-total' => 'Grand Total', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'download' => 'Download', + 'destroy-success' => 'Quote deleted successfully.', + 'empty-title' => 'No Quotes Found', + 'empty-info' => 'No Quotes Found for this Lead', + 'add-btn' => 'Add Quote', + 'mail' => 'Mail', + 'quote-not-found' => 'Quote not found for this lead.', + 'person-email-unavailable' => 'Lead person email is not available.', + 'mail-subject' => 'Quote: :subject', + 'mail-body' => 'Please find the attached quote PDF. Mail Generated by System.', + 'mail-send-failed' => 'Unable to send quote email.', + ], + 'products' => [ + 'product-name' => 'Product Name', + 'quantity' => 'Quantity', + 'price' => 'Price', + 'amount' => 'Amount', + 'action' => 'Action', + 'add-more' => 'Add More', + 'total' => 'Total', + 'empty-title' => 'No Products Found', + 'empty-info' => 'No Products Found for this Lead', + 'add-product' => 'Add Product', + ], + 'persons' => [ + 'title' => 'About Persons', + 'job-title' => ':job_title at :organization', + 'no-person' => 'No contact person is allocated to this lead.', + 'attach-btn' => 'Attach Contact Person', + 'change-btn' => 'Change', + 'change-title' => 'Change Contact Person', + 'save-btn' => 'Save', + 'cancel-btn' => 'Cancel', + ], + 'stages' => [ + 'won' => 'Won', + 'lost' => 'Lost', + 'need-more-info' => 'Need More Details', + 'closed-at' => 'Closed At', + 'won-value' => 'Won Value', + 'lost-reason' => 'Lost Reason', + 'save-btn' => 'Save', + ], + 'tags' => [ + 'create-success' => 'Tag created successfully.', + 'destroy-success' => 'Tag deleted successfully.', + ], + ], + ], + 'configuration' => [ + 'index' => [ + 'back' => 'Back', + 'delete' => 'Delete', + 'save-btn' => 'Save Configuration', + 'save-success' => 'Configuration Saved Successfully.', + 'search' => 'Search', + 'select-country' => 'Select Country', + 'select-state' => 'Select State', + 'title' => 'Configuration', + 'general' => [ + 'title' => 'General', + 'info' => 'General Configuration', + 'general' => [ + 'title' => 'General', + 'info' => 'Update your general settings here.', + 'locale-settings' => [ + 'title' => 'Locale Settings', + 'title-info' => 'Defines the language used in the admin panel interface. The dropdown lists every locale available in your installation.', + ], + 'admin-logo' => [ + 'logo-image' => 'Logo Image', + 'title' => 'Admin Logo', + 'title-info' => 'Configure logo image for your admin panel.', + 'favicon-image' => 'Favicon Image', + 'favicon-image-info' => 'Configure favicon image for your admin panel.', + ], + ], + 'settings' => [ + 'title' => 'Settings', + 'info' => 'Update your settings here.', + 'footer' => [ + 'show' => 'Show Powered By', + 'show-info' => 'Show or hide the "Powered by" bar at the bottom of the admin panel.', + 'info' => 'We can configure the powered by section here.', + 'powered-by' => 'Powered by text editor', + 'title' => 'Powered by Section Configurations', + ], + 'menu' => [ + 'activities' => 'Activities', + 'configuration' => 'Configuration', + 'contacts' => 'Contacts', + 'dashboard' => 'Dashboard', + 'draft' => 'Draft', + 'inbox' => 'Inbox', + 'info' => 'We can configure the menu items name here.', + 'leads' => 'Leads', + 'mail' => 'Mail', + 'organizations' => 'Organizations', + 'outbox' => 'Outbox', + 'persons' => 'Persons', + 'products' => 'Products', + 'quotes' => 'Quotes', + 'sent' => 'Sent', + 'settings' => 'Settings', + 'title' => 'Menu Item Configurations', + 'trash' => 'Trash', + ], + 'dashboard' => [ + 'title' => 'Dashboard Configurations', + 'info' => 'We can configure the default date range of the dashboard here.', + 'date-range' => 'Default Date Range', + 'date-range-info' => 'The period the dashboard loads with. The date pickers on the dashboard can still be adjusted for a one-off view.', + 'custom-days' => 'Custom Range (Days)', + 'custom-days-info' => 'Number of days to look back when the date range is set to Custom.', + 'ranges' => [ + '1-month' => '1 Month', + '3-months' => '3 Months', + '9-months' => '9 Months', + '1-year' => '1 Year', + '2-years' => '2 Years', + 'custom' => 'Custom', + ], + ], + 'menu-color' => [ + 'brand-color' => 'Brand Color', + 'info' => 'We can change the menu items colors here.', + 'title' => 'Menu Item Color Configurations', + ], + ], + ], + 'email' => [ + 'title' => 'Email Settings', + 'info' => 'Email configuration for the application.', + 'imap' => [ + 'title' => 'IMAP Settings', + 'info' => 'IMAP email configuration for receiving emails.', + 'account' => [ + 'title' => 'IMAP Account', + 'title-info' => 'Configure your IMAP account settings here.', + 'host' => 'Host', + 'port' => 'Port', + 'encryption' => 'Encryption Type', + 'validate-cert' => 'Validate SSL Certificate', + 'username' => 'IMAP Username', + 'password' => 'IMAP Password', + ], + ], + ], + 'magic-ai' => [ + 'title' => 'Magic AI', + 'info' => 'Magic AI configuration for the application.', + 'settings' => [ + 'api-key' => 'API Key', + 'api-key-info' => 'Remember to use a OpenRouter API key for each model. It\'s a simple step to enhance security and performance.', + 'enable' => 'Enable', + 'info' => 'Enhance your Magic AI experience with your OpenRouter API Key. Integrate it now for a seamless, personalized AI adventure tailored just for you! Effortlessly customize settings and take control of your AI journey.', + 'other' => 'Other Model', + 'other-model' => 'For other models, use the Model ID from OpenRouter.', + 'doc-generation' => 'DOC Generation', + 'doc-generation-info' => 'Enable the DOC Generation feature to automatically extract data from DOC files and convert them into text format. Enhance your productivity and efficiency by enabling this feature to streamline your workflow.', + 'title' => 'General Settings', + 'models' => [ + 'deepseek-r1' => 'Deepseek R1 Distill-llama-8b', + 'gemini-2-0-flash-001' => 'Gemini 2.0 flash-001', + 'gpt-4o' => 'GPT-4.0', + 'gpt-4o-mini' => 'GPT-4.0 mini', + 'grok-2-1212' => 'Grok 2.12', + 'llama-3-2-3b-instruct' => 'Llama 3.2 3b Instruct', + 'title' => 'Models', + ], + ], + ], + ], + ], + 'dashboard' => [ + 'index' => [ + 'title' => 'Dashboard', + 'start-date' => 'Start Date', + 'end-date' => 'End Date', + 'export-pdf' => 'Export PDF', + 'pdf' => [ + 'title' => 'Dashboard Report', + 'filtered-date' => 'Filtered Date', + 'revenue' => 'Revenue', + 'overview' => 'Overview', + 'total-leads' => 'Total Leads', + 'metric' => 'Metric', + 'current' => 'Current', + 'previous' => 'Previous', + 'progress' => 'Progress', + 'date' => 'Date', + 'all' => 'All', + 'total' => 'Total', + 'won' => 'Won', + 'lost' => 'Lost', + 'source' => 'Source', + 'type' => 'Type', + 'stage' => 'Stage', + 'product' => 'Product', + 'person' => 'Person', + 'price' => 'Price', + 'revenue-by-sources' => 'Revenue By Sources', + 'revenue-by-types' => 'Revenue By Types', + 'open-leads-by-stages' => 'Open Leads By Stages', + 'top-selling-products' => 'Top Selling Products', + 'top-persons' => 'Top Persons', + ], + 'revenue' => [ + 'lost-revenue' => 'Lost Revenue', + 'won-revenue' => 'Won Revenue', + ], + 'over-all' => [ + 'average-lead-value' => 'Average Lead Value', + 'total-leads' => 'Total Leads', + 'average-leads-per-day' => 'Average Leads Per Day', + 'total-quotations' => 'Total Quotations', + 'total-persons' => 'Total Persons', + 'total-organizations' => 'Total Organizations', + ], + 'total-leads' => [ + 'title' => 'Leads', + 'total' => 'Total Leads', + 'won' => 'Won Leads', + 'lost' => 'Lost Leads', + ], + 'revenue-by-sources' => [ + 'title' => 'Revenue By Sources', + 'empty-title' => 'No Data Available', + 'empty-info' => 'No data available for selected interval', + ], + 'revenue-by-types' => [ + 'title' => 'Revenue By Types', + 'empty-title' => 'No Data Available', + 'empty-info' => 'No data available for selected interval', + ], + 'top-selling-products' => [ + 'title' => 'Top Products', + 'empty-title' => 'No Products Found', + 'empty-info' => 'No products available for selected interval', + ], + 'top-persons' => [ + 'title' => 'Top Persons', + 'empty-title' => 'No Persons Found', + 'empty-info' => 'No persons available for selected interval', + ], + 'open-leads-by-states' => [ + 'title' => 'Open Leads By Stages', + 'empty-title' => 'No Data Available', + 'empty-info' => 'No data available for selected interval', + ], + ], + ], + 'help' => [ + 'index' => [ + 'title' => 'Help & Resources', + 'description' => 'Everything you need to get the most out of Krayin Admin — hosting, support and professional services, plus extensions and developer documentation.', + 'contact-us' => 'Contact us', + 'view-all' => 'View all', + 'still-need-help-title' => 'Still need a hand?', + 'still-need-help-description' => 'Talk to the Krayin team about hosting, custom development or anything else.', + + 'services' => [ + 'title' => 'Services & Resources', + 'cloud-hosting' => [ + 'title' => 'Cloud Hosting', + 'description' => 'Cost-effective, managed cloud hosting — try and launch Krayin on the cloud in minutes, fully optimised and scalable.', + ], + 'support' => [ + 'title' => 'Support & Maintenance', + 'description' => 'Dedicated technical support and ongoing maintenance plans to keep your CRM secure, updated and running smoothly.', + ], + 'extensions' => [ + 'title' => 'Extensions', + 'description' => 'Browse official and community add-ons to extend Krayin with new connectors, channels and features.', + ], + ], + + 'resources' => [ + 'title' => 'Documentation', + 'user-docs' => [ + 'title' => 'User Guide', + 'description' => 'Expert help for module integration, customisation, data migration, version upgrades and bespoke development.', + ], + 'dev-docs' => [ + 'title' => 'Dev Docs & Blogs', + 'description' => 'Developer guides, tutorials and the latest articles to help you build, configure and stay up to date.', + ], + 'api-docs' => [ + 'title' => 'API Docs', + 'description' => 'Full REST API reference with endpoints, authentication and examples to integrate Krayin with your stack.', + ], + ], + + 'community' => [ + 'forums' => [ + 'title' => 'Community Forums', + 'description' => 'Connect with thousands of other Krayin users and developers to share tips and solve problems.', + 'link' => 'Join the community', + ], + 'tutorials' => [ + 'title' => 'Video Tutorials', + 'description' => 'Watch step-by-step video guides on setting up your CRM, configuring pipelines and managing users.', + 'link' => 'Browse YouTube channel', + ], + ], + ], + ], + 'layouts' => [ + 'app-version' => 'Version: :version', + 'dashboard' => 'Dashboard', + 'help' => 'Help & Resources', + 'leads' => 'Leads', + 'quotes' => 'Quotes', + 'quote' => 'Quote', + 'mail' => [ + 'title' => 'Mail', + 'compose' => 'Compose', + 'inbox' => 'Inbox', + 'draft' => 'Draft', + 'outbox' => 'Outbox', + 'sent' => 'Sent', + 'trash' => 'Trash', + 'setting' => 'Setting', + ], + 'activities' => 'Activities', + 'contacts' => 'Contacts', + 'persons' => 'Persons', + 'person' => 'Person', + 'organizations' => 'Organizations', + 'organization' => 'Organization', + 'products' => 'Products', + 'product' => 'Product', + 'settings' => 'Settings', + 'user' => 'User', + 'user-info' => 'Manage all your users and their permissions in the CRM, what they’re allowed to do.', + 'groups' => 'Groups', + 'groups-info' => 'Add, edit or delete groups from CRM', + 'roles' => 'Roles', + 'role' => 'Role', + 'roles-info' => 'Add, edit or delete roles from CRM', + 'users' => 'Users', + 'users-info' => 'Add, edit or delete users from CRM', + 'lead' => 'Lead', + 'lead-info' => 'Manage all your leads related settings in the CRM', + 'sidebar' => [ + 'collapse' => 'Collapse', + 'expand' => 'Expand', + ], + 'quick-add' => [ + 'title' => 'Quick Add', + 'save' => 'Save', + 'to' => 'To', + 'subject' => 'Subject', + 'message' => 'Message', + ], + 'pipelines' => 'Pipelines', + 'pipelines-info' => 'Add, edit or delete pipelines from CRM', + 'sources' => 'Sources', + 'sources-info' => 'Add, edit or delete sources from CRM', + 'types' => 'Types', + 'types-info' => 'Add, edit or delete types from CRM', + 'automation' => 'Automation', + 'automation-info' => 'Manage all your automation related settings in the CRM', + 'attributes' => 'Attributes', + 'attribute' => 'Attribute', + 'attributes-info' => 'Add, edit or delete attributes from CRM', + 'email-templates' => 'Email Templates', + 'email' => 'Email', + 'email-templates-info' => 'Add, edit or delete email templates from CRM', + 'events' => 'Events', + 'events-info' => 'Add, edit or delete events from CRM', + 'campaigns' => 'Campaigns', + 'campaigns-info' => 'Add, edit or delete campaigns from CRM', + 'workflows' => 'Workflows', + 'workflows-info' => 'Add, edit or delete workflows from CRM', + 'webhooks' => 'Webhooks', + 'webhooks-info' => 'Add, edit or delete webhooks from CRM', + 'other-settings' => 'Other Settings', + 'other-settings-info' => 'Manage all your extra settings in the CRM', + 'tags' => 'Tags', + 'tags-info' => 'Add, edit or delete tags from CRM', + 'my-account' => 'My Account', + 'sign-out' => 'Sign Out', + 'back' => 'Back', + 'name' => 'Name', + 'configuration' => 'Configuration', + 'howdy' => 'Howdy!', + 'warehouses' => 'Warehouses', + 'warehouse' => 'Warehouse', + 'warehouses-info' => 'Add, edit or delete warehouses from CRM', + 'inventory' => 'Inventory', + 'inventory-info' => 'Manage all your inventory related settings in the CRM', + 'data_transfer' => 'Data Transfer', + 'data_transfer_info' => 'Manage persons, products and leads data transfer related settings in the CRM', + ], + 'user' => [ + 'account' => [ + 'name' => 'Name', + 'email' => 'Email', + 'password' => 'Password', + 'my_account' => 'My account', + 'update_details' => 'Update Details', + 'current_password' => 'Current password', + 'confirm_password' => 'Confirm password', + 'password-match' => 'Current password does not match.', + 'account-save' => 'Account changes saved successfully.', + 'permission-denied' => 'Permission Denied', + 'remove-image' => 'Remove Image', + 'upload_image_pix' => 'Upload a Profile Image (100px x 100px)', + 'upload_image_format' => 'in PNG or JPG Format', + 'image_upload_message' => 'Only images (.jpeg, .jpg, .png, ..) are allowed.', + ], + ], + 'emails' => [ + 'common' => [ + 'dear' => 'Dear :name', + 'cheers' => 'Cheers,
    Team :app_name', + 'user' => [ + 'dear' => 'Dear :username', + 'create-subject' => 'You are added as a member.', + 'create-body' => 'Congratulations! You are now a member of our team.', + 'forget-password' => [ + 'subject' => 'Customer Reset Password', + 'dear' => 'Dear :username', + 'reset-password' => 'Reset Password', + 'info' => 'You are receiving this email because we received a password reset request for your account', + 'final-summary' => 'If you did not request a password reset, no further action is required', + 'thanks' => 'Thanks!', + ], + ], + ], + ], + 'validations' => [ + 'message' => [ + 'code' => 'The field must be a valid code.', + 'decimal' => 'The :attribute must be a decimal.', + ], + ], + 'errors' => [ + 'dashboard' => 'Dashboard', + 'go-back' => 'Go Back', + 'support' => 'If the problem persists, reach out to us at
    :email for assistance.', + 'unauthorized' => 'This action is unauthorized.', + 'role-exceeds-own' => 'You can only assign a role whose permissions are the same as or fewer than your own.', + 'user-exceeds-own' => 'You can only manage users whose permissions are the same as or fewer than your own.', + 'own-privileges' => 'You cannot change your own role or data scope.', + 'scope-exceeds-own' => 'You cannot grant a data scope wider than your own.', + 'primary-admin-protected' => 'The primary administrator account is protected and cannot be modified here.', + 'role-permissions-exceed-own' => 'You can only create or edit roles whose permissions are the same as or fewer than your own.', + + '404' => [ + 'description' => 'Oops! The page you\'re looking for is on vacation. It seems we couldn\'t find what you were searching for.', + 'title' => '404 Page Not Found', + ], + '401' => [ + 'description' => 'Oops! Looks like you\'re not allowed to access this page. It seems you\'re missing the necessary credentials.', + 'title' => '401 Unauthorized', + ], + '403' => [ + 'description' => 'Oops! This page is off-limits. It appears you don\'t have the required permissions to view this content.', + 'title' => '403 Forbidden', + ], + '500' => [ + 'description' => 'Oops! Something went wrong. It seems we\'re having trouble loading the page you\'re looking for.', + 'title' => '500 Internal Server Error', + ], + '503' => [ + 'description' => 'Oops! Looks like we\'re temporarily down for maintenance. Please check back in a bit.', + 'title' => '503 Service Unavailable', + ], + ], + 'export' => [ + 'csv' => 'CSV', + 'download' => 'Download', + 'export' => 'Export', + 'google-contacts' => 'Google Contacts', + 'google-contacts-in-progress' => 'Exporting your contacts to Google… this may take a moment.', + 'google-contacts-total' => 'Total Contacts:', + 'google-contacts-exported' => 'Exported:', + 'google-contacts-duplicate' => 'Already Existed:', + 'google-contacts-failed' => 'Failed:', + 'google-contacts-summary' => 'Export finished: :exported exported, :duplicate already existed, :failed failed.', + 'no-records' => 'Nothing to export', + 'xls' => 'XLS', + 'xlsx' => 'XLSX', + ], +]; diff --git a/packages/Webkul/Admin/src/Resources/lang/es/app.php b/packages/Webkul/Admin/src/Resources/lang/es/app.php new file mode 100644 index 0000000..8ee06ed --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/lang/es/app.php @@ -0,0 +1,2481 @@ + [ + 'help' => 'Ayuda', + 'leads' => 'Clientes potenciales', + 'lead' => 'Cliente potencial', + 'quotes' => 'Cotizaciones', + 'mail' => 'Correo', + 'inbox' => 'Bandeja de entrada', + 'draft' => 'Borradores', + 'outbox' => 'Bandeja de salida', + 'sent' => 'Enviados', + 'trash' => 'Papelera', + 'activities' => 'Actividades', + 'webhook' => 'Webhook', + 'contacts' => 'Contactos', + 'persons' => 'Personas', + 'organizations' => 'Organizaciones', + 'products' => 'Productos', + 'settings' => 'Configuraciones', + 'groups' => 'Grupos', + 'roles' => 'Roles', + 'users' => 'Usuarios', + 'user' => 'Usuario', + 'automation' => 'Automatización', + 'attributes' => 'Atributos', + 'pipelines' => 'Canales', + 'sources' => 'Fuentes', + 'types' => 'Tipos', + 'email-templates' => 'Plantillas de correo electrónico', + 'workflows' => 'Flujos de trabajo', + 'other-settings' => 'Otras configuraciones', + 'tags' => 'Etiquetas', + 'configuration' => 'Configuración', + 'create' => 'Crear', + 'quick_add' => 'Agregar Rápido', + 'edit' => 'Editar', + 'view' => 'Ver', + 'print' => 'Imprimir', + 'delete' => 'Eliminar', + 'export' => 'Exportar', + 'mass-delete' => 'Eliminar en masa', + 'data-transfer' => 'Transferencia de Datos', + 'imports' => 'Importaciones', + 'import' => 'Importar', + 'event' => 'Evento', + 'campaigns' => 'Campañas', + 'warehouses' => 'Almacenes', + 'inventory' => 'Inventario', + ], + 'users' => [ + 'activate-warning' => 'Tu cuenta aún no está activada. Por favor, contacta al administrador.', + 'login-error' => 'Las credenciales no coinciden con nuestros registros.', + 'not-permission' => 'No tienes permiso para acceder al panel de administración.', + 'login' => [ + 'email' => 'Dirección de correo electrónico', + 'forget-password-link' => '¿Olvidaste tu contraseña?', + 'password' => 'Contraseña', + 'submit-btn' => 'Iniciar sesión', + 'title' => 'Iniciar sesión', + ], + 'forget-password' => [ + 'create' => [ + 'email' => 'Correo electrónico registrado', + 'email-not-exist' => 'El correo electrónico no existe', + 'page-title' => 'Olvidé mi contraseña', + 'reset-link-sent' => 'Enlace para restablecer la contraseña enviado', + 'sign-in-link' => '¿Volver a iniciar sesión?', + 'submit-btn' => 'Restablecer', + 'title' => 'Recuperar contraseña', + ], + ], + 'reset-password' => [ + 'back-link-title' => '¿Volver a iniciar sesión?', + 'confirm-password' => 'Confirmar contraseña', + 'email' => 'Correo electrónico registrado', + 'password' => 'Contraseña', + 'submit-btn' => 'Restablecer contraseña', + 'title' => 'Restablecer contraseña', + ], + ], + 'account' => [ + 'edit' => [ + 'back-btn' => 'Regresar', + 'change-password' => 'Cambiar contraseña', + 'confirm-password' => 'Confirmar contraseña', + 'current-password' => 'Contraseña actual', + 'email' => 'Correo electrónico', + 'general' => 'General', + 'invalid-password' => 'La contraseña actual que ingresaste es incorrecta.', + 'name' => 'Nombre', + 'password' => 'Contraseña', + 'profile-image' => 'Imagen de perfil', + 'save-btn' => 'Guardar cuenta', + 'title' => 'Mi cuenta', + 'update-success' => 'Cuenta actualizada con éxito', + 'upload-image-info' => 'Sube una imagen de perfil (110 px × 110 px) en formato PNG o JPG', + ], + ], + 'components' => [ + 'activities' => [ + 'actions' => [ + 'mail' => [ + 'btn' => 'Correo', + 'title' => 'Redactar correo', + 'to' => 'Para', + 'enter-emails' => 'Presiona Enter para agregar direcciones de correo electrónico', + 'cc' => 'CC', + 'bcc' => 'CCO', + 'subject' => 'Asunto', + 'send-btn' => 'Enviar', + 'message' => 'Mensaje', + ], + 'file' => [ + 'btn' => 'Archivo', + 'title' => 'Agregar archivo', + 'title-control' => 'Título', + 'name' => 'Nombre', + 'description' => 'Descripción', + 'file' => 'Archivo', + 'save-btn' => 'Guardar archivo', + ], + 'note' => [ + 'btn' => 'Nota', + 'title' => 'Agregar nota', + 'comment' => 'Comentario', + 'save-btn' => 'Guardar nota', + ], + 'activity' => [ + 'btn' => 'Actividad', + 'title' => 'Agregar actividad', + 'title-control' => 'Título', + 'description' => 'Descripción', + 'schedule-from' => 'Programar desde', + 'schedule-to' => 'Programar hasta', + 'location' => 'Ubicación', + 'call' => 'Llamada', + 'meeting' => 'Reunión', + 'lunch' => 'Almuerzo', + 'save-btn' => 'Guardar actividad', + 'participants' => [ + 'title' => 'Participantes', + 'placeholder' => 'Escribe para buscar participantes', + 'users' => 'Usuarios', + 'persons' => 'Personas', + 'no-results' => 'No se encontraron resultados...', + ], + ], + ], + 'index' => [ + 'all' => 'Todo', + 'bcc' => 'CCO', + 'by-user' => 'Por :user', + 'calls' => 'Llamadas', + 'cc' => 'CC', + 'change-log' => 'Registros de cambios', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'emails' => 'Correos electrónicos', + 'empty' => 'Vacío', + 'files' => 'Archivos', + 'from' => 'De', + 'location' => 'Ubicación', + 'lunches' => 'Almuerzos', + 'mark-as-done' => 'Marcar como hecho', + 'meetings' => 'Reuniones', + 'notes' => 'Notas', + 'participants' => 'Participantes', + 'planned' => 'Planificado', + 'quotes' => 'Cotizaciones', + 'scheduled-on' => 'Programado en', + 'system' => 'Sistema', + 'to' => 'A', + 'unlink' => 'Desvincular', + 'view' => 'Ver', + 'empty-placeholders' => [ + 'all' => [ + 'title' => 'No se encontraron actividades', + 'description' => 'No se encontraron actividades para esto. Puedes agregar actividades haciendo clic en el botón Actividad en el panel izquierdo.', + ], + 'planned' => [ + 'title' => 'No se encontraron actividades planificadas', + 'description' => 'No se encontraron actividades planificadas para esto. Puedes agregarlas haciendo clic en el botón Actividad en el panel izquierdo.', + ], + 'notes' => [ + 'title' => 'No se encontraron notas', + 'description' => 'No se encontraron notas para esto. Puedes agregarlas haciendo clic en el botón Nota en el panel izquierdo.', + ], + 'calls' => [ + 'title' => 'No se encontraron llamadas', + 'description' => 'No se encontraron llamadas para esto. Puedes agregarlas haciendo clic en el botón Actividad en el panel izquierdo y seleccionando el tipo Llamada.', + ], + 'meetings' => [ + 'title' => 'No se encontraron reuniones', + 'description' => 'No se encontraron reuniones para esto. Puedes agregarlas haciendo clic en el botón Actividad en el panel izquierdo y seleccionando el tipo Reunión.', + ], + 'lunches' => [ + 'title' => 'No se encontraron almuerzos', + 'description' => 'No se encontraron almuerzos para esto. Puedes agregarlos haciendo clic en el botón Actividad en el panel izquierdo y seleccionando el tipo Almuerzo.', + ], + 'files' => [ + 'title' => 'No se encontraron archivos', + 'description' => 'No se encontraron archivos para esto. Puedes agregarlos haciendo clic en el botón Archivo en el panel izquierdo.', + ], + 'emails' => [ + 'title' => 'No se encontraron correos electrónicos', + 'description' => 'No se encontraron correos electrónicos para esto. Puedes agregarlos haciendo clic en el botón Correo en el panel izquierdo.', + ], + 'system' => [ + 'title' => 'No se encontraron registros de cambios', + 'description' => 'No se encontraron registros de cambios para esto.', + ], + ], + ], + ], + 'media' => [ + 'images' => [ + 'add-image-btn' => 'Agregar imagen', + 'ai-add-image-btn' => 'Inteligencia artificial mágica', + 'allowed-types' => 'png, jpeg, jpg', + 'not-allowed-error' => 'Solo se aceptan archivos de imagen (.jpeg, .jpg, .png, ..)', + 'placeholders' => [ + 'front' => 'Frontal', + 'next' => 'Siguiente', + 'size' => 'Tamaño', + 'use-cases' => 'Casos de uso', + 'zoom' => 'Acercar', + ], + ], + 'videos' => [ + 'add-video-btn' => 'Agregar video', + 'allowed-types' => 'mp4, webm, mkv', + 'not-allowed-error' => 'Solo se aceptan archivos de video (.mp4, .mov, .ogg ..)', + ], + ], + 'datagrid' => [ + 'index' => [ + 'no-records-selected' => 'No se han seleccionado registros.', + 'must-select-a-mass-action-option' => 'Debes seleccionar una opción de acción en masa.', + 'must-select-a-mass-action' => 'Debes seleccionar una acción en masa.', + ], + 'toolbar' => [ + 'length-of' => ':length de', + 'of' => 'de', + 'per-page' => 'Por Página', + 'results' => ':total Resultados', + 'delete' => 'Eliminar', + 'selected' => ':total Elementos Seleccionados', + 'mass-actions' => [ + 'submit' => 'Enviar', + 'select-option' => 'Seleccionar Opción', + 'select-action' => 'Seleccionar Acción', + ], + 'filter' => [ + 'apply-filters-btn' => 'Aplicar Filtros', + 'back-btn' => 'Regresar', + 'create-new-filter' => 'Crear Nuevo Filtro', + 'custom-filters' => 'Filtros Personalizados', + 'delete-error' => 'Hubo un error al eliminar el filtro, por favor intente de nuevo.', + 'delete-success' => 'Filtro eliminado con éxito.', + 'empty-description' => 'No hay filtros seleccionados disponibles para guardar. Por favor, seleccione filtros para guardar.', + 'empty-title' => 'Agregar Filtros para Guardar', + 'name' => 'Nombre', + 'quick-filters' => 'Filtros Rápidos', + 'save-btn' => 'Guardar', + 'save-filter' => 'Guardar Filtro', + 'saved-success' => 'Filtro guardado con éxito.', + 'selected-filters' => 'Filtros Seleccionados', + 'title' => 'Filtro', + 'update' => 'Actualizar', + 'update-filter' => 'Actualizar Filtro', + 'updated-success' => 'Filtro actualizado con éxito.', + ], + 'search' => [ + 'title' => 'Buscar', + ], + 'column-settings' => [ + 'title' => 'Configuración de Columnas', + ], + ], + 'filters' => [ + 'select' => 'Seleccionar', + 'title' => 'Filtros', + 'dropdown' => [ + 'searchable' => [ + 'at-least-two-chars' => 'Escribe al menos 2 caracteres...', + 'no-results' => 'No se encontraron resultados...', + ], + ], + 'custom-filters' => [ + 'clear-all' => 'Borrar Todo', + 'title' => 'Filtros Personalizados', + ], + 'boolean-options' => [ + 'false' => 'Falso', + 'true' => 'Verdadero', + ], + 'date-options' => [ + 'last-month' => 'Mes Pasado', + 'last-six-months' => 'Últimos 6 Meses', + 'last-three-months' => 'Últimos 3 Meses', + 'this-month' => 'Este Mes', + 'this-week' => 'Esta Semana', + 'this-year' => 'Este Año', + 'today' => 'Hoy', + 'yesterday' => 'Ayer', + ], + ], + 'table' => [ + 'actions' => 'Acciones', + 'no-records-available' => 'No hay Registros Disponibles.', + ], + ], + 'modal' => [ + 'confirm' => [ + 'agree-btn' => 'Aceptar', + 'disagree-btn' => 'Rechazar', + 'message' => '¿Estás seguro de que quieres realizar esta acción?', + 'title' => '¿Estás seguro?', + ], + ], + 'tags' => [ + 'index' => [ + 'title' => 'Etiquetas', + 'added-tags' => 'Etiquetas Agregadas', + 'save-btn' => 'Guardar Etiqueta', + 'placeholder' => 'Escribe para buscar etiquetas', + 'add-tag' => 'Agregar \\":term\\"...', + 'aquarelle-red' => 'Rojo Aquarelle', + 'crushed-cashew' => 'Anacardo Triturado', + 'beeswax' => 'Cera de Abejas', + 'lemon-chiffon' => 'Chiffon de Limón', + 'snow-flurry' => 'Tormenta de Nieve', + 'honeydew' => 'Melón', + ], + ], + 'layouts' => [ + 'powered-by' => [ + 'description' => 'Desarrollado por :krayin, un proyecto de código abierto de :webkul.', + ], + 'header' => [ + 'mega-search' => [ + 'title' => 'Búsqueda Mega', + 'tabs' => [ + 'leads' => 'Oportunidades', + 'quotes' => 'Cotizaciones', + 'persons' => 'Personas', + 'products' => 'Productos', + 'settings' => 'Ajustes', + 'configurations' => 'Configuraciones', + ], + 'explore-all-products' => 'Explorar todos los Productos', + 'explore-all-leads' => 'Explorar todas las Oportunidades', + 'explore-all-contacts' => 'Explorar todos los Contactos', + 'explore-all-quotes' => 'Explorar todas las Cotizaciones', + 'explore-all-settings' => 'Explorar todos los Ajustes', + 'explore-all-configurations' => 'Explorar todas las Configuraciones', + 'explore-all-matching-products' => 'Explorar todos los productos que coinciden con ":query" (:count)', + 'explore-all-matching-leads' => 'Explorar todas las oportunidades que coinciden con ":query" (:count)', + 'explore-all-matching-contacts' => 'Explorar todos los contactos que coinciden con ":query" (:count)', + 'explore-all-matching-quotes' => 'Explorar todas las cotizaciones que coinciden con ":query" (:count)', + ], + ], + ], + 'attributes' => [ + 'edit' => [ + 'delete' => 'Eliminar', + ], + 'lookup' => [ + 'click-to-add' => 'Haz clic para agregar', + 'search' => 'Buscar...', + 'no-result-found' => 'No se encontraron resultados', + ], + ], + 'lookup' => [ + 'click-to-add' => 'Haz clic para agregar', + 'no-results' => 'No se encontraron resultados', + 'add-as-new' => 'Agregar como nuevo', + 'search' => 'Buscar...', + ], + 'flash-group' => [ + 'success' => 'Éxito', + 'error' => 'Error', + 'warning' => 'Advertencia', + 'info' => 'Información', + ], + 'tiny-mce' => [ + 'http-error' => 'Error HTTP', + 'invalid-json' => 'Respuesta JSON no válida del servidor.', + 'upload-failed' => 'Error al subir el archivo. Por favor, inténtelo de nuevo.', + ], + ], + 'quotes' => [ + 'index' => [ + 'title' => 'Cotizaciones', + 'create-btn' => 'Crear Cotización', + 'create-success' => 'Cotización creada con éxito.', + 'update-success' => 'Cotización actualizada con éxito.', + 'delete-success' => 'Cotización eliminada con éxito.', + 'delete-failed' => 'No se puede eliminar la cotización.', + 'datagrid' => [ + 'subject' => 'Asunto', + 'sales-person' => 'Vendedor', + 'expired-at' => 'Vence en', + 'created-at' => 'Creado en', + 'person' => 'Persona', + 'subtotal' => 'Subtotal', + 'discount' => 'Descuento', + 'tax' => 'Impuesto', + 'adjustment' => 'Ajuste', + 'grand-total' => 'Total General', + 'edit' => 'Editar', + 'delete' => 'Eliminar', + 'print' => 'Imprimir', + ], + 'pdf' => [ + 'adjustment' => 'Ajuste', + 'amount' => 'Monto', + 'billing-address' => 'Dirección de Facturación', + 'date' => 'Fecha', + 'discount' => 'Descuento', + 'description' => 'Descripción', + 'expired-at' => 'Vence en', + 'grand-total' => 'Total General', + 'person' => 'Persona', + 'price' => 'Precio', + 'product-name' => 'Nombre del Producto', + 'quantity' => 'Cantidad', + 'quote-id' => 'ID de Cotización', + 'sales-person' => 'Vendedor', + 'shipping-address' => 'Dirección de Envío', + 'sku' => 'SKU', + 'sub-total' => 'Subtotal', + 'subject' => 'Asunto', + 'tax' => 'Impuesto', + 'title' => 'Cotización', + ], + ], + 'create' => [ + 'title' => 'Crear Cotización', + 'save-btn' => 'Guardar Cotización', + 'quote-info' => 'Información de la Cotización', + 'quote-info-info' => 'Introduce la información básica de la cotización.', + 'address-info' => 'Información de la Dirección', + 'address-info-info' => 'Información sobre la dirección relacionada con la cotización.', + 'same-as-billing' => 'Dirección de envío igual que la dirección de facturación', + 'quote-items' => 'Artículos de la Cotización', + 'search-products' => 'Buscar Productos', + 'link-to-lead' => 'Vincular a cliente potencial', + 'quote-item-info' => 'Agregar Solicitud de Producto para esta cotización.', + 'quote-name' => 'Nombre de la Cotización', + 'quantity' => 'Cantidad', + 'price' => 'Precio', + 'discount' => 'Descuento', + 'tax' => 'Impuesto', + 'total' => 'Total', + 'amount' => 'Monto', + 'add-item' => '+ Agregar Artículo', + 'sub-total' => 'Subtotal (:symbol)', + 'total-discount' => 'Descuento (:symbol)', + 'total-tax' => 'Impuesto (:symbol)', + 'total-adjustment' => 'Ajuste (:symbol)', + 'grand-total' => 'Total General (:symbol)', + 'discount-amount' => 'Monto del Descuento', + 'tax-amount' => 'Monto del Impuesto', + 'adjustment-amount' => 'Monto del Ajuste', + 'product-name' => 'Nombre del Producto', + 'action' => 'Acción', + ], + 'edit' => [ + 'title' => 'Editar Cotización', + 'save-btn' => 'Guardar Cotización', + 'quote-info' => 'Información de la Cotización', + 'quote-info-info' => 'Introduce la información básica de la cotización.', + 'address-info' => 'Información de la Dirección', + 'address-info-info' => 'Información sobre la dirección relacionada con la cotización.', + 'quote-items' => 'Artículos de la Cotización', + 'link-to-lead' => 'Vincular a cliente potencial', + 'quote-item-info' => 'Agregar Solicitud de Producto para esta cotización.', + 'quote-name' => 'Nombre de la Cotización', + 'quantity' => 'Cantidad', + 'price' => 'Precio', + 'search-products' => 'Buscar Productos', + 'discount' => 'Descuento', + 'tax' => 'Impuesto', + 'total' => 'Total', + 'amount' => 'Monto', + 'add-item' => '+ Agregar Artículo', + 'sub-total' => 'Subtotal (:symbol)', + 'total-discount' => 'Descuento (:symbol)', + 'total-tax' => 'Impuesto (:symbol)', + 'total-adjustment' => 'Ajuste (:symbol)', + 'grand-total' => 'Total General (:symbol)', + 'discount-amount' => 'Monto del Descuento', + 'tax-amount' => 'Monto del Impuesto', + 'adjustment-amount' => 'Monto del Ajuste', + 'product-name' => 'Nombre del Producto', + 'action' => 'Acción', + ], + ], + 'contacts' => [ + 'persons' => [ + 'index' => [ + 'title' => 'Personas', + 'create-btn' => 'Crear Persona', + 'create-success' => 'Persona creada con éxito.', + 'update-success' => 'Persona actualizada con éxito.', + 'all-delete-success' => 'Todas las personas seleccionadas fueron eliminadas exitosamente.', + 'partial-delete-warning' => 'Algunas personas fueron eliminadas con éxito. Otras no se pudieron eliminar porque están vinculadas a clientes potenciales.', + 'none-delete-warning' => 'Ninguna de las personas seleccionadas pudo ser eliminada porque están vinculadas a clientes potenciales.', + 'no-selection' => 'No se seleccionaron personas para eliminar.', + 'delete-failed' => 'No se pudieron eliminar las personas seleccionadas.', + 'datagrid' => [ + 'contact-numbers' => 'Números de Contacto', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'emails' => 'Correos Electrónicos', + 'id' => 'ID', + 'view' => 'Ver', + 'name' => 'Nombre', + 'organization-name' => 'Nombre de la Organización', + 'tag-name' => 'Nombre de etiqueta', + 'google-export-not-connected' => 'Conecta primero tu cuenta de Google desde Configuración > Otras Configuraciones > Google Contacts.', + 'google-export-started' => 'Exportación a Google Contacts iniciada.', + ], + ], + 'view' => [ + 'title' => ':name', + 'about-person' => 'Sobre la Persona', + 'about-organization' => 'Acerca de la organización', + 'activities' => [ + 'index' => [ + 'all' => 'Todos', + 'calls' => 'Llamadas', + 'meetings' => 'Reuniones', + 'lunches' => 'Almuerzos', + 'files' => 'Archivos', + 'quotes' => 'Cotizaciones', + 'notes' => 'Notas', + 'emails' => 'Correos Electrónicos', + 'by-user' => 'Por :user', + 'scheduled-on' => 'Programado para', + 'location' => 'Ubicación', + 'participants' => 'Participantes', + 'mark-as-done' => 'Marcar como Hecho', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + ], + 'actions' => [ + 'mail' => [ + 'btn' => 'Correo', + 'title' => 'Redactar Correo', + 'to' => 'Para', + 'cc' => 'CC', + 'bcc' => 'CCO', + 'subject' => 'Asunto', + 'send-btn' => 'Enviar', + 'message' => 'Mensaje', + ], + 'file' => [ + 'btn' => 'Archivo', + 'title' => 'Agregar Archivo', + 'title-control' => 'Título', + 'name' => 'Nombre del Archivo', + 'description' => 'Descripción', + 'file' => 'Archivo', + 'save-btn' => 'Guardar Archivo', + ], + 'note' => [ + 'btn' => 'Nota', + 'title' => 'Agregar Nota', + 'comment' => 'Comentario', + 'save-btn' => 'Guardar Nota', + ], + 'activity' => [ + 'btn' => 'Actividad', + 'title' => 'Agregar Actividad', + 'title-control' => 'Título', + 'description' => 'Descripción', + 'schedule-from' => 'Programar Desde', + 'schedule-to' => 'Programar Hasta', + 'location' => 'Ubicación', + 'call' => 'Llamada', + 'meeting' => 'Reunión', + 'lunch' => 'Almuerzo', + 'save-btn' => 'Guardar Actividad', + ], + ], + ], + 'tags' => [ + 'create-success' => 'Etiqueta creada con éxito.', + 'destroy-success' => 'Etiqueta eliminada con éxito.', + ], + ], + 'create' => [ + 'title' => 'Crear Persona', + 'save-btn' => 'Guardar Persona', + ], + 'edit' => [ + 'title' => 'Editar Persona', + 'save-btn' => 'Guardar Persona', + ], + ], + 'organizations' => [ + 'index' => [ + 'title' => 'Organizaciones', + 'create-btn' => 'Crear Organización', + 'create-success' => 'Organización creada con éxito.', + 'update-success' => 'Organización actualizada con éxito.', + 'delete-success' => 'Organización eliminada con éxito.', + 'delete-failed' => 'No se puede eliminar la organización.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + 'persons-count' => 'Número de Personas', + ], + ], + 'create' => [ + 'title' => 'Crear Organización', + 'save-btn' => 'Guardar Organización', + ], + 'edit' => [ + 'title' => 'Editar Organización', + 'save-btn' => 'Guardar Organización', + ], + ], + ], + 'products' => [ + 'index' => [ + 'title' => 'Productos', + 'create-btn' => 'Crear Producto', + 'create-success' => 'Producto creado con éxito.', + 'update-success' => 'Producto actualizado con éxito.', + 'delete-success' => 'Producto eliminado con éxito.', + 'delete-failed' => 'No se puede eliminar el producto.', + 'datagrid' => [ + 'allocated' => 'Asignado', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'id' => 'ID', + 'in-stock' => 'En Stock', + 'name' => 'Nombre', + 'on-hand' => 'Disponible', + 'tag-name' => 'Nombre de etiqueta', + 'price' => 'Precio', + 'sku' => 'SKU', + 'view' => 'Ver', + ], + ], + 'create' => [ + 'save-btn' => 'Guardar Productos', + 'title' => 'Crear Productos', + 'general' => 'General', + 'price' => 'Precio', + ], + 'edit' => [ + 'title' => 'Editar Productos', + 'save-btn' => 'Guardar Productos', + 'general' => 'General', + 'price' => 'Precio', + ], + 'view' => [ + 'sku' => 'SKU', + 'all' => 'Todos', + 'notes' => 'Notas', + 'files' => 'Archivos', + 'inventories' => 'Inventario', + 'change-logs' => 'Registros de Cambios', + 'attributes' => [ + 'about-product' => 'Sobre el Producto', + ], + 'inventory' => [ + 'warehouse' => 'Almacén', + 'in-stock' => 'En Stock', + 'allocated' => 'Asignado', + 'on-hand' => 'Disponible', + 'actions' => 'Acciones', + 'assign' => 'Asignar', + 'add-warehouse' => 'Agregar almacén', + 'location' => 'Ubicación', + 'add-more' => 'Agregar Más', + 'save' => 'Guardar', + ], + ], + ], + 'settings' => [ + 'title' => 'Configuraciones', + 'groups' => [ + 'index' => [ + 'create-btn' => 'Crear Grupo', + 'title' => 'Grupos', + 'create-success' => 'Grupo creado con éxito.', + 'update-success' => 'Grupo actualizado con éxito.', + 'destroy-success' => 'Grupo eliminado con éxito.', + 'delete-failed' => 'No se puede eliminar el grupo.', + 'delete-failed-associated-users' => 'No se puede eliminar el grupo, ya que está siendo utilizado por usuarios.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'description' => 'Descripción', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + ], + 'edit' => [ + 'title' => 'Editar Grupo', + ], + 'create' => [ + 'name' => 'Nombre', + 'title' => 'Crear Grupo', + 'description' => 'Descripción', + 'save-btn' => 'Guardar Grupo', + ], + ], + ], + 'roles' => [ + 'index' => [ + 'being-used' => 'El rol no se puede eliminar, ya que está siendo utilizado por un usuario administrador.', + 'create-btn' => 'Crear Roles', + 'create-success' => 'Rol creado con éxito.', + 'current-role-delete-error' => 'No se puede eliminar el rol asignado al usuario actual.', + 'current-role-edit-error' => 'No se puede editar el rol asignado a su propia cuenta.', + 'delete-failed' => 'No se puede eliminar el rol.', + 'delete-success' => 'Rol eliminado con éxito.', + 'last-delete-error' => 'Se requiere al menos un rol.', + 'settings' => 'Configuraciones', + 'title' => 'Roles', + 'update-success' => 'Rol actualizado con éxito.', + 'user-define-error' => 'No se puede eliminar el rol del sistema.', + 'datagrid' => [ + 'all' => 'Todos', + 'custom' => 'Personalizado', + 'delete' => 'Eliminar', + 'description' => 'Descripción', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + 'permission-type' => 'Tipo de Permiso', + ], + ], + 'create' => [ + 'access-control' => 'Control de Acceso', + 'all' => 'Todos', + 'back-btn' => 'Volver', + 'custom' => 'Personalizado', + 'description' => 'Descripción', + 'general' => 'General', + 'name' => 'Nombre', + 'permissions' => 'Permisos', + 'save-btn' => 'Guardar Rol', + 'title' => 'Crear Rol', + ], + 'edit' => [ + 'access-control' => 'Control de Acceso', + 'all' => 'Todos', + 'back-btn' => 'Volver', + 'custom' => 'Personalizado', + 'description' => 'Descripción', + 'general' => 'General', + 'name' => 'Nombre', + 'permissions' => 'Permisos', + 'save-btn' => 'Guardar Rol', + 'title' => 'Editar Rol', + ], + ], + 'types' => [ + 'index' => [ + 'create-btn' => 'Crear Tipo', + 'create-success' => 'Tipo creado con éxito.', + 'delete-failed' => 'No se puede eliminar el tipo.', + 'delete-success' => 'Tipo eliminado con éxito.', + 'title' => 'Tipos', + 'update-success' => 'Tipo actualizado con éxito.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'description' => 'Descripción', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + ], + 'create' => [ + 'name' => 'Nombre', + 'save-btn' => 'Guardar Tipo', + 'title' => 'Crear Tipo', + ], + 'edit' => [ + 'title' => 'Editar Tipo', + ], + ], + ], + 'sources' => [ + 'index' => [ + 'title' => 'Fuentes', + 'create-btn' => 'Crear Fuente', + 'create-success' => 'Fuente creada con éxito.', + 'delete-failed' => 'No se puede eliminar la fuente.', + 'delete-success' => 'Fuente eliminada con éxito.', + 'update-success' => 'Fuente actualizada con éxito.', + 'delete-failed-associated-leads' => 'No se puede eliminar la fuente porque está asociada a clientes potenciales existentes. Por favor, desvincúlelos o actualícelos antes de eliminar.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + ], + 'create' => [ + 'name' => 'Nombre', + 'save-btn' => 'Guardar fuente', + 'title' => 'Crear fuente', + ], + 'edit' => [ + 'title' => 'Editar fuente', + ], + ], + ], + 'workflows' => [ + 'index' => [ + 'title' => 'Flujos de Trabajo', + 'create-btn' => 'Crear Flujo de Trabajo', + 'create-success' => 'Flujo de trabajo creado con éxito.', + 'update-success' => 'Flujo de trabajo actualizado con éxito.', + 'delete-success' => 'Flujo de trabajo eliminado con éxito.', + 'delete-failed' => 'No se puede eliminar el flujo de trabajo.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'description' => 'Descripción', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + ], + ], + 'helpers' => [ + 'update-related-leads' => 'Actualizar leads relacionados', + 'send-email-to-sales-owner' => 'Enviar correo al propietario de ventas', + 'send-email-to-participants' => 'Enviar correo a los participantes', + 'add-webhook' => 'Agregar Webhook', + 'update-lead' => 'Actualizar Lead', + 'update-person' => 'Actualizar Persona', + 'send-email-to-person' => 'Enviar correo a la persona', + 'add-tag' => 'Agregar Etiqueta', + 'add-note-as-activity' => 'Agregar Nota como Actividad', + 'update-quote' => 'Actualizar cotización', + ], + 'create' => [ + 'title' => 'Crear Flujo de Trabajo', + 'event' => 'Evento', + 'back-btn' => 'Volver', + 'save-btn' => 'Guardar Flujo de Trabajo', + 'name' => 'Nombre', + 'basic-details' => 'Detalles Básicos', + 'description' => 'Descripción', + 'actions' => 'Acciones', + 'basic-details-info' => 'Coloque la información básica del flujo de trabajo.', + 'event-info' => 'Un evento desencadena, verifica, condiciones y realiza acciones predefinidas.', + 'conditions' => 'Condiciones', + 'conditions-info' => 'Las condiciones son reglas que verifican escenarios, desencadenadas en ocasiones específicas.', + 'actions-info' => 'Una acción no solo reduce la carga de trabajo, sino que también facilita la automatización de CRM.', + 'value' => 'Valor', + 'condition-type' => 'Tipo de Condición', + 'all-condition-are-true' => 'Todas las condiciones son verdaderas', + 'any-condition-are-true' => 'Cualquiera de las condiciones es verdadera', + 'add-condition' => 'Agregar Condición', + 'add-action' => 'Agregar Acción', + 'no-webhook-found' => 'No se encontraron webhooks.', + 'create-webhook' => 'Crear un nuevo webhook.', + 'yes' => 'Sí', + 'no' => 'No', + 'email' => 'Correo Electrónico', + 'is-equal-to' => 'Es igual a', + 'is-not-equal-to' => 'No es igual a', + 'equals-or-greater-than' => 'Es igual o mayor que', + 'equals-or-less-than' => 'Es igual o menor que', + 'greater-than' => 'Mayor que', + 'less-than' => 'Menor que', + 'type' => 'Tipo', + 'contain' => 'Contiene', + 'contains' => 'Contiene', + 'does-not-contain' => 'No contiene', + ], + 'edit' => [ + 'title' => 'Editar Flujo de Trabajo', + 'event' => 'Evento', + 'back-btn' => 'Volver', + 'save-btn' => 'Guardar Flujo de Trabajo', + 'name' => 'Nombre', + 'basic-details' => 'Detalles Básicos', + 'description' => 'Descripción', + 'actions' => 'Acciones', + 'type' => 'Tipo', + 'basic-details-info' => 'Coloque la información básica del flujo de trabajo.', + 'event-info' => 'Un evento desencadena, verifica, condiciones y realiza acciones predefinidas.', + 'conditions' => 'Condiciones', + 'conditions-info' => 'Las condiciones son reglas que verifican escenarios, desencadenadas en ocasiones específicas.', + 'actions-info' => 'Una acción no solo reduce la carga de trabajo, sino que también facilita la automatización de CRM.', + 'value' => 'Valor', + 'condition-type' => 'Tipo de Condición', + 'all-condition-are-true' => 'Todas las condiciones son verdaderas', + 'any-condition-are-true' => 'Cualquiera de las condiciones es verdadera', + 'add-condition' => 'Agregar Condición', + 'add-action' => 'Agregar Acción', + 'yes' => 'Sí', + 'no' => 'No', + 'email' => 'Correo Electrónico', + 'is-equal-to' => 'Es igual a', + 'is-not-equal-to' => 'No es igual a', + 'equals-or-greater-than' => 'Es igual o mayor que', + 'equals-or-less-than' => 'Es igual o menor que', + 'greater-than' => 'Mayor que', + 'less-than' => 'Menor que', + 'contain' => 'Contiene', + 'contains' => 'Contiene', + 'does-not-contain' => 'No contiene', + ], + ], + 'google-contacts' => [ + 'index' => [ + 'title' => 'Google Contacts', + 'not-connected-info' => 'Conecta una cuenta de Google para exportar contactos del CRM a Google Contacts.', + 'connected-info' => 'Conectado como :email.', + 'connect-btn' => 'Conectar Cuenta de Google', + 'disconnect-btn' => 'Desconectar', + 'connect-success' => 'Cuenta de Google conectada con éxito.', + 'connect-failed' => 'No se pudo conectar tu cuenta de Google. Por favor, inténtalo de nuevo.', + 'not-configured' => 'Google OAuth no está configurado. Por favor, configura primero las variables de entorno requeridas.', + 'disconnect-success' => 'Cuenta de Google desconectada con éxito.', + ], + ], + 'webforms' => [ + 'index' => [ + 'title' => 'Webforms', + 'create-btn' => 'Crear Webform', + 'create-success' => 'Webform creado con éxito.', + 'update-success' => 'Webform actualizado con éxito.', + 'delete-success' => 'Webform eliminado con éxito.', + 'delete-failed' => 'No se puede eliminar el Webform.', + 'datagrid' => [ + 'id' => 'ID', + 'title' => 'Título', + 'edit' => 'Editar', + 'delete' => 'Eliminar', + ], + ], + 'create' => [ + 'title' => 'Crear formulario web', + 'add-attribute-btn' => 'Agregar Botón de Atributo', + 'attribute-label-color' => 'Color de Etiqueta del Atributo', + 'attributes' => 'Atributos', + 'attributes-info' => 'Agregue atributos personalizados al formulario.', + 'background-color' => 'Color de Fondo', + 'create-lead' => 'Crear Lead', + 'customize-webform' => 'Personalizar Webform', + 'customize-webform-info' => 'Personalice su formulario web con los colores de los elementos de su elección.', + 'description' => 'Descripción', + 'display-custom-message' => 'Mostrar mensaje personalizado', + 'form-background-color' => 'Color de Fondo del Formulario', + 'form-submit-btn-color' => 'Color del Botón de Enviar del Formulario', + 'form-submit-button-color' => 'Color del Botón de Enviar del Formulario', + 'form-title-color' => 'Color del Título del Formulario', + 'general' => 'General', + 'leads' => 'Leads', + 'person' => 'Persona', + 'pipeline' => 'Canal', + 'save-btn' => 'Guardar Webform', + 'submit-button-label' => 'Etiqueta del Botón de Enviar', + 'submit-success-action' => 'Acción en caso de éxito de envío', + 'redirect-to-url' => 'Redirigir a la URL', + 'choose-value' => 'Elige un valor', + 'select-file' => 'Seleccionar archivo', + 'select-image' => 'Seleccionar imagen', + 'enter-value' => 'Introducir valor', + ], + 'edit' => [ + 'add-attribute-btn' => 'Agregar Botón de Atributo', + 'attribute-label-color' => 'Color de Etiqueta del Atributo', + 'attributes' => 'Atributos', + 'attributes-info' => 'Agregue atributos personalizados al formulario.', + 'background-color' => 'Color de Fondo', + 'choose-value' => 'Elige un valor', + 'code-snippet' => 'Fragmento de Código', + 'copied' => 'Copiado', + 'copy' => 'Copiar', + 'create-lead' => 'Crear Lead', + 'customize-webform' => 'Personalizar Webform', + 'customize-webform-info' => 'Personalice su formulario web con los colores de los elementos de su elección.', + 'description' => 'Descripción', + 'display-custom-message' => 'Mostrar mensaje personalizado', + 'embed' => 'Incrustar', + 'enter-value' => 'Introducir valor', + 'form-background-color' => 'Color de Fondo del Formulario', + 'form-submit-btn-color' => 'Color del Botón de Enviar del Formulario', + 'form-submit-button-color' => 'Color del Botón de Enviar del Formulario', + 'form-title-color' => 'Color del Título del Formulario', + 'general' => 'General', + 'leads' => 'Leads', + 'person' => 'Persona', + 'pipeline' => 'Canal', + 'preview' => 'Vista Previa', + 'public-url' => 'URL Pública', + 'redirect-to-url' => 'Redirigir a la URL', + 'save-btn' => 'Guardar Webform', + 'select-file' => 'Seleccionar archivo', + 'select-image' => 'Seleccionar imagen', + 'submit-button-label' => 'Etiqueta del Botón de Enviar', + 'submit-success-action' => 'Acción en caso de éxito de envío', + 'title' => 'Editar formulario web', + ], + ], + 'email-template' => [ + 'index' => [ + 'create-btn' => 'Crear Plantilla de Correo Electrónico', + 'title' => 'Plantillas de Correo Electrónico', + 'create-success' => 'Plantilla de Correo Electrónico creada exitosamente.', + 'update-success' => 'Plantilla de Correo Electrónico actualizada exitosamente.', + 'delete-success' => 'Plantilla de Correo Electrónico eliminada exitosamente.', + 'delete-failed' => 'No se puede eliminar la Plantilla de Correo Electrónico.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + 'subject' => 'Asunto', + ], + ], + 'create' => [ + 'title' => 'Crear Plantilla de Correo Electrónico', + 'save-btn' => 'Guardar Plantilla de Correo Electrónico', + 'email-template' => 'Plantilla de Correo Electrónico', + 'subject' => 'Asunto', + 'content' => 'Contenido', + 'subject-placeholders' => 'Marcadores de Posición del Asunto', + 'general' => 'General', + 'name' => 'Nombre', + ], + 'edit' => [ + 'title' => 'Editar Plantilla de Correo Electrónico', + 'save-btn' => 'Guardar Plantilla de Correo Electrónico', + 'email-template' => 'Plantilla de Correo Electrónico', + 'subject' => 'Asunto', + 'content' => 'Contenido', + 'subject-placeholders' => 'Marcadores de Posición del Asunto', + 'general' => 'General', + 'name' => 'Nombre', + ], + ], + 'marketing' => [ + 'events' => [ + 'index' => [ + 'create-btn' => 'Crear Evento', + 'title' => 'Eventos', + 'create-success' => 'Evento creado con éxito.', + 'update-success' => 'Evento actualizado con éxito.', + 'delete-success' => 'Evento eliminado con éxito.', + 'delete-failed' => 'No se puede eliminar el evento.', + 'mass-delete-success' => 'Eventos eliminados con éxito', + 'delete-failed-associated-campaigns' => 'No se puede eliminar el evento porque está asociado a campañas existentes. Por favor, desvincúlelas o actualícelas antes de eliminar.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + 'description' => 'Descripción', + 'date' => 'Fecha', + ], + 'create' => [ + 'title' => 'Crear Evento', + 'name' => 'Nombre', + 'date' => 'Fecha', + 'description' => 'Descripción', + 'save-btn' => 'Guardar Evento', + ], + 'edit' => [ + 'title' => 'Editar Evento', + ], + ], + ], + 'campaigns' => [ + 'index' => [ + 'create-btn' => 'Crear Campaña', + 'title' => 'Campañas', + 'create-success' => 'Campaña creada con éxito.', + 'update-success' => 'Campaña actualizada con éxito.', + 'delete-success' => 'Campaña eliminada con éxito.', + 'delete-failed' => 'No se puede eliminar la campaña.', + 'mass-delete-success' => 'Campañas eliminadas con éxito.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Nombre', + 'subject' => 'Asunto', + 'status' => 'Estado', + 'active' => 'Activo', + 'inactive' => 'Inactivo', + 'edit' => 'Editar', + 'delete' => 'Eliminar', + ], + 'create' => [ + 'title' => 'Crear Campaña', + 'name' => 'Nombre', + 'type' => 'Tipo', + 'subject' => 'Asunto', + 'event' => 'Evento', + 'email-template' => 'Plantilla de Correo', + 'status' => 'Estado', + ], + 'edit' => [ + 'title' => 'Editar Campaña', + ], + ], + ], + ], + 'tags' => [ + 'index' => [ + 'create-btn' => 'Crear Etiqueta', + 'title' => 'Etiquetas', + 'create-success' => 'Etiqueta creada exitosamente.', + 'update-success' => 'Etiqueta actualizada exitosamente.', + 'delete-success' => 'Etiqueta eliminada exitosamente.', + 'delete-failed' => 'No se puede eliminar la Etiqueta.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nombre', + 'users' => 'Usuarios', + 'created-at' => 'Creado En', + ], + 'create' => [ + 'name' => 'Nombre', + 'save-btn' => 'Guardar Etiqueta', + 'title' => 'Crear Etiqueta', + 'color' => 'Color', + ], + 'edit' => [ + 'title' => 'Editar Etiqueta', + ], + ], + ], + 'users' => [ + 'index' => [ + 'create-btn' => 'Crear Usuario', + 'create-success' => 'Usuario creado exitosamente.', + 'delete-failed' => 'No se puede eliminar el Usuario.', + 'delete-success' => 'Usuario eliminado exitosamente.', + 'last-delete-error' => 'Se requiere al menos un usuario.', + 'mass-delete-failed' => 'No se pueden eliminar los Usuarios.', + 'mass-delete-success' => 'Usuarios eliminados exitosamente.', + 'mass-update-failed' => 'No se pueden actualizar los Usuarios.', + 'mass-update-success' => 'Usuarios actualizados exitosamente.', + 'title' => 'Usuarios', + 'update-success' => 'Usuario actualizado exitosamente.', + 'user-define-error' => 'No se puede eliminar el usuario del sistema.', + 'active' => 'Activo', + 'inactive' => 'Inactivo', + 'datagrid' => [ + 'active' => 'Activo', + 'created-at' => 'Creado En', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'email' => 'Correo Electrónico', + 'id' => 'ID', + 'inactive' => 'Inactivo', + 'name' => 'Nombre', + 'status' => 'Estado', + 'update-status' => 'Actualizar Estado', + 'group' => 'Grupo Asociado', + 'no-group' => 'Sin Grupo', + 'users' => 'Usuarios', + ], + 'create' => [ + 'confirm-password' => 'Confirmar Contraseña', + 'email' => 'Correo Electrónico', + 'general' => 'General', + 'global' => 'Global', + 'group' => 'Grupo', + 'individual' => 'Individual', + 'name' => 'Nombre', + 'password' => 'Contraseña', + 'permission' => 'Permiso', + 'role' => 'Rol', + 'save-btn' => 'Guardar Usuario', + 'status' => 'Estado', + 'title' => 'Crear Usuario', + 'view-permission' => 'Ver Permiso', + 'select-at-lest-one-group' => 'Select at least one group', + ], + 'edit' => [ + 'title' => 'Editar Usuario', + ], + ], + ], + 'pipelines' => [ + 'index' => [ + 'title' => 'Canales', + 'create-btn' => 'Crear Canal', + 'create-success' => 'Canal creado exitosamente.', + 'update-success' => 'Canal actualizado exitosamente.', + 'default-required' => 'Se requiere al menos una canalización predeterminada.', + 'delete-success' => 'Canal eliminado exitosamente.', + 'delete-failed' => 'No se puede eliminar el Canal.', + 'default-delete-error' => 'No se puede eliminar el canal predeterminado.', + 'datagrid' => [ + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'id' => 'ID', + 'is-default' => 'Es Predeterminado', + 'name' => 'Nombre', + 'no' => 'No', + 'rotten-days' => 'Días de Pudrición', + 'yes' => 'Sí', + ], + ], + 'create' => [ + 'title' => 'Crear Canal', + 'save-btn' => 'Guardar Canal', + 'name' => 'Nombre', + 'rotten-days' => 'Días de Pudrición', + 'mark-as-default' => 'Marcar como Predeterminado', + 'general' => 'General', + 'probability' => 'Probabilidad (%)', + 'new-stage' => 'Nuevo', + 'won-stage' => 'Ganado', + 'lost-stage' => 'Perdido', + 'stage-btn' => 'Añadir Etapa', + 'stages' => 'Etapas', + 'duplicate-name' => 'El campo Nombre no puede ser duplicado', + 'delete-stage' => 'Eliminar Etapa', + 'add-new-stages' => 'Añadir Nuevas Etapas', + 'add-stage-info' => 'Añadir nueva etapa para tu Canal', + 'newly-added' => 'Añadido Recientemente', + 'stage-delete-success' => 'Etapa Eliminada Exitosamente', + ], + 'edit' => [ + 'title' => 'Editar Canal', + 'save-btn' => 'Guardar Canal', + 'name' => 'Nombre', + 'rotten-days' => 'Días de Pudrición', + 'mark-as-default' => 'Marcar como Predeterminado', + 'general' => 'General', + 'probability' => 'Probabilidad (%)', + 'new-stage' => 'Nuevo', + 'won-stage' => 'Ganado', + 'lost-stage' => 'Perdido', + 'stage-btn' => 'Añadir Etapa', + 'stages' => 'Etapas', + 'duplicate-name' => 'El campo Nombre no puede ser duplicado', + 'delete-stage' => 'Eliminar Etapa', + 'add-new-stages' => 'Añadir Nuevas Etapas', + 'add-stage-info' => 'Añadir nueva etapa para tu Canal', + 'stage-delete-success' => 'Etapa Eliminada Exitosamente', + ], + ], + 'webhooks' => [ + 'index' => [ + 'title' => 'Webhooks', + 'create-btn' => 'Crear Webhook', + 'create-success' => 'Webhook creado exitosamente.', + 'update-success' => 'Webhook actualizado exitosamente.', + 'delete-success' => 'Webhook eliminado exitosamente.', + 'delete-failed' => 'No se puede eliminar el Webhook.', + 'invalid-endpoint' => 'La URL del endpoint del webhook debe ser una dirección HTTP o HTTPS pública.', + 'datagrid' => [ + 'id' => 'ID', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'name' => 'Nombre', + 'entity-type' => 'Tipo de Entidad', + 'end-point' => 'Punto Final', + ], + ], + 'create' => [ + 'title' => 'Crear Webhook', + 'save-btn' => 'Guardar Webhook', + 'info' => 'Ingrese los detalles del webhook', + 'url-and-parameters' => 'URL Y Parámetros', + 'method' => 'Método', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'Punto Final de URL', + 'parameters' => 'Parámetros', + 'add-new-parameter' => 'Añadir Nuevo Parámetro', + 'url-preview' => 'Vista Previa de URL:', + 'headers' => 'Encabezados', + 'add-new-header' => 'Añadir Nuevo Encabezado', + 'body' => 'Cuerpo', + 'default' => 'Predeterminado', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Clave y Valor', + 'add-new-payload' => 'Añadir nueva carga', + 'raw' => 'Crudo', + 'general' => 'General', + 'name' => 'Nombre', + 'entity-type' => 'Tipo de Entidad', + 'insert-placeholder' => 'Insertar Marcador de Posición', + 'description' => 'Descripción', + 'json' => 'Json', + 'text' => 'Texto', + ], + 'edit' => [ + 'title' => 'Editar Webhook', + 'edit-btn' => 'Guardar Webhook', + 'save-btn' => 'Guardar Webhook', + 'info' => 'Ingrese los detalles del webhook', + 'url-and-parameters' => 'URL Y Parámetros', + 'method' => 'Método', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'Punto Final de URL', + 'parameters' => 'Parámetros', + 'add-new-parameter' => 'Añadir Nuevo Parámetro', + 'url-preview' => 'Vista Previa de URL:', + 'headers' => 'Encabezados', + 'add-new-header' => 'Añadir Nuevo Encabezado', + 'body' => 'Cuerpo', + 'default' => 'Predeterminado', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Clave y Valor', + 'add-new-payload' => 'Añadir nueva carga', + 'raw' => 'Crudo', + 'general' => 'General', + 'name' => 'Nombre', + 'entity-type' => 'Tipo de Entidad', + 'insert-placeholder' => 'Insertar Marcador de Posición', + 'description' => 'Descripción', + 'json' => 'Json', + 'text' => 'Texto', + ], + ], + 'warehouses' => [ + 'index' => [ + 'title' => 'Almacenes', + 'create-btn' => 'Crear Almacén', + 'create-success' => 'Almacén creado con éxito.', + 'name-exists' => 'El nombre del almacén ya existe.', + 'update-success' => 'Almacén actualizado con éxito.', + 'delete-success' => 'Almacén eliminado con éxito.', + 'delete-failed' => 'No se puede eliminar el almacén.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Nombre', + 'contact-name' => 'Nombre del Contacto', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'view' => 'Ver', + 'created-at' => 'Creado En', + 'products' => 'Productos', + 'contact-emails' => 'Correos Electrónicos de Contacto', + 'contact-numbers' => 'Números de Teléfono de Contacto', + ], + ], + 'create' => [ + 'title' => 'Crear Almacén', + 'save-btn' => 'Guardar Almacén', + 'contact-info' => 'Información de Contacto', + ], + 'edit' => [ + 'title' => 'Editar Almacén', + 'save-btn' => 'Guardar Almacén', + 'contact-info' => 'Información de Contacto', + ], + 'view' => [ + 'all' => 'Todos', + 'notes' => 'Notas', + 'files' => 'Archivos', + 'location' => 'Ubicación', + 'change-logs' => 'Registros de Cambios', + 'locations' => [ + 'action' => 'Acción', + 'add-location' => 'Agregar Ubicación', + 'create-success' => 'Ubicación creada con éxito.', + 'delete' => 'Eliminar', + 'delete-failed' => 'No se puede eliminar la ubicación.', + 'delete-success' => 'Ubicación eliminada con éxito.', + 'name' => 'Nombre', + 'save-btn' => 'Guardar', + ], + 'general-information' => [ + 'title' => 'Información General', + ], + 'contact-information' => [ + 'title' => 'Información de Contacto', + ], + ], + ], + 'attributes' => [ + 'index' => [ + 'title' => 'Atributos', + 'create-btn' => 'Crear Atributo', + 'create-success' => 'Atributo creados con éxito.', + 'update-success' => 'Atributo actualizados con éxito.', + 'delete-success' => 'Atributo eliminados con éxito.', + 'delete-failed' => 'No se pueden eliminar los atributo.', + 'user-define-error' => 'No se puede eliminar el atributo del sistema.', + 'mass-delete-failed' => 'No se pueden eliminar los atributos del sistema.', + 'datagrid' => [ + 'yes' => 'Sí', + 'no' => 'No', + 'id' => 'ID', + 'code' => 'Código', + 'name' => 'Nombre', + 'entity-type' => 'Tipo de Entidad', + 'type' => 'Tipo', + 'is-default' => 'Es Predeterminado', + 'edit' => 'Editar', + 'delete' => 'Eliminar', + 'quick-add' => 'Agregar Rápido', + 'entity-types' => [ + 'leads' => 'Clientes potenciales', + 'organizations' => 'Organizaciones', + 'persons' => 'Personas', + 'products' => 'Productos', + 'quotes' => 'Cotizaciones', + 'warehouses' => 'Almacenes', + ], + 'types' => [ + 'text' => 'Texto', + 'textarea' => 'Área de texto', + 'price' => 'Precio', + 'boolean' => 'Booleano', + 'select' => 'Seleccionar', + 'multiselect' => 'Selección múltiple', + 'checkbox' => 'Casilla de verificación', + 'email' => 'Correo electrónico', + 'address' => 'Dirección', + 'phone' => 'Teléfono', + 'lookup' => 'Búsqueda', + 'datetime' => 'Fecha y hora', + 'date' => 'Fecha', + 'image' => 'Imagen', + 'file' => 'Archivo', + ], + ], + ], + 'create' => [ + 'title' => 'Crear Atributo', + 'save-btn' => 'Guardar Atributo', + 'code' => 'Código', + 'name' => 'Nombre', + 'quick_add' => 'Añadir Rápido', + 'entity-type' => 'Tipo de Entidad', + 'type' => 'Tipo', + 'validations' => 'Validaciones', + 'is-required' => 'Es Requerido', + 'input-validation' => 'Validación de Entrada', + 'is-unique' => 'Es Único', + 'labels' => 'Etiquetas', + 'general' => 'General', + 'none' => 'Ninguno', + 'numeric' => 'Numérico', + 'decimal' => 'Decimal', + 'url' => 'Url', + 'options' => 'Opciones', + 'option-type' => 'Tipo de Opción', + 'lookup-type' => 'Tipo de Búsqueda', + 'add-option' => 'Agregar Opción', + 'save-option' => 'Guardar Opción', + 'option-name' => 'Nombre de Opción', + 'add-attribute-options' => 'Agregar Opciones de Atributo', + 'text' => 'Texto', + 'textarea' => 'Área de Texto', + 'price' => 'Precio', + 'boolean' => 'Booleano', + 'select' => 'Seleccionar', + 'multiselect' => 'Selección Múltiple', + 'email' => 'Correo Electrónico', + 'address' => 'Dirección', + 'phone' => 'Teléfono', + 'datetime' => 'Fecha y Hora', + 'date' => 'Fecha', + 'image' => 'Imagen', + 'file' => 'Archivo', + 'lookup' => 'Búsqueda', + 'entity_type' => 'Tipo de Entidad', + 'checkbox' => 'Casilla de Verificación', + 'is_required' => 'Es Requerido', + 'is_unique' => 'Es Único', + 'actions' => 'Acciones', + ], + 'edit' => [ + 'actions' => 'Acciones', + 'add-attribute-options' => 'Agregar Opciones de Atributo', + 'add-option' => 'Agregar Opción', + 'address' => 'Dirección', + 'boolean' => 'Booleano', + 'checkbox' => 'Casilla de Verificación', + 'code' => 'Código', + 'date' => 'Fecha', + 'datetime' => 'Fecha y Hora', + 'decimal' => 'Decimal', + 'email' => 'Correo Electrónico', + 'entity-type' => 'Tipo de Entidad', + 'entity_type' => 'Tipo de Entidad', + 'file' => 'Archivo', + 'general' => 'General', + 'image' => 'Imagen', + 'input-validation' => 'Validación de Entrada', + 'is-required' => 'Es Requerido', + 'is-unique' => 'Es Único', + 'is_required' => 'Es Requerido', + 'is_unique' => 'Es Único', + 'labels' => 'Etiquetas', + 'lookup' => 'Búsqueda', + 'lookup-type' => 'Tipo de Búsqueda', + 'multiselect' => 'Selección Múltiple', + 'name' => 'Nombre', + 'quick_add' => 'Añadir Rápido', + 'none' => 'Ninguno', + 'numeric' => 'Numérico', + 'option-deleted' => 'Opción de Atributo eliminada con éxito', + 'option-name' => 'Nombre de Opción', + 'option-type' => 'Tipo de Opción', + 'options' => 'Opciones', + 'phone' => 'Teléfono', + 'price' => 'Precio', + 'save-btn' => 'Guardar Atributo', + 'save-option' => 'Guardar Opción', + 'select' => 'Seleccionar', + 'text' => 'Texto', + 'textarea' => 'Área de Texto', + 'title' => 'Editar Atributo', + 'type' => 'Tipo', + 'url' => 'Url', + 'validations' => 'Validaciones', + ], + ], + 'data-transfer' => [ + 'importers' => [ + 'persons' => [ + 'title' => 'Personas', + 'validation' => [ + 'errors' => [ + 'duplicate-email' => 'Correo electrónico: \'%s\' se encontró más de una vez en el archivo de importación.', + 'duplicate-phone' => 'Teléfono: \'%s\' se encontró más de una vez en el archivo de importación.', + 'email-not-found' => 'Correo electrónico: \'%s\' no se encontró en el sistema.', + 'invalid-date' => 'Fecha: \'%s\' no es una fecha válida.', + ], + ], + ], + 'products' => [ + 'title' => 'Productos', + 'validation' => [ + 'errors' => [ + 'sku-not-found' => 'Producto con el SKU especificado no encontrado.', + ], + ], + ], + 'leads' => [ + 'title' => 'Clientes Potenciales', + 'validation' => [ + 'errors' => [ + 'id-not-found' => 'ID: \'%s\' no se encuentra en el sistema.', + ], + ], + ], + ], + 'validation' => [ + 'errors' => [ + 'column-empty-headers' => 'Las columnas número "%s" tienen encabezados vacíos.', + 'column-name-invalid' => 'Nombres de columnas no válidos: "%s".', + 'column-not-found' => 'No se encontraron las columnas requeridas: %s.', + 'column-numbers' => 'El número de columnas no corresponde al número de filas en el encabezado.', + 'invalid-attribute' => 'El encabezado contiene atributos no válidos: "%s".', + 'system' => 'Ocurrió un error inesperado en el sistema.', + 'wrong-quotes' => 'Se usaron comillas curvas en lugar de comillas rectas.', + 'already-exists' => 'The :attribute already exists.', + ], + ], + 'imports' => [ + 'create' => [ + 'action' => 'Acción', + 'allowed-errors' => 'Errores Permitidos', + 'back-btn' => 'Atrás', + 'create-update' => 'Crear/Actualizar', + 'delete' => 'Eliminar', + 'download-sample' => 'Descargar Muestra', + 'field-separator' => 'Separador de Campos', + 'file' => 'Archivo', + 'general' => 'General', + 'images-directory' => 'Ruta del Directorio de Imágenes', + 'process-in-queue' => 'Procesar en Cola', + 'results' => 'Resultados', + 'save-btn' => 'Guardar Importación', + 'settings' => 'Configuraciones', + 'skip-errors' => 'Omitir Errores', + 'stop-on-errors' => 'Detener en Errores', + 'title' => 'Crear Importación', + 'type' => 'Tipo', + 'validation-strategy' => 'Estrategia de Validación', + ], + 'edit' => [ + 'action' => 'Acción', + 'allowed-errors' => 'Errores Permitidos', + 'back-btn' => 'Atrás', + 'create-update' => 'Crear/Actualizar', + 'delete' => 'Eliminar', + 'download-sample' => 'Descargar Muestra', + 'field-separator' => 'Separador de Campos', + 'file' => 'Archivo', + 'general' => 'General', + 'images-directory' => 'Ruta del Directorio de Imágenes', + 'process-in-queue' => 'Procesar en Cola', + 'results' => 'Resultados', + 'save-btn' => 'Guardar Importación', + 'settings' => 'Configuraciones', + 'skip-errors' => 'Omitir Errores', + 'stop-on-errors' => 'Detener en Errores', + 'title' => 'Editar Importación', + 'type' => 'Tipo', + 'validation-strategy' => 'Estrategia de Validación', + ], + 'index' => [ + 'button-title' => 'Crear Importación', + 'title' => 'Importaciones', + 'datagrid' => [ + 'actions' => 'Acciones', + 'completed-at' => 'Completado en', + 'created' => 'Creado', + 'delete' => 'Eliminar', + 'deleted' => 'Eliminado', + 'edit' => 'Editar', + 'error-file' => 'Archivo de Errores', + 'id' => 'ID', + 'started-at' => 'Iniciado en', + 'state' => 'Estado', + 'summary' => 'Resumen', + 'type' => 'Tipo', + 'updated' => 'Actualizado', + 'uploaded-file' => 'Archivo Subido', + ], + ], + 'import' => [ + 'back-btn' => 'Atrás', + 'completed-batches' => 'Total de Lotes Completados:', + 'download-error-report' => 'Descargar Informe Completo', + 'edit-btn' => 'Editar', + 'imported-info' => '¡Felicidades! Tu importación fue exitosa.', + 'importing-info' => 'Importación en Proceso', + 'importing-queue-hint' => 'Si esta importación permanece en 0%, es posible que el worker de la cola no esté en ejecución. Pida al administrador que ejecute \'php artisan queue:work\', o desactive \'Procesar en cola\' e importe de nuevo.', + 'indexing-info' => 'Indexación de Recursos (Precios, Inventario y Elastic Search) en Progreso', + 'linking-info' => 'Vinculación de Recursos en Progreso', + 'progress' => 'Progreso:', + 'title' => 'Importación', + 'total-batches' => 'Total de Lotes:', + 'total-created' => 'Total de Registros Creados:', + 'total-deleted' => 'Total de Registros Eliminados:', + 'total-errors' => 'Total de Errores:', + 'total-invalid-rows' => 'Total de Filas Inválidas:', + 'total-rows-processed' => 'Total de Filas Procesadas:', + 'total-updated' => 'Total de Registros Actualizados:', + 'validate' => 'Validar', + 'validate-info' => 'Haz clic en Validar Datos para comprobar tu importación.', + 'validating-info' => 'La lectura y validación de los datos ha comenzado', + 'validation-failed-info' => 'Tu importación no es válida. Por favor, corrige los siguientes errores e intenta de nuevo.', + 'validation-success-info' => 'Tu importación es válida. Haz clic en Importar para iniciar el proceso de importación.', + ], + 'create-success' => 'Importación creada exitosamente.', + 'delete-failed' => 'La eliminación de la importación falló inesperadamente.', + 'delete-success' => 'Importación eliminada exitosamente.', + 'not-valid' => 'La importación no es válida', + 'nothing-to-import' => 'No hay recursos para importar.', + 'queue-worker-not-running' => 'El worker de la cola no está en ejecución. Ejecute php artisan queue:work para procesar esta importación, o desactive Procesar en cola e inténtelo de nuevo.', + 'setup-queue-error' => 'Por favor, cambia tu controlador de cola a "database" o "redis" para iniciar el proceso de importación.', + 'update-success' => 'Importación actualizada exitosamente.', + ], + ], + ], + 'activities' => [ + 'index' => [ + 'title' => 'Actividades', + 'datagrid' => [ + 'comment' => 'Comentario', + 'created_at' => 'Creado En', + 'created_by' => 'Creado Por', + 'edit' => 'Editar', + 'id' => 'ID', + 'done' => 'Completado', + 'not-done' => 'No Completado', + 'lead' => 'Oportunidad', + 'mass-delete' => 'Eliminación Masiva', + 'mass-update' => 'Actualización Masiva', + 'schedule-from' => 'Programar Desde', + 'schedule-to' => 'Programar Hasta', + 'schedule_from' => 'Programar Desde', + 'schedule_to' => 'Programar Hasta', + 'title' => 'Título', + 'is_done' => 'Completado', + 'type' => 'Tipo', + 'update' => 'Actualizar', + 'call' => 'Llamada', + 'meeting' => 'Reunión', + 'lunch' => 'Almuerzo', + ], + ], + 'edit' => [ + 'title' => 'Editar Actividad', + 'back-btn' => 'Volver', + 'save-btn' => 'Guardar Actividad', + 'type' => 'Tipo de Actividad', + 'call' => 'Llamada', + 'meeting' => 'Reunión', + 'lunch' => 'Almuerzo', + 'schedule_to' => 'Programar Hasta', + 'schedule_from' => 'Programar Desde', + 'location' => 'Ubicación', + 'comment' => 'Comentario', + 'lead' => 'Oportunidad', + 'participants' => 'Participantes', + 'general' => 'General', + 'persons' => 'Personas', + 'no-result-found' => 'No se encontraron registros.', + 'users' => 'Usuarios', + ], + 'updated' => 'Actualizado :attribute', + 'created' => 'Creado', + 'duration-overlapping' => 'Los participantes tienen otra reunión en este momento. ¿Deseas continuar?', + 'create-success' => 'Actividad creada con éxito.', + 'update-success' => 'Actividad actualizada con éxito.', + 'overlapping-error' => 'Los participantes tienen otra reunión en este momento.', + 'destroy-success' => 'Actividad eliminada con éxito.', + 'delete-failed' => 'No se puede eliminar la actividad.', + 'mass-update-success' => 'Actividades actualizadas con éxito.', + 'mass-destroy-success' => 'Actividades eliminadas con éxito.', + 'mass-delete-failed' => 'No se pueden eliminar las actividades.', + ], + 'mail' => [ + 'index' => [ + 'compose' => 'Redactar', + 'draft' => 'Borrador', + 'inbox' => 'Bandeja de entrada', + 'outbox' => 'Bandeja de salida', + 'sent' => 'Enviados', + 'trash' => 'Papelera', + 'compose-mail-btn' => 'Redactar Correo', + 'btn' => 'Correo', + 'mail' => [ + 'title' => 'Redactar Correo', + 'to' => 'Para', + 'enter-emails' => 'Presiona enter para añadir correos', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Asunto', + 'send-btn' => 'Enviar', + 'message' => 'Mensaje', + 'draft' => 'Borrador', + ], + 'datagrid' => [ + 'id' => 'ID', + 'from' => 'De', + 'to' => 'Para', + 'subject' => 'Asunto', + 'tags' => 'Etiquetas', + 'content' => 'Contenido', + 'attachments' => 'Archivos adjuntos', + 'date' => 'Fecha', + 'move-to-inbox' => 'Mover a la bandeja de entrada', + 'move-to-trash' => 'Movido a la papelera', + 'edit' => 'Editar', + 'view' => 'Ver', + 'delete' => 'Eliminar', + ], + ], + 'create-success' => 'Correo enviado con éxito.', + 'update-success' => 'Correo actualizado con éxito.', + 'mass-update-success' => 'Correos actualizados con éxito.', + 'delete-success' => 'Correo eliminado con éxito.', + 'delete-failed' => 'No se puede eliminar el correo.', + 'invalid-route' => 'Ruta inválida para el correo.', + 'unauthorized' => 'Esta acción no está autorizada.', + 'view' => [ + 'title' => 'Correos', + 'subject' => ':subject', + 'link-mail' => 'Enlace de Correo', + 'to' => 'Para', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'reply' => 'Responder', + 'reply-all' => 'Responder a Todos', + 'forward' => 'Reenviar', + 'delete' => 'Eliminar', + 'enter-mails' => 'Ingresar id de correo', + 'rotten-days' => 'El lead está en estado crítico por :days días', + 'search-an-existing-lead' => 'Buscar un lead existente', + 'search-an-existing-contact' => 'Buscar un contacto existente', + 'message' => 'Mensaje', + 'add-attachments' => 'Añadir Archivos Adjuntos', + 'discard' => 'Descartar', + 'send' => 'Enviar', + 'no-result-found' => 'No se encontraron resultados', + 'add-new-contact' => 'Añadir Nuevo Contacto', + 'description' => 'Descripción', + 'search' => 'Buscar...', + 'add-new-lead' => 'Añadir Nuevo Lead', + 'create-new-contact' => 'Crear Nuevo Contacto', + 'save-contact' => 'Guardar Contacto', + 'create-lead' => 'Crear Lead', + 'linked-contact' => 'Contacto Vinculado', + 'link-to-contact' => 'Vincular a Contacto', + 'link-to-lead' => 'Vincular a Lead', + 'linked-lead' => 'Lead Vinculado', + 'lead-details' => 'Detalles del Lead', + 'contact-person' => 'Persona de Contacto', + 'product' => 'Producto', + 'tags' => [ + 'create-success' => 'Etiqueta creada con éxito.', + 'destroy-success' => 'Etiqueta eliminada con éxito.', + ], + ], + ], + 'common' => [ + 'custom-attributes' => [ + 'add-more' => 'Añadir Más', + 'address' => 'Dirección', + 'city' => 'Ciudad', + 'contact' => 'Números de Contacto', + 'country' => 'País', + 'email' => 'Correo Electrónico', + 'home' => 'Hogar', + 'postcode' => 'Código Postal', + 'save' => 'Guardar', + 'select' => 'Seleccionar', + 'select-country' => 'Seleccionar País', + 'select-state' => 'Seleccionar Estado', + 'state' => 'Estado', + 'update-contact-title' => 'Actualizar Números de Contacto', + 'update-emails-title' => 'Actualizar Correos Electrónicos de Contacto', + 'work' => 'Trabajo', + ], + ], + 'leads' => [ + 'create-success' => 'Lead creado exitosamente.', + 'update-success' => 'Lead actualizado exitosamente.', + 'update-failed' => 'No se pueden eliminar los clientes potenciales.', + 'destroy-success' => 'Lead eliminado exitosamente.', + 'destroy-failed' => 'No se puede eliminar el lead.', + 'file' => [ + 'data-not-found' => 'Datos no encontrados.', + 'empty-content' => 'El contenido del PDF está vacío o no se pudo extraer.', + 'failed-extract' => 'No se pudo extraer el texto del archivo.', + 'insufficient-info' => 'Debido a datos insuficientes, no podemos procesar su solicitud en este momento.', + 'invalid-base64' => 'Formato base64 inválido.', + 'invalid-format' => 'Formato JSON inválido.', + 'invalid-response' => 'Formato de respuesta de IA inválido.', + 'missing-api-key' => 'Falta la clave API o la configuración del modelo.', + 'not-found' => 'Archivo no encontrado.', + 'recursive-call' => 'Se detectó una llamada recursiva.', + 'text-generation-failed' => 'La extracción de texto falló. El archivo podría estar vacío o ilegible.', + ], + 'index' => [ + 'title' => 'Leads', + 'create-btn' => 'Crear Lead', + 'datagrid' => [ + 'id' => 'ID', + 'sales-person' => 'Persona de Ventas', + 'subject' => 'Asunto', + 'source' => 'Fuente', + 'lead-value' => 'Valor Estimado del Lead', + 'lead-type' => 'Tipo de cliente potencial', + 'tag-name' => 'Nombre de la etiqueta', + 'contact-person' => 'Persona de Contacto', + 'stage' => 'Etapa', + 'rotten-lead' => 'Lead Podrido', + 'date-to' => 'Fecha Hasta', + 'created-at' => 'Creado En', + 'no' => 'No', + 'yes' => 'Sí', + 'delete' => 'Eliminar', + 'mass-delete' => 'Eliminar en Masa', + 'mass-update' => 'Actualizar en Masa', + ], + 'kanban' => [ + 'rotten-days' => 'El lead está podrido desde hace :days días', + 'empty-list' => 'Tu lista de leads está vacía', + 'empty-list-description' => 'Crea un lead para organizar tus objetivos.', + 'create-lead-btn' => 'Crear Lead', + 'columns' => [ + 'contact-person' => 'Persona de Contacto', + 'id' => 'ID', + 'lead-type' => 'Tipo de Lead', + 'lead-value' => 'Valor Estimado del Lead', + 'sales-person' => 'Persona de Ventas', + 'source' => 'Fuente', + 'title' => 'Título', + 'tags' => 'Etiquetas', + 'expected-close-date' => 'Fecha de Cierre Esperada', + 'date-to' => 'Fecha Hasta', + 'created-at' => 'Creado En', + ], + 'toolbar' => [ + 'search' => [ + 'title' => 'Buscar por título', + ], + 'filters' => [ + 'apply-filters' => 'Aplicar Filtros', + 'clear-all' => 'Limpiar Todo', + 'filter' => 'Filtrar', + 'filters' => 'Filtros', + 'from' => 'De', + 'select' => 'Seleccionar', + 'to' => 'A', + ], + 'card-settings' => [ + 'title' => 'Configuración de Tarjeta', + 'contact-person' => 'Persona de Contacto', + 'rotten-indicator' => 'Indicador de Podrido', + 'sales-person' => 'Persona de Ventas', + 'estimated-lead-value' => 'Valor Estimado del Lead', + 'source' => 'Fuente', + 'lead-type' => 'Tipo de Lead', + 'tags' => 'Etiquetas', + ], + ], + 'stages' => [ + 'won' => 'Ganado', + 'lost' => 'Perdido', + 'need-more-info' => 'Necesita Más Información', + 'closed-at' => 'Cerrado En', + 'won-value' => 'Valor Ganado', + 'lost-reason' => 'Razón de la Pérdida', + 'save-btn' => 'Guardar', + ], + ], + 'view-switcher' => [ + 'all-pipelines' => 'Todos los Canales', + 'create-new-pipeline' => 'Crear Nuevo Canal', + ], + 'upload' => [ + 'create-lead' => 'Crear Lead Usando IA', + 'file' => 'Carga de archivo', + 'file-info' => 'Solo se aceptan archivos en formato pdf, bmp, jpg, jpeg, png.', + 'file-required' => 'Por favor, selecciona al menos un archivo válido para continuar.', + 'save-btn' => 'Guardar', + 'upload-file' => 'Subir archivo', + ], + ], + 'create' => [ + 'title' => 'Crear Lead', + 'save-btn' => 'Guardar', + 'details' => 'Detalles', + 'details-info' => 'Introduce la Información Básica del Lead', + 'contact-person' => 'Persona de Contacto', + 'contact-info' => 'Información Sobre la Persona de Contacto', + 'products' => 'Productos', + 'products-info' => 'Información Sobre los Productos', + ], + 'edit' => [ + 'title' => 'Editar Lead', + 'save-btn' => 'Guardar', + 'details' => 'Detalles', + 'details-info' => 'Introduce la Información Básica del Lead', + 'contact-person' => 'Persona de Contacto', + 'contact-info' => 'Información Sobre la Persona de Contacto', + 'products' => 'Productos', + 'products-info' => 'Información Sobre los Productos', + ], + 'common' => [ + 'contact' => [ + 'name' => 'Nombre', + 'name-search-placeholder' => 'Buscar por nombre, correo electrónico y número', + 'email' => 'Correo Electrónico', + 'contact-number' => 'Número de Contacto', + 'organization' => 'Organización', + ], + 'products' => [ + 'product-name' => 'Nombre del Producto', + 'quantity' => 'Cantidad', + 'price' => 'Precio', + 'amount' => 'Monto', + 'action' => 'Acción', + 'add-more' => 'Agregar Más', + 'total' => 'Total', + ], + ], + 'view' => [ + 'title' => 'Lead: :title', + 'rotten-days' => ':days Días', + 'tabs' => [ + 'description' => 'Descripción', + 'products' => 'Productos', + 'quotes' => 'Cotizaciones', + ], + 'attributes' => [ + 'title' => 'Sobre el Lead', + ], + 'quotes' => [ + 'id' => 'ID', + 'subject' => 'Asunto', + 'expired-at' => 'Expirado En', + 'sub-total' => 'Subtotal', + 'discount' => 'Descuento', + 'tax' => 'Impuesto', + 'adjustment' => 'Ajuste', + 'grand-total' => 'Total General', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'download' => 'Descargar', + 'destroy-success' => 'Cotización eliminada exitosamente.', + 'empty-title' => 'No se Encontraron Cotizaciones', + 'empty-info' => 'No se Encontraron Cotizaciones para este Lead', + 'add-btn' => 'Agregar Cotización', + 'mail' => 'Correo', + 'quote-not-found' => 'No se encontró la cotización para este lead.', + 'person-email-unavailable' => 'El correo de la persona del lead no está disponible.', + 'mail-subject' => 'Cotización: :subject', + 'mail-body' => 'Por favor, encuentre adjunto el PDF de la cotización. Correo generado por el sistema.', + 'mail-send-failed' => 'No se pudo enviar el correo de la cotización.', + ], + 'products' => [ + 'product-name' => 'Nombre del Producto', + 'quantity' => 'Cantidad', + 'price' => 'Precio', + 'amount' => 'Monto', + 'action' => 'Acción', + 'add-more' => 'Agregar Más', + 'total' => 'Total', + 'empty-title' => 'No se Encontraron Productos', + 'empty-info' => 'No se Encontraron Productos para este Lead', + 'add-product' => 'Agregar Producto', + ], + 'persons' => [ + 'title' => 'Sobre las Personas', + 'job-title' => ':job_title en :organization', + 'no-person' => 'No se ha asignado ninguna persona de contacto a este lead.', + 'attach-btn' => 'Adjuntar Persona de Contacto', + 'change-btn' => 'Cambiar', + 'change-title' => 'Cambiar Persona de Contacto', + 'save-btn' => 'Guardar', + 'cancel-btn' => 'Cancelar', + ], + 'stages' => [ + 'won' => 'Ganado', + 'lost' => 'Perdido', + 'need-more-info' => 'Necesita Más Información', + 'closed-at' => 'Cerrado En', + 'won-value' => 'Valor Ganado', + 'lost-reason' => 'Razón de la Pérdida', + 'save-btn' => 'Guardar', + ], + 'tags' => [ + 'create-success' => 'Etiqueta creada con éxito.', + 'destroy-success' => 'Etiqueta eliminada con éxito.', + ], + ], + ], + 'configuration' => [ + 'index' => [ + 'back' => 'Regresar', + 'delete' => 'Eliminar', + 'save-btn' => 'Guardar Configuración', + 'save-success' => 'Configuración Guardada Exitosamente.', + 'search' => 'Buscar', + 'select-country' => 'Seleccionar País', + 'select-state' => 'Seleccionar Estado', + 'title' => 'Configuración', + 'general' => [ + 'title' => 'General', + 'info' => 'Configuración General', + 'general' => [ + 'title' => 'General', + 'info' => 'Actualiza tus configuraciones generales aquí.', + 'locale-settings' => [ + 'title' => 'Configuraciones de Idioma', + 'title-info' => 'Define el idioma utilizado en la interfaz del panel de administración. La lista desplegable muestra todos los idiomas disponibles en su instalación.', + ], + 'admin-logo' => [ + 'logo-image' => 'Imagen del Logo', + 'title' => 'Logo del Administrador', + 'title-info' => 'Configura la imagen del logo para tu panel de administración.', + 'favicon-image' => 'Imagen del Favicon', + 'favicon-image-info' => 'Configura la imagen del favicon para tu panel de administración.', + ], + ], + 'settings' => [ + 'title' => 'Configuraciones', + 'info' => 'Actualiza tus configuraciones aquí.', + 'footer' => [ + 'show' => 'Mostrar "Desarrollado por"', + 'show-info' => 'Muestra u oculta la barra "Desarrollado por" en la parte inferior del panel de administración.', + 'info' => 'Podemos configurar la sección de powered by aquí.', + 'powered-by' => 'Editor de texto impulsado por', + 'title' => 'Configuraciones de la Sección Powered by', + ], + 'menu' => [ + 'activities' => 'Actividades', + 'configuration' => 'Configuración', + 'contacts' => 'Contactos', + 'dashboard' => 'Tablero', + 'draft' => 'Borrador', + 'inbox' => 'Bandeja de Entrada', + 'info' => 'Podemos configurar los nombres de los elementos del menú aquí.', + 'leads' => 'Leads', + 'mail' => 'Correo', + 'organizations' => 'Organizaciones', + 'outbox' => 'Bandeja de Salida', + 'persons' => 'Personas', + 'products' => 'Productos', + 'quotes' => 'Cotizaciones', + 'sent' => 'Enviados', + 'settings' => 'Configuraciones', + 'title' => 'Configuraciones de Elementos del Menú', + 'trash' => 'Papelera', + ], + 'dashboard' => [ + 'title' => 'Configuraciones del Panel', + 'info' => 'Aquí podemos configurar el rango de fechas predeterminado del panel.', + 'date-range' => 'Rango de Fechas Predeterminado', + 'date-range-info' => 'El período con el que se carga el panel. Los selectores de fecha del panel aún se pueden ajustar para una vista puntual.', + 'custom-days' => 'Rango Personalizado (Días)', + 'custom-days-info' => 'Número de días a consultar cuando el rango de fechas es Personalizado.', + 'ranges' => [ + '1-month' => '1 mes', + '3-months' => '3 meses', + '9-months' => '9 meses', + '1-year' => '1 año', + '2-years' => '2 años', + 'custom' => 'Personalizado', + ], + ], + 'menu-color' => [ + 'brand-color' => 'Color de Marca', + 'info' => 'Podemos cambiar los colores de los elementos del menú aquí.', + 'title' => 'Configuraciones de Color de Elementos del Menú', + ], + ], + ], + 'email' => [ + 'title' => 'Configuración de Correo Electrónico', + 'info' => 'Configuración de correo electrónico para la aplicación.', + 'imap' => [ + 'title' => 'Configuración IMAP', + 'info' => 'Configuración de correo electrónico IMAP para recibir correos electrónicos.', + 'account' => [ + 'title' => 'Cuenta IMAP', + 'title-info' => 'Configura los ajustes de tu cuenta IMAP aquí.', + 'host' => 'Host', + 'port' => 'Puerto', + 'encryption' => 'Tipo de Cifrado', + 'validate-cert' => 'Validar Certificado SSL', + 'username' => 'Nombre de Usuario IMAP', + 'password' => 'Contraseña IMAP', + ], + ], + ], + 'magic-ai' => [ + 'title' => 'Magic AI', + 'info' => 'Configuración de Magic AI para la aplicación.', + 'settings' => [ + 'api-key' => 'Clave API', + 'api-key-info' => 'Recuerda usar una clave API de OpenRouter para cada modelo. Es un paso simple para mejorar la seguridad y el rendimiento.', + 'enable' => 'Habilitar', + 'info' => 'Mejora tu experiencia con Magic AI con tu clave API de OpenRouter. ¡Intégrala ahora para una aventura de IA personalizada y sin problemas, hecha a tu medida! Personaliza la configuración sin esfuerzo y toma el control de tu viaje de IA.', + 'other' => 'Otro Modelo', + 'other-model' => 'Para otros modelos, usa el ID del Modelo de OpenRouter.', + 'doc-generation' => 'Generación de DOC', + 'doc-generation-info' => 'Habilita la función de generación de DOC para extraer automáticamente datos de archivos DOC y convertirlos a formato de texto. Mejora tu productividad y eficiencia habilitando esta función para agilizar tu flujo de trabajo.', + 'title' => 'Configuraciones Generales', + 'models' => [ + 'deepseek-r1' => 'Deepseek R1 Distill-llama-8b', + 'gemini-2-0-flash-001' => 'Gemini 2.0 flash-001', + 'gpt-4o' => 'GPT-4.0', + 'gpt-4o-mini' => 'GPT-4.0 mini', + 'grok-2-1212' => 'Grok 2.12', + 'llama-3-2-3b-instruct' => 'Llama 3.2 3b Instruct', + 'title' => 'Modelos', + ], + ], + ], + ], + ], + 'dashboard' => [ + 'index' => [ + 'title' => 'Tablero', + 'start-date' => 'Start Date', + 'end-date' => 'End Date', + 'export-pdf' => 'Exportar PDF', + 'pdf' => [ + 'title' => 'Informe del Tablero', + 'filtered-date' => 'Fecha Filtrada', + 'revenue' => 'Ingresos', + 'overview' => 'Resumen', + 'total-leads' => 'Total de Leads', + 'metric' => 'Métrica', + 'current' => 'Actual', + 'previous' => 'Anterior', + 'progress' => 'Progreso', + 'date' => 'Fecha', + 'all' => 'Todos', + 'total' => 'Total', + 'won' => 'Ganados', + 'lost' => 'Perdidos', + 'source' => 'Fuente', + 'type' => 'Tipo', + 'stage' => 'Etapa', + 'product' => 'Producto', + 'person' => 'Persona', + 'price' => 'Precio', + 'revenue-by-sources' => 'Ingresos por Fuentes', + 'revenue-by-types' => 'Ingresos por Tipos', + 'open-leads-by-stages' => 'Leads Abiertos por Etapas', + 'top-selling-products' => 'Productos Más Vendidos', + 'top-persons' => 'Personas Más Destacadas', + ], + 'revenue' => [ + 'lost-revenue' => 'Ingresos Perdidos', + 'won-revenue' => 'Ingresos Ganados', + ], + 'over-all' => [ + 'average-lead-value' => 'Valor Promedio de Lead', + 'total-leads' => 'Total de Leads', + 'average-leads-per-day' => 'Leads Promedio por Día', + 'total-quotations' => 'Total de Cotizaciones', + 'total-persons' => 'Total de Personas', + 'total-organizations' => 'Total de Organizaciones', + ], + 'total-leads' => [ + 'title' => 'Leads', + 'total' => 'Total de Leads', + 'won' => 'Leads Ganados', + 'lost' => 'Leads Perdidos', + ], + 'revenue-by-sources' => [ + 'title' => 'Ingresos por Fuentes', + 'empty-title' => 'No Hay Datos Disponibles', + 'empty-info' => 'No hay datos disponibles para el intervalo seleccionado', + ], + 'revenue-by-types' => [ + 'title' => 'Ingresos por Tipos', + 'empty-title' => 'No Hay Datos Disponibles', + 'empty-info' => 'No hay datos disponibles para el intervalo seleccionado', + ], + 'top-selling-products' => [ + 'title' => 'Productos Más Vendidos', + 'empty-title' => 'No Se Encontraron Productos', + 'empty-info' => 'No hay productos disponibles para el intervalo seleccionado', + ], + 'top-persons' => [ + 'title' => 'Personas Más Destacadas', + 'empty-title' => 'No Se Encontraron Personas', + 'empty-info' => 'No hay personas disponibles para el intervalo seleccionado', + ], + 'open-leads-by-states' => [ + 'title' => 'Leads Abiertos por Etapas', + 'empty-title' => 'No Hay Datos Disponibles', + 'empty-info' => 'No hay datos disponibles para el intervalo seleccionado', + ], + ], + ], + 'help' => [ + 'index' => [ + 'title' => 'Ayuda y recursos', + 'description' => 'Todo lo que necesitas para sacar el máximo partido a Krayin Admin: alojamiento, soporte y servicios profesionales, además de extensiones y documentación para desarrolladores.', + 'contact-us' => 'Contáctanos', + 'view-all' => 'Ver todo', + 'still-need-help-title' => '¿Todavía necesitas ayuda?', + 'still-need-help-description' => 'Habla con el equipo de Krayin sobre alojamiento, desarrollo personalizado o cualquier otra cosa.', + + 'services' => [ + 'title' => 'Servicios y recursos', + 'cloud-hosting' => [ + 'title' => 'Alojamiento en la nube', + 'description' => 'Alojamiento en la nube gestionado y rentable: prueba y lanza Krayin en la nube en minutos, totalmente optimizado y escalable.', + ], + 'support' => [ + 'title' => 'Soporte y mantenimiento', + 'description' => 'Soporte técnico dedicado y planes de mantenimiento continuo para mantener tu CRM seguro, actualizado y funcionando sin problemas.', + ], + 'extensions' => [ + 'title' => 'Extensiones', + 'description' => 'Explora complementos oficiales y de la comunidad para ampliar Krayin con nuevos conectores, canales y funciones.', + ], + ], + + 'resources' => [ + 'title' => 'Documentación', + 'user-docs' => [ + 'title' => 'Guía del usuario', + 'description' => 'Ayuda experta para la integración de módulos, personalización, migración de datos, actualizaciones de versión y desarrollo a medida.', + ], + 'dev-docs' => [ + 'title' => 'Documentación para desarrolladores y blogs', + 'description' => 'Guías para desarrolladores, tutoriales y los últimos artículos para ayudarte a crear, configurar y mantenerte al día.', + ], + 'api-docs' => [ + 'title' => 'Documentación de la API', + 'description' => 'Referencia completa de la API REST con endpoints, autenticación y ejemplos para integrar Krayin con tu stack.', + ], + ], + + 'community' => [ + 'forums' => [ + 'title' => 'Foros de la comunidad', + 'description' => 'Conecta con miles de usuarios y desarrolladores de Krayin para compartir consejos y resolver problemas.', + 'link' => 'Únete a la comunidad', + ], + 'tutorials' => [ + 'title' => 'Tutoriales en vídeo', + 'description' => 'Mira guías en vídeo paso a paso sobre cómo configurar tu CRM, configurar canales y gestionar usuarios.', + 'link' => 'Explorar el canal de YouTube', + ], + ], + ], + ], + 'layouts' => [ + 'app-version' => 'Versión: :version', + 'dashboard' => 'Tablero', + 'help' => 'Ayuda y recursos', + 'leads' => 'Leads', + 'quotes' => 'Cotizaciones', + 'quote' => 'Cotización', + 'mail' => [ + 'title' => 'Correo', + 'compose' => 'Redactar', + 'inbox' => 'Bandeja de Entrada', + 'draft' => 'Borradores', + 'outbox' => 'Enviados', + 'sent' => 'Enviados', + 'trash' => 'Papelera', + 'setting' => 'Configuración', + ], + 'activities' => 'Actividades', + 'contacts' => 'Contactos', + 'persons' => 'Personas', + 'person' => 'Persona', + 'organizations' => 'Organizaciones', + 'organization' => 'Organización', + 'products' => 'Productos', + 'product' => 'Producto', + 'settings' => 'Configuraciones', + 'user' => 'Usuario', + 'user-info' => 'Administra todos tus usuarios y sus permisos en el CRM, lo que están autorizados a hacer.', + 'groups' => 'Grupos', + 'groups-info' => 'Agregar, editar o eliminar grupos del CRM', + 'roles' => 'Roles', + 'role' => 'Rol', + 'roles-info' => 'Agregar, editar o eliminar roles del CRM', + 'users' => 'Usuarios', + 'users-info' => 'Agregar, editar o eliminar usuarios del CRM', + 'lead' => 'Lead', + 'lead-info' => 'Administra todas las configuraciones relacionadas con los leads en el CRM', + 'sidebar' => [ + 'collapse' => 'Contraer', + 'expand' => 'Expandir', + ], + 'quick-add' => [ + 'title' => 'Añadir rápido', + 'save' => 'Guardar', + 'to' => 'Para', + 'subject' => 'Asunto', + 'message' => 'Mensaje', + ], + 'pipelines' => 'Pipelines', + 'pipelines-info' => 'Agregar, editar o eliminar pipelines del CRM', + 'sources' => 'Fuentes', + 'sources-info' => 'Agregar, editar o eliminar fuentes del CRM', + 'types' => 'Tipos', + 'types-info' => 'Agregar, editar o eliminar tipos del CRM', + 'automation' => 'Automatización', + 'automation-info' => 'Administra todas las configuraciones relacionadas con la automatización en el CRM', + 'attributes' => 'Atributos', + 'attribute' => 'Atributo', + 'attributes-info' => 'Agregar, editar o eliminar atributos del CRM', + 'email-templates' => 'Plantillas de Correo', + 'email' => 'Correo', + 'email-templates-info' => 'Agregar, editar o eliminar plantillas de correo del CRM', + 'events' => 'Eventos', + 'events-info' => 'Agregar, editar o eliminar eventos del CRM', + 'campaigns' => 'Campañas', + 'campaigns-info' => 'Agregar, editar o eliminar campañas del CRM', + 'workflows' => 'Flujos de Trabajo', + 'workflows-info' => 'Agregar, editar o eliminar flujos de trabajo del CRM', + 'webhooks' => 'Webhooks', + 'webhooks-info' => 'Agregar, editar o eliminar webhooks desde CRM', + 'other-settings' => 'Otras Configuraciones', + 'other-settings-info' => 'Administra todas tus configuraciones adicionales en el CRM', + 'tags' => 'Etiquetas', + 'tags-info' => 'Agregar, editar o eliminar etiquetas del CRM', + 'my-account' => 'Mi Cuenta', + 'sign-out' => 'Cerrar Sesión', + 'back' => 'Volver', + 'name' => 'Nombre', + 'configuration' => 'Configuración', + 'howdy' => '¡Hola!', + 'warehouses' => 'Almacenes', + 'warehouse' => 'Almacén', + 'warehouses-info' => 'Agregar, editar o eliminar almacenes del CRM', + 'inventory' => 'Inventario', + 'inventory-info' => 'Gestionar todos los ajustes relacionados con el inventario en el CRM', + 'data_transfer' => 'Transferencia de Datos', + 'data_transfer_info' => 'Gestionar la configuración relacionada con la transferencia de datos de personas, productos y clientes potenciales en el CRM', + ], + 'user' => [ + 'account' => [ + 'name' => 'Nombre', + 'email' => 'Correo Electrónico', + 'password' => 'Contraseña', + 'my_account' => 'Mi cuenta', + 'update_details' => 'Actualizar Detalles', + 'current_password' => 'Contraseña Actual', + 'confirm_password' => 'Confirmar Contraseña', + 'password-match' => 'La contraseña actual no coincide.', + 'account-save' => 'Cambios en la cuenta guardados exitosamente.', + 'permission-denied' => 'Permiso Denegado', + 'remove-image' => 'Eliminar Imagen', + 'upload_image_pix' => 'Sube una Imagen de Perfil (100px x 100px)', + 'upload_image_format' => 'en formato PNG o JPG', + 'image_upload_message' => 'Solo se permiten imágenes (.jpeg, .jpg, .png, ..).', + ], + ], + 'emails' => [ + 'common' => [ + 'dear' => 'Estimado/a :name', + 'cheers' => 'Saludos,
    Equipo :app_name', + 'user' => [ + 'dear' => 'Estimado/a :username', + 'create-subject' => 'Has sido agregado como miembro.', + 'create-body' => '¡Felicidades! Ahora eres miembro de nuestro equipo.', + 'forget-password' => [ + 'subject' => 'Restablecimiento de Contraseña del Cliente', + 'dear' => 'Estimado/a :username', + 'reset-password' => 'Restablecer Contraseña', + 'info' => 'Estás recibiendo este correo porque recibimos una solicitud de restablecimiento de contraseña para tu cuenta', + 'final-summary' => 'Si no solicitaste el restablecimiento de contraseña, no se requiere ninguna acción adicional', + 'thanks' => '¡Gracias!', + ], + ], + ], + ], + 'validations' => [ + 'message' => [ + 'code' => 'El campo debe ser un código válido.', + 'decimal' => 'El :attribute debe ser un número decimal.', + ], + ], + 'errors' => [ + 'dashboard' => 'Tablero', + 'go-back' => 'Volver', + 'support' => 'Si el problema persiste, contáctanos en :email para obtener ayuda.', + 'unauthorized' => 'Esta acción no está autorizada.', + 'role-exceeds-own' => 'You can only assign a role whose permissions are the same as or fewer than your own.', + 'user-exceeds-own' => 'You can only manage users whose permissions are the same as or fewer than your own.', + 'own-privileges' => 'You cannot change your own role or data scope.', + 'scope-exceeds-own' => 'You cannot grant a data scope wider than your own.', + 'primary-admin-protected' => 'The primary administrator account is protected and cannot be modified here.', + 'role-permissions-exceed-own' => 'You can only create or edit roles whose permissions are the same as or fewer than your own.', + + '404' => [ + 'description' => '¡Ups! La página que estás buscando está de vacaciones. Parece que no pudimos encontrar lo que estabas buscando.', + 'title' => '404 Página No Encontrada', + ], + '401' => [ + 'description' => '¡Ups! Parece que no tienes permiso para acceder a esta página. Parece que te faltan las credenciales necesarias.', + 'title' => '401 No Autorizado', + ], + '403' => [ + 'description' => '¡Ups! Esta página está fuera de límites. Parece que no tienes los permisos necesarios para ver este contenido.', + 'title' => '403 Prohibido', + ], + '500' => [ + 'description' => '¡Ups! Algo salió mal. Parece que tenemos problemas para cargar la página que estás buscando.', + 'title' => '500 Error Interno del Servidor', + ], + '503' => [ + 'description' => '¡Ups! Parece que estamos temporalmente fuera de servicio por mantenimiento. Vuelve a intentarlo en un rato.', + 'title' => '503 Servicio No Disponible', + ], + ], + 'export' => [ + 'csv' => 'CSV', + 'download' => 'Descargar', + 'export' => 'Exportar', + 'google-contacts' => 'Google Contacts', + 'google-contacts-in-progress' => 'Exportando tus contactos a Google… esto puede tardar un momento.', + 'google-contacts-total' => 'Contactos Totales:', + 'google-contacts-exported' => 'Exportados:', + 'google-contacts-duplicate' => 'Ya Existían:', + 'google-contacts-failed' => 'Fallidos:', + 'google-contacts-summary' => 'Exportación finalizada: :exported exportados, :duplicate ya existían, :failed fallidos.', + 'no-records' => 'No se encontraron registros.', + 'xls' => 'XLS', + 'xlsx' => 'XLSX', + ], +]; diff --git a/packages/Webkul/Admin/src/Resources/lang/ja/app.php b/packages/Webkul/Admin/src/Resources/lang/ja/app.php new file mode 100644 index 0000000..bbc0f62 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/lang/ja/app.php @@ -0,0 +1,2483 @@ + [ + 'help' => 'ヘルプ', + 'leads' => 'リード', + 'lead' => 'リード', + 'quotes' => '見積', + 'mail' => 'メール', + 'inbox' => '受信トレイ', + 'draft' => '下書き', + 'outbox' => '送信トレイ', + 'sent' => '送信済み', + 'trash' => 'ゴミ箱', + 'activities' => '活動', + 'webhook' => 'Webhook', + 'contacts' => '連絡先', + 'persons' => '個人', + 'organizations' => '組織', + 'products' => '製品', + 'settings' => '設定', + 'groups' => 'グループ', + 'roles' => 'ロール', + 'users' => 'ユーザー', + 'user' => 'ユーザー', + 'automation' => '自動化', + 'attributes' => '属性', + 'pipelines' => 'パイプライン', + 'sources' => 'ソース', + 'types' => 'タイプ', + 'email-templates' => 'メールテンプレート', + 'workflows' => 'ワークフロー', + 'other-settings' => 'その他の設定', + 'tags' => 'タグ', + 'configuration' => '環境設定', + 'create' => '作成', + 'quick_add' => 'クイック追加', + 'edit' => '編集', + 'view' => '表示', + 'print' => '印刷', + 'delete' => '削除', + 'export' => 'エクスポート', + 'mass-delete' => '一括削除', + 'data-transfer' => 'データ転送', + 'imports' => 'インポート', + 'import' => 'インポート', + 'event' => 'イベント', + 'campaigns' => 'キャンペーン', + 'warehouses' => '倉庫', + 'inventory' => '在庫', + ], + 'users' => [ + 'activate-warning' => 'アカウントはまだ有効化されていません。管理者にお問い合わせください。', + 'login-error' => '認証情報が登録内容と一致しません。', + 'not-permission' => '管理パネルへのアクセス権限がありません。', + 'login' => [ + 'email' => 'メールアドレス', + 'forget-password-link' => 'パスワードをお忘れですか?', + 'password' => 'パスワード', + 'submit-btn' => 'サインイン', + 'title' => 'サインイン', + ], + 'forget-password' => [ + 'create' => [ + 'email' => '登録済みメールアドレス', + 'email-not-exist' => 'メールアドレスが存在しません', + 'page-title' => 'パスワードを忘れた場合', + 'reset-link-sent' => 'パスワード再設定リンクを送信しました', + 'sign-in-link' => 'サインインに戻る', + 'submit-btn' => 'リセット', + 'title' => 'パスワードの再設定', + ], + ], + 'reset-password' => [ + 'back-link-title' => 'サインインに戻る', + 'confirm-password' => 'パスワード(確認)', + 'email' => '登録済みメールアドレス', + 'password' => 'パスワード', + 'submit-btn' => 'パスワードを再設定', + 'title' => 'パスワードの再設定', + ], + ], + 'account' => [ + 'edit' => [ + 'back-btn' => '戻る', + 'change-password' => 'パスワードの変更', + 'confirm-password' => 'パスワード(確認)', + 'current-password' => '現在のパスワード', + 'email' => 'メールアドレス', + 'general' => '一般', + 'invalid-password' => '入力された現在のパスワードが正しくありません。', + 'name' => '名前', + 'password' => 'パスワード', + 'profile-image' => 'プロフィール画像', + 'save-btn' => 'アカウントを保存', + 'title' => 'マイアカウント', + 'update-success' => 'アカウントを更新しました', + 'upload-image-info' => 'プロフィール画像(110px×110px)をPNGまたはJPG形式でアップロードしてください', + ], + ], + 'components' => [ + 'activities' => [ + 'actions' => [ + 'mail' => [ + 'btn' => 'メール', + 'title' => 'メールの作成', + 'to' => '宛先', + 'enter-emails' => 'Enterキーでメールアドレスを追加', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => '件名', + 'send-btn' => '送信', + 'message' => 'メッセージ', + ], + 'file' => [ + 'btn' => 'ファイル', + 'title' => 'ファイルを追加', + 'title-control' => 'タイトル', + 'name' => '名前', + 'description' => '説明', + 'file' => 'ファイル', + 'save-btn' => 'ファイルを保存', + ], + 'note' => [ + 'btn' => 'メモ', + 'title' => 'メモを追加', + 'comment' => 'コメント', + 'save-btn' => 'メモを保存', + ], + 'activity' => [ + 'btn' => '活動', + 'title' => '活動を追加', + 'title-control' => 'タイトル', + 'description' => '説明', + 'schedule-from' => '開始日時', + 'schedule-to' => '終了日時', + 'location' => '場所', + 'call' => '電話', + 'meeting' => '会議', + 'lunch' => '会食', + 'save-btn' => '活動を保存', + 'participants' => [ + 'title' => '参加者', + 'placeholder' => '入力して参加者を検索', + 'users' => 'ユーザー', + 'persons' => '個人', + 'no-results' => '結果が見つかりません...', + ], + ], + ], + 'index' => [ + 'all' => 'すべて', + 'bcc' => 'BCC', + 'by-user' => ':user による', + 'calls' => '電話', + 'cc' => 'CC', + 'change-log' => '変更履歴', + 'delete' => '削除', + 'edit' => '編集', + 'emails' => 'メール', + 'empty' => '空', + 'files' => 'ファイル', + 'from' => '差出人', + 'location' => '場所', + 'lunches' => '会食', + 'mark-as-done' => '完了にする', + 'meetings' => '会議', + 'notes' => 'メモ', + 'participants' => '参加者', + 'planned' => '予定', + 'quotes' => '見積', + 'scheduled-on' => '予定日時', + 'system' => 'システム', + 'to' => '宛先', + 'unlink' => 'リンク解除', + 'view' => '表示', + 'empty-placeholders' => [ + 'all' => [ + 'title' => '活動が見つかりません', + 'description' => '活動はまだありません。左パネルの「活動」ボタンから追加できます。', + ], + 'planned' => [ + 'title' => '予定された活動が見つかりません', + 'description' => '予定された活動はまだありません。左パネルの「活動」ボタンから追加できます。', + ], + 'notes' => [ + 'title' => 'メモが見つかりません', + 'description' => 'メモはまだありません。左パネルの「メモ」ボタンから追加できます。', + ], + 'calls' => [ + 'title' => '電話が見つかりません', + 'description' => '電話の記録はまだありません。左パネルの「活動」ボタンから「電話」タイプを選んで追加できます。', + ], + 'meetings' => [ + 'title' => '会議が見つかりません', + 'description' => '会議の記録はまだありません。左パネルの「活動」ボタンから「会議」タイプを選んで追加できます。', + ], + 'lunches' => [ + 'title' => '会食が見つかりません', + 'description' => '会食の記録はまだありません。左パネルの「活動」ボタンから「会食」タイプを選んで追加できます。', + ], + 'files' => [ + 'title' => 'ファイルが見つかりません', + 'description' => 'ファイルはまだありません。左パネルの「ファイル」ボタンから追加できます。', + ], + 'emails' => [ + 'title' => 'メールが見つかりません', + 'description' => 'メールはまだありません。左パネルの「メール」ボタンから追加できます。', + ], + 'system' => [ + 'title' => '変更履歴が見つかりません', + 'description' => '変更履歴はまだありません。', + ], + ], + ], + ], + 'media' => [ + 'images' => [ + 'add-image-btn' => '画像を追加', + 'ai-add-image-btn' => 'Magic AI', + 'allowed-types' => 'png, jpeg, jpg', + 'not-allowed-error' => '画像ファイル(.jpeg、.jpg、.png など)のみ利用できます。', + 'placeholders' => [ + 'front' => '正面', + 'next' => '次へ', + 'size' => 'サイズ', + 'use-cases' => '用途', + 'zoom' => 'ズーム', + ], + ], + 'videos' => [ + 'add-video-btn' => '動画を追加', + 'allowed-types' => 'mp4, webm, mkv', + 'not-allowed-error' => '動画ファイル(.mp4、.mov、.ogg など)のみ利用できます。', + ], + ], + 'datagrid' => [ + 'index' => [ + 'no-records-selected' => 'レコードが選択されていません。', + 'must-select-a-mass-action-option' => '一括操作のオプションを選択してください。', + 'must-select-a-mass-action' => '一括操作を選択してください。', + ], + 'toolbar' => [ + 'length-of' => ':length /', + 'of' => '/', + 'per-page' => '表示件数', + 'results' => ':total 件', + 'delete' => '削除', + 'selected' => ':total 件選択中', + 'mass-actions' => [ + 'submit' => '実行', + 'select-option' => 'オプションを選択', + 'select-action' => '操作を選択', + ], + 'filter' => [ + 'apply-filters-btn' => 'フィルターを適用', + 'back-btn' => '戻る', + 'create-new-filter' => '新しいフィルターを作成', + 'custom-filters' => 'カスタムフィルター', + 'delete-error' => 'フィルターの削除中に問題が発生しました。もう一度お試しください。', + 'delete-success' => 'フィルターを削除しました。', + 'empty-description' => '保存できるフィルターが選択されていません。保存するフィルターを選択してください。', + 'empty-title' => '保存するフィルターを追加', + 'name' => '名前', + 'quick-filters' => 'クイックフィルター', + 'save-btn' => '保存', + 'save-filter' => 'フィルターを保存', + 'saved-success' => 'フィルターを保存しました。', + 'selected-filters' => '選択中のフィルター', + 'title' => 'フィルター', + 'update' => '更新', + 'update-filter' => 'フィルターを更新', + 'updated-success' => 'フィルターを更新しました。', + ], + 'search' => [ + 'title' => '検索', + ], + 'column-settings' => [ + 'title' => '列の設定', + ], + ], + 'filters' => [ + 'select' => '選択', + 'title' => 'フィルター', + 'dropdown' => [ + 'searchable' => [ + 'at-least-two-chars' => '2文字以上入力してください...', + 'no-results' => '結果が見つかりません...', + ], + ], + 'custom-filters' => [ + 'clear-all' => 'すべてクリア', + 'title' => 'カスタムフィルター', + ], + 'boolean-options' => [ + 'false' => 'いいえ', + 'true' => 'はい', + ], + 'date-options' => [ + 'last-month' => '先月', + 'last-six-months' => '過去6か月', + 'last-three-months' => '過去3か月', + 'this-month' => '今月', + 'this-week' => '今週', + 'this-year' => '今年', + 'today' => '今日', + 'yesterday' => '昨日', + ], + ], + 'table' => [ + 'actions' => '操作', + 'no-records-available' => 'レコードがありません。', + ], + ], + 'modal' => [ + 'confirm' => [ + 'agree-btn' => '実行する', + 'disagree-btn' => 'キャンセル', + 'message' => 'この操作を実行してもよろしいですか?', + 'title' => 'よろしいですか?', + ], + ], + 'tags' => [ + 'index' => [ + 'title' => 'タグ', + 'added-tags' => '追加済みのタグ', + 'save-btn' => 'タグを保存', + 'placeholder' => '入力してタグを検索', + 'add-tag' => '「:term」を追加...', + 'aquarelle-red' => 'アクアレルレッド', + 'crushed-cashew' => 'クラッシュドカシュー', + 'beeswax' => 'ビーズワックス', + 'lemon-chiffon' => 'レモンシフォン', + 'snow-flurry' => 'スノーフラリー', + 'honeydew' => 'ハニーデュー', + ], + ], + 'layouts' => [ + 'powered-by' => [ + 'description' => 'Powered by :krayin — :webkul によるオープンソースプロジェクト', + ], + 'header' => [ + 'mega-search' => [ + 'title' => 'メガ検索', + 'tabs' => [ + 'leads' => 'リード', + 'quotes' => '見積', + 'persons' => '個人', + 'products' => '製品', + 'settings' => '設定', + 'configurations' => '環境設定', + ], + + 'explore-all-products' => 'すべての製品を見る', + 'explore-all-leads' => 'すべてのリードを見る', + 'explore-all-contacts' => 'すべての連絡先を見る', + 'explore-all-quotes' => 'すべての見積を見る', + 'explore-all-settings' => 'すべての設定を見る', + 'explore-all-configurations' => 'すべての環境設定を見る', + 'explore-all-matching-products' => '「:query」に一致する製品をすべて見る (:count)', + 'explore-all-matching-leads' => '「:query」に一致するリードをすべて見る (:count)', + 'explore-all-matching-contacts' => '「:query」に一致する連絡先をすべて見る (:count)', + 'explore-all-matching-quotes' => '「:query」に一致する見積をすべて見る (:count)', + ], + ], + ], + 'attributes' => [ + 'edit' => [ + 'delete' => '削除', + ], + 'lookup' => [ + 'click-to-add' => 'クリックして追加', + 'search' => '検索...', + 'no-result-found' => '結果が見つかりません', + ], + ], + 'lookup' => [ + 'click-to-add' => 'クリックして追加', + 'no-results' => '結果が見つかりません', + 'add-as-new' => '新規として追加', + 'search' => '検索...', + ], + 'flash-group' => [ + 'success' => '成功', + 'error' => 'エラー', + 'warning' => '警告', + 'info' => '情報', + ], + 'tiny-mce' => [ + 'http-error' => 'HTTPエラー', + 'invalid-json' => 'サーバーから不正なJSONレスポンスが返されました。', + 'upload-failed' => 'ファイルのアップロードに失敗しました。もう一度お試しください。', + ], + ], + 'quotes' => [ + 'index' => [ + 'title' => '見積', + 'create-btn' => '見積を作成', + 'create-success' => '見積を作成しました。', + 'update-success' => '見積を更新しました。', + 'delete-success' => '見積を削除しました。', + 'delete-failed' => '見積を削除できません。', + 'datagrid' => [ + 'subject' => '件名', + 'sales-person' => '営業担当者', + 'expired-at' => '有効期限', + 'created-at' => '作成日', + 'person' => '個人', + 'subtotal' => '小計', + 'discount' => '値引', + 'tax' => '税', + 'adjustment' => '調整', + 'grand-total' => '総合計', + 'edit' => '編集', + 'delete' => '削除', + 'print' => '印刷', + ], + 'pdf' => [ + 'adjustment' => '調整', + 'amount' => '金額', + 'billing-address' => '請求先住所', + 'date' => '日付', + 'discount' => '値引', + 'description' => '説明', + 'expired-at' => '有効期限', + 'grand-total' => '総合計', + 'person' => '個人', + 'price' => '価格', + 'product-name' => '製品名', + 'quantity' => '数量', + 'quote-id' => '見積ID', + 'sales-person' => '営業担当者', + 'shipping-address' => '納品先住所', + 'sku' => 'SKU', + 'sub-total' => '小計', + 'subject' => '件名', + 'tax' => '税', + 'title' => '見積書', + ], + ], + 'create' => [ + 'title' => '見積を作成', + 'save-btn' => '見積を保存', + 'quote-info' => '見積情報', + 'quote-info-info' => '見積の基本情報を入力します。', + 'address-info' => '住所情報', + 'address-info-info' => '見積に関する住所の情報です。', + 'same-as-billing' => '納品先住所は請求先住所と同じ', + 'quote-items' => '見積明細', + 'search-products' => '製品を検索', + 'link-to-lead' => 'リードに紐づける', + 'quote-item-info' => 'この見積に製品を追加します。', + 'quote-name' => '見積名', + 'quantity' => '数量', + 'price' => '価格', + 'discount' => '値引', + 'tax' => '税', + 'total' => '合計', + 'amount' => '金額', + 'add-item' => '+ 明細を追加', + 'sub-total' => '小計 (:symbol)', + 'total-discount' => '値引 (:symbol)', + 'total-tax' => '税 (:symbol)', + 'total-adjustment' => '調整 (:symbol)', + 'grand-total' => '総合計 (:symbol)', + 'discount-amount' => '値引額', + 'tax-amount' => '税額', + 'adjustment-amount' => '調整額', + 'product-name' => '製品名', + 'action' => '操作', + ], + 'edit' => [ + 'title' => '見積を編集', + 'save-btn' => '見積を保存', + 'quote-info' => '見積情報', + 'quote-info-info' => '見積の基本情報を入力します。', + 'address-info' => '住所情報', + 'address-info-info' => '見積に関する住所の情報です。', + 'quote-items' => '見積明細', + 'link-to-lead' => 'リードに紐づける', + 'quote-item-info' => 'この見積に製品を追加します。', + 'quote-name' => '見積名', + 'quantity' => '数量', + 'price' => '価格', + 'search-products' => '製品を検索', + 'discount' => '値引', + 'tax' => '税', + 'total' => '合計', + 'amount' => '金額', + 'add-item' => '+ 明細を追加', + 'sub-total' => '小計 (:symbol)', + 'total-discount' => '値引 (:symbol)', + 'total-tax' => '税 (:symbol)', + 'total-adjustment' => '調整 (:symbol)', + 'grand-total' => '総合計 (:symbol)', + 'discount-amount' => '値引額', + 'tax-amount' => '税額', + 'adjustment-amount' => '調整額', + 'product-name' => '製品名', + 'action' => '操作', + ], + ], + 'contacts' => [ + 'persons' => [ + 'index' => [ + 'title' => '個人', + 'create-btn' => '個人を作成', + 'create-success' => '個人を作成しました。', + 'update-success' => '個人を更新しました。', + 'all-delete-success' => '選択した個人をすべて削除しました。', + 'partial-delete-warning' => '一部の個人を削除しました。リードに紐づいている個人は削除できませんでした。', + 'none-delete-warning' => '選択した個人はいずれもリードに紐づいているため削除できませんでした。', + 'no-selection' => '削除する個人が選択されていません。', + 'delete-failed' => '選択した個人の削除に失敗しました。', + 'datagrid' => [ + 'contact-numbers' => '電話番号', + 'delete' => '削除', + 'edit' => '編集', + 'emails' => 'メールアドレス', + 'id' => 'ID', + 'view' => '表示', + 'name' => '氏名', + 'organization-name' => '組織名', + 'tag-name' => 'タグ名', + 'google-export-not-connected' => '先に「設定 > その他の設定 > Google連絡先」からGoogleアカウントを接続してください。', + 'google-export-started' => 'Google連絡先へのエクスポートを開始しました。', + ], + ], + 'view' => [ + 'title' => ':name', + 'about-person' => '個人情報', + 'about-organization' => '組織情報', + 'activities' => [ + 'index' => [ + 'all' => 'すべて', + 'calls' => '電話', + 'meetings' => '会議', + 'lunches' => '会食', + 'files' => 'ファイル', + 'quotes' => '見積', + 'notes' => 'メモ', + 'emails' => 'メール', + 'by-user' => ':user による', + 'scheduled-on' => '予定日時', + 'location' => '場所', + 'participants' => '参加者', + 'mark-as-done' => '完了にする', + 'delete' => '削除', + 'edit' => '編集', + ], + 'actions' => [ + 'mail' => [ + 'btn' => 'メール', + 'title' => 'メールの作成', + 'to' => '宛先', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => '件名', + 'send-btn' => '送信', + 'message' => 'メッセージ', + ], + 'file' => [ + 'btn' => 'ファイル', + 'title' => 'ファイルを追加', + 'title-control' => 'タイトル', + 'name' => 'ファイル名', + 'description' => '説明', + 'file' => 'ファイル', + 'save-btn' => 'ファイルを保存', + ], + 'note' => [ + 'btn' => 'メモ', + 'title' => 'メモを追加', + 'comment' => 'コメント', + 'save-btn' => 'メモを保存', + ], + 'activity' => [ + 'btn' => '活動', + 'title' => '活動を追加', + 'title-control' => 'タイトル', + 'description' => '説明', + 'schedule-from' => '開始日時', + 'schedule-to' => '終了日時', + 'location' => '場所', + 'call' => '電話', + 'meeting' => '会議', + 'lunch' => '会食', + 'save-btn' => '活動を保存', + ], + ], + ], + 'tags' => [ + 'create-success' => 'タグを作成しました。', + 'destroy-success' => 'タグを削除しました。', + ], + ], + 'create' => [ + 'title' => '個人を作成', + 'save-btn' => '個人を保存', + ], + 'edit' => [ + 'title' => '個人を編集', + 'save-btn' => '個人を保存', + ], + ], + 'organizations' => [ + 'index' => [ + 'title' => '組織', + 'create-btn' => '組織を作成', + 'create-success' => '組織を作成しました。', + 'update-success' => '組織を更新しました。', + 'delete-success' => '組織を削除しました。', + 'delete-failed' => '組織を削除できません。', + 'datagrid' => [ + 'delete' => '削除', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名称', + 'persons-count' => '個人数', + ], + ], + 'create' => [ + 'title' => '組織を作成', + 'save-btn' => '組織を保存', + ], + 'edit' => [ + 'title' => '組織を編集', + 'save-btn' => '組織を保存', + ], + ], + ], + 'products' => [ + 'index' => [ + 'title' => '製品', + 'create-btn' => '製品を作成', + 'create-success' => '製品を作成しました。', + 'update-success' => '製品を更新しました。', + 'delete-success' => '製品を削除しました。', + 'delete-failed' => '製品を削除できません。', + 'datagrid' => [ + 'allocated' => '引当済み', + 'delete' => '削除', + 'edit' => '編集', + 'id' => 'ID', + 'in-stock' => '在庫あり', + 'name' => '名称', + 'on-hand' => '手持在庫', + 'tag-name' => 'タグ名', + 'price' => '価格', + 'sku' => 'SKU', + 'view' => '表示', + ], + ], + 'create' => [ + 'save-btn' => '製品を保存', + 'title' => '製品を作成', + 'general' => '一般', + 'price' => '価格', + ], + 'edit' => [ + 'title' => '製品を編集', + 'save-btn' => '製品を保存', + 'general' => '一般', + 'price' => '価格', + ], + 'view' => [ + 'sku' => 'SKU', + 'all' => 'すべて', + 'notes' => 'メモ', + 'files' => 'ファイル', + 'inventories' => '在庫', + 'change-logs' => '変更履歴', + 'attributes' => [ + 'about-product' => '製品情報', + ], + 'inventory' => [ + 'warehouse' => '倉庫', + 'in-stock' => '在庫あり', + 'allocated' => '引当済み', + 'on-hand' => '手持在庫', + 'actions' => '操作', + 'assign' => '割り当て', + 'add-warehouse' => '倉庫を追加', + 'location' => '場所', + 'add-more' => 'さらに追加', + 'save' => '保存', + ], + ], + ], + 'settings' => [ + 'title' => '設定', + 'groups' => [ + 'index' => [ + 'create-btn' => 'グループを作成', + 'title' => 'グループ', + 'create-success' => 'グループを作成しました。', + 'update-success' => 'グループを更新しました。', + 'destroy-success' => 'グループを削除しました。', + 'delete-failed' => 'グループを削除できません。', + 'delete-failed-associated-users' => 'このグループはユーザーに使用されているため削除できません。', + 'datagrid' => [ + 'delete' => '削除', + 'description' => '説明', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名前', + ], + 'edit' => [ + 'title' => 'グループを編集', + ], + 'create' => [ + 'name' => '名前', + 'title' => 'グループを作成', + 'description' => '説明', + 'save-btn' => 'グループを保存', + ], + ], + ], + 'roles' => [ + 'index' => [ + 'being-used' => 'このロールは管理ユーザーに使用されているため削除できません。', + 'create-btn' => 'ロールを作成', + 'create-success' => 'ロールを作成しました。', + 'current-role-delete-error' => '現在のユーザーに割り当てられているロールは削除できません。', + 'current-role-edit-error' => '自分のアカウントに割り当てられているロールは編集できません。', + 'delete-failed' => 'ロールを削除できません。', + 'delete-success' => 'ロールを削除しました。', + 'last-delete-error' => '少なくとも1つのロールが必要です。', + 'settings' => '設定', + 'title' => 'ロール', + 'update-success' => 'ロールを更新しました。', + 'user-define-error' => 'システムロールは削除できません。', + 'datagrid' => [ + 'all' => 'すべて', + 'custom' => 'カスタム', + 'delete' => '削除', + 'description' => '説明', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名前', + 'permission-type' => '権限タイプ', + ], + ], + 'create' => [ + 'access-control' => 'アクセス制御', + 'all' => 'すべて', + 'back-btn' => '戻る', + 'custom' => 'カスタム', + 'description' => '説明', + 'general' => '一般', + 'name' => '名前', + 'permissions' => '権限', + 'save-btn' => 'ロールを保存', + 'title' => 'ロールを作成', + ], + 'edit' => [ + 'access-control' => 'アクセス制御', + 'all' => 'すべて', + 'back-btn' => '戻る', + 'custom' => 'カスタム', + 'description' => '説明', + 'general' => '一般', + 'name' => '名前', + 'permissions' => '権限', + 'save-btn' => 'ロールを保存', + 'title' => 'ロールを編集', + ], + ], + 'types' => [ + 'index' => [ + 'create-btn' => 'タイプを作成', + 'create-success' => 'タイプを作成しました。', + 'delete-failed' => 'タイプを削除できません。', + 'delete-success' => 'タイプを削除しました。', + 'title' => 'タイプ', + 'update-success' => 'タイプを更新しました。', + 'datagrid' => [ + 'delete' => '削除', + 'description' => '説明', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名前', + ], + 'create' => [ + 'name' => '名前', + 'save-btn' => 'タイプを保存', + 'title' => 'タイプを作成', + ], + 'edit' => [ + 'title' => 'タイプを編集', + ], + ], + ], + 'sources' => [ + 'index' => [ + 'title' => 'ソース', + 'create-btn' => 'ソースを作成', + 'create-success' => 'ソースを作成しました。', + 'delete-failed' => 'ソースを削除できません。', + 'delete-success' => 'ソースを削除しました。', + 'update-success' => 'ソースを更新しました。', + 'delete-failed-associated-leads' => 'このソースは既存のリードに関連付けられているため削除できません。先にリードの関連付けを解除または更新してください。', + 'datagrid' => [ + 'delete' => '削除', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名前', + ], + 'create' => [ + 'name' => '名前', + 'save-btn' => 'ソースを保存', + 'title' => 'ソースを作成', + ], + 'edit' => [ + 'title' => 'ソースを編集', + ], + ], + ], + 'workflows' => [ + 'index' => [ + 'title' => 'ワークフロー', + 'create-btn' => 'ワークフローを作成', + 'create-success' => 'ワークフローを作成しました。', + 'update-success' => 'ワークフローを更新しました。', + 'delete-success' => 'ワークフローを削除しました。', + 'delete-failed' => 'ワークフローを削除できません。', + 'datagrid' => [ + 'delete' => '削除', + 'description' => '説明', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名前', + ], + ], + 'helpers' => [ + 'update-related-leads' => '関連リードを更新', + 'send-email-to-sales-owner' => '営業担当者へメール送信', + 'send-email-to-participants' => '参加者へメール送信', + 'add-webhook' => 'Webhookを追加', + 'update-lead' => 'リードを更新', + 'update-person' => '個人を更新', + 'send-email-to-person' => '個人へメール送信', + 'add-tag' => 'タグを追加', + 'add-note-as-activity' => 'メモを活動として追加', + 'update-quote' => '見積を更新', + ], + 'create' => [ + 'title' => 'ワークフローを作成', + 'event' => 'イベント', + 'back-btn' => '戻る', + 'save-btn' => 'ワークフローを保存', + 'name' => '名前', + 'basic-details' => '基本情報', + 'description' => '説明', + 'actions' => 'アクション', + 'basic-details-info' => 'ワークフローの基本情報を入力します。', + 'event-info' => 'イベントが発生すると条件を確認し、あらかじめ定義したアクションを実行します。', + 'conditions' => '条件', + 'conditions-info' => '条件は、特定のタイミングで評価されるルールです。', + 'actions-info' => 'アクションは作業負荷を減らし、CRMの自動化を簡単にします', + 'value' => '値', + 'condition-type' => '条件タイプ', + 'all-condition-are-true' => 'すべての条件を満たす', + 'any-condition-are-true' => 'いずれかの条件を満たす', + 'add-condition' => '条件を追加', + 'add-action' => 'アクションを追加', + 'no-webhook-found' => 'Webhookが見つかりません。', + 'create-webhook' => '新しいWebhookを作成します。', + 'yes' => 'はい', + 'no' => 'いいえ', + 'email' => 'メール', + 'is-equal-to' => '等しい', + 'is-not-equal-to' => '等しくない', + 'equals-or-greater-than' => '以上', + 'equals-or-less-than' => '以下', + 'greater-than' => 'より大きい', + 'less-than' => 'より小さい', + 'type' => 'タイプ', + 'contain' => '含む', + 'contains' => '含む', + 'does-not-contain' => '含まない', + ], + 'edit' => [ + 'title' => 'ワークフローを編集', + 'event' => 'イベント', + 'back-btn' => '戻る', + 'save-btn' => 'ワークフローを保存', + 'name' => '名前', + 'basic-details' => '基本情報', + 'description' => '説明', + 'actions' => 'アクション', + 'type' => 'タイプ', + 'basic-details-info' => 'ワークフローの基本情報を入力します。', + 'event-info' => 'イベントが発生すると条件を確認し、あらかじめ定義したアクションを実行します。', + 'conditions' => '条件', + 'conditions-info' => '条件は、特定のタイミングで評価されるルールです。', + 'actions-info' => 'アクションは作業負荷を減らし、CRMの自動化を簡単にします', + 'value' => '値', + 'condition-type' => '条件タイプ', + 'all-condition-are-true' => 'すべての条件を満たす', + 'any-condition-are-true' => 'いずれかの条件を満たす', + 'add-condition' => '条件を追加', + 'add-action' => 'アクションを追加', + 'yes' => 'はい', + 'no' => 'いいえ', + 'email' => 'メール', + 'is-equal-to' => '等しい', + 'is-not-equal-to' => '等しくない', + 'equals-or-greater-than' => '以上', + 'equals-or-less-than' => '以下', + 'greater-than' => 'より大きい', + 'less-than' => 'より小さい', + 'contain' => '含む', + 'contains' => '含む', + 'does-not-contain' => '含まない', + ], + ], + 'google-contacts' => [ + 'index' => [ + 'title' => 'Google連絡先', + 'not-connected-info' => 'Googleアカウントを接続すると、CRMの連絡先をGoogle連絡先へエクスポートできます。', + 'connected-info' => ':email として接続中。', + 'connect-btn' => 'Googleアカウントを接続', + 'disconnect-btn' => '接続解除', + 'connect-success' => 'Googleアカウントを接続しました。', + 'connect-failed' => 'Googleアカウントに接続できませんでした。もう一度お試しください。', + 'not-configured' => 'Google OAuthが設定されていません。先に必要な環境変数を設定してください。', + 'disconnect-success' => 'Googleアカウントの接続を解除しました。', + ], + ], + + 'webforms' => [ + 'index' => [ + 'title' => 'Webフォーム', + 'create-btn' => 'Webフォームを作成', + 'create-success' => 'Webフォームを作成しました。', + 'update-success' => 'Webフォームを更新しました。', + 'delete-success' => 'Webフォームを削除しました。', + 'delete-failed' => 'Webフォームを削除できません。', + 'datagrid' => [ + 'id' => 'ID', + 'title' => 'タイトル', + 'edit' => '編集', + 'delete' => '削除', + ], + ], + 'create' => [ + 'title' => 'Webフォームを作成', + 'add-attribute-btn' => '属性追加ボタン', + 'attribute-label-color' => '属性ラベルの色', + 'attributes' => '属性', + 'attributes-info' => 'フォームにカスタム属性を追加します。', + 'background-color' => '背景色', + 'create-lead' => 'リードを作成', + 'customize-webform' => 'Webフォームのカスタマイズ', + 'customize-webform-info' => '各要素の色を選んでWebフォームをカスタマイズできます。', + 'description' => '説明', + 'display-custom-message' => 'カスタムメッセージを表示', + 'form-background-color' => 'フォーム背景色', + 'form-submit-btn-color' => '送信ボタンの色', + 'form-submit-button-color' => '送信ボタンの色', + 'form-title-color' => 'フォームタイトルの色', + 'general' => '一般', + 'leads' => 'リード', + 'person' => '個人', + 'pipeline' => 'パイプライン', + 'save-btn' => 'Webフォームを保存', + 'submit-button-label' => '送信ボタンのラベル', + 'submit-success-action' => '送信成功時の動作', + 'redirect-to-url' => 'URLへリダイレクト', + 'choose-value' => '値を選択', + 'select-file' => 'ファイルを選択', + 'select-image' => '画像を選択', + 'enter-value' => '値を入力', + ], + 'edit' => [ + 'add-attribute-btn' => '属性追加ボタン', + 'attribute-label-color' => '属性ラベルの色', + 'attributes' => '属性', + 'attributes-info' => 'フォームにカスタム属性を追加します。', + 'background-color' => '背景色', + 'choose-value' => '値を選択', + 'code-snippet' => 'コードスニペット', + 'copied' => 'コピーしました', + 'copy' => 'コピー', + 'create-lead' => 'リードを作成', + 'customize-webform' => 'Webフォームのカスタマイズ', + 'customize-webform-info' => '各要素の色を選んでWebフォームをカスタマイズできます。', + 'description' => '説明', + 'display-custom-message' => 'カスタムメッセージを表示', + 'embed' => '埋め込み', + 'enter-value' => '値を入力', + 'form-background-color' => 'フォーム背景色', + 'form-submit-btn-color' => '送信ボタンの色', + 'form-submit-button-color' => '送信ボタンの色', + 'form-title-color' => 'フォームタイトルの色', + 'general' => '一般', + 'leads' => 'リード', + 'person' => '個人', + 'pipeline' => 'パイプライン', + 'preview' => 'プレビュー', + 'public-url' => '公開URL', + 'redirect-to-url' => 'URLへリダイレクト', + 'save-btn' => 'Webフォームを保存', + 'select-file' => 'ファイルを選択', + 'select-image' => '画像を選択', + 'submit-button-label' => '送信ボタンのラベル', + 'submit-success-action' => '送信成功時の動作', + 'title' => 'Webフォームを編集', + ], + ], + 'email-template' => [ + 'index' => [ + 'create-btn' => 'メールテンプレートを作成', + 'title' => 'メールテンプレート', + 'create-success' => 'メールテンプレートを作成しました。', + 'update-success' => 'メールテンプレートを更新しました。', + 'delete-success' => 'メールテンプレートを削除しました。', + 'delete-failed' => 'メールテンプレートを削除できません。', + 'datagrid' => [ + 'delete' => '削除', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名前', + 'subject' => '件名', + ], + ], + 'create' => [ + 'title' => 'メールテンプレートを作成', + 'save-btn' => 'メールテンプレートを保存', + 'email-template' => 'メールテンプレート', + 'subject' => '件名', + 'content' => '本文', + 'subject-placeholders' => '件名のプレースホルダー', + 'general' => '一般', + 'name' => '名前', + ], + 'edit' => [ + 'title' => 'メールテンプレートを編集', + 'save-btn' => 'メールテンプレートを保存', + 'email-template' => 'メールテンプレート', + 'subject' => '件名', + 'content' => '本文', + 'subject-placeholders' => '件名のプレースホルダー', + 'general' => '一般', + 'name' => '名前', + ], + ], + 'marketing' => [ + 'events' => [ + 'index' => [ + 'create-btn' => 'イベントを作成', + 'title' => 'イベント', + 'create-success' => 'イベントを作成しました。', + 'update-success' => 'イベントを更新しました。', + 'delete-success' => 'イベントを削除しました。', + 'delete-failed' => 'イベントを削除できません。', + 'mass-delete-success' => 'イベントを削除しました', + 'delete-failed-associated-campaigns' => 'このイベントは既存のキャンペーンに関連付けられているため削除できません。先にキャンペーンの関連付けを解除または更新してください。', + 'datagrid' => [ + 'delete' => '削除', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名前', + 'description' => '説明', + 'date' => '日付', + ], + 'create' => [ + 'title' => 'イベントを作成', + 'name' => '名前', + 'date' => '日付', + 'description' => '説明', + 'save-btn' => 'イベントを保存', + ], + 'edit' => [ + 'title' => 'イベントを編集', + ], + ], + ], + 'campaigns' => [ + 'index' => [ + 'create-btn' => 'キャンペーンを作成', + 'title' => 'キャンペーン', + 'create-success' => 'キャンペーンを作成しました。', + 'update-success' => 'キャンペーンを更新しました。', + 'delete-success' => 'キャンペーンを削除しました。', + 'delete-failed' => 'キャンペーンを削除できません。', + 'mass-delete-success' => 'キャンペーンを削除しました。', + 'datagrid' => [ + 'id' => 'ID', + 'name' => '名前', + 'subject' => '件名', + 'status' => 'ステータス', + 'active' => '有効', + 'inactive' => '無効', + 'edit' => '編集', + 'delete' => '削除', + ], + 'create' => [ + 'title' => 'キャンペーンを作成', + 'name' => '名前', + 'type' => 'タイプ', + 'subject' => '件名', + 'event' => 'イベント', + 'email-template' => 'メールテンプレート', + 'status' => 'ステータス', + ], + 'edit' => [ + 'title' => 'キャンペーンを編集', + ], + ], + ], + ], + 'tags' => [ + 'index' => [ + 'create-btn' => 'タグを作成', + 'title' => 'タグ', + 'create-success' => 'タグを作成しました。', + 'update-success' => 'タグを更新しました。', + 'delete-success' => 'タグを削除しました。', + 'delete-failed' => 'タグを削除できません。', + 'datagrid' => [ + 'delete' => '削除', + 'edit' => '編集', + 'id' => 'ID', + 'name' => '名前', + 'users' => 'ユーザー', + 'created-at' => '作成日', + ], + 'create' => [ + 'name' => '名前', + 'save-btn' => 'タグを保存', + 'title' => 'タグを作成', + 'color' => '色', + ], + 'edit' => [ + 'title' => 'タグを編集', + ], + ], + ], + 'users' => [ + 'index' => [ + 'create-btn' => 'ユーザーを作成', + 'create-success' => 'ユーザーを作成しました。', + 'delete-failed' => 'ユーザーを削除できません。', + 'delete-success' => 'ユーザーを削除しました。', + 'last-delete-error' => '少なくとも1人のユーザーが必要です。', + 'mass-delete-failed' => 'ユーザーを削除できません。', + 'mass-delete-success' => 'ユーザーを削除しました。', + 'mass-update-failed' => 'ユーザーを更新できません。', + 'mass-update-success' => 'ユーザーを更新しました。', + 'title' => 'ユーザー', + 'update-success' => 'ユーザーを更新しました。', + 'user-define-error' => 'システムユーザーは削除できません。', + 'active' => '有効', + 'inactive' => '無効', + 'datagrid' => [ + 'active' => '有効', + 'created-at' => '作成日', + 'delete' => '削除', + 'edit' => '編集', + 'email' => 'メールアドレス', + 'group' => '所属グループ', + 'id' => 'ID', + 'inactive' => '無効', + 'name' => '名前', + 'no-group' => 'グループなし', + 'status' => 'ステータス', + 'update-status' => 'ステータスを更新', + 'users' => 'ユーザー', + ], + 'create' => [ + 'confirm-password' => 'パスワード(確認)', + 'email' => 'メールアドレス', + 'general' => '一般', + 'global' => '全体', + 'group' => 'グループ', + 'individual' => '個人', + 'name' => '名前', + 'password' => 'パスワード', + 'permission' => '権限', + 'role' => 'ロール', + 'save-btn' => 'ユーザーを保存', + 'status' => 'ステータス', + 'title' => 'ユーザーを作成', + 'view-permission' => '閲覧権限', + 'select-at-lest-one-group' => '少なくとも1つのグループを選択してください', + ], + 'edit' => [ + 'title' => 'ユーザーを編集', + ], + ], + ], + 'pipelines' => [ + 'index' => [ + 'title' => 'パイプライン', + 'create-btn' => 'パイプラインを作成', + 'create-success' => 'パイプラインを作成しました。', + 'update-success' => 'パイプラインを更新しました。', + 'default-required' => '少なくとも1つの既定パイプラインが必要です。', + 'delete-success' => 'パイプラインを削除しました。', + 'delete-failed' => 'パイプラインを削除できません。', + 'default-delete-error' => '既定のパイプラインは削除できません。', + 'datagrid' => [ + 'delete' => '削除', + 'edit' => '編集', + 'id' => 'ID', + 'is-default' => '既定', + 'name' => '名前', + 'no' => 'いいえ', + 'rotten-days' => '停滞日数', + 'yes' => 'はい', + ], + ], + 'create' => [ + 'title' => 'パイプラインを作成', + 'save-btn' => 'パイプラインを保存', + 'name' => '名前', + 'rotten-days' => '停滞日数', + 'mark-as-default' => '既定にする', + 'general' => '一般', + 'probability' => '確度(%)', + 'new-stage' => '新規', + 'won-stage' => '受注', + 'lost-stage' => '失注', + 'stage-btn' => 'ステージを追加', + 'stages' => 'ステージ', + 'duplicate-name' => '名前が重複しています', + 'delete-stage' => 'ステージを削除', + 'add-new-stages' => '新しいステージを追加', + 'add-stage-info' => 'パイプラインに新しいステージを追加します', + 'newly-added' => '新規追加', + 'stage-delete-success' => 'ステージを削除しました', + ], + 'edit' => [ + 'title' => 'パイプラインを編集', + 'save-btn' => 'パイプラインを保存', + 'name' => '名前', + 'rotten-days' => '停滞日数', + 'mark-as-default' => '既定にする', + 'general' => '一般', + 'probability' => '確度(%)', + 'new-stage' => '新規', + 'won-stage' => '受注', + 'lost-stage' => '失注', + 'stage-btn' => 'ステージを追加', + 'stages' => 'ステージ', + 'duplicate-name' => '名前が重複しています', + 'delete-stage' => 'ステージを削除', + 'add-new-stages' => '新しいステージを追加', + 'add-stage-info' => 'パイプラインに新しいステージを追加します', + 'stage-delete-success' => 'ステージを削除しました', + ], + ], + 'webhooks' => [ + 'index' => [ + 'title' => 'Webhook', + 'create-btn' => 'Webhookを作成', + 'create-success' => 'Webhookを作成しました。', + 'update-success' => 'Webhookを更新しました。', + 'delete-success' => 'Webhookを削除しました。', + 'delete-failed' => 'Webhookを削除できません。', + 'invalid-endpoint' => 'WebhookのエンドポイントURLは公開されたHTTPまたはHTTPSアドレスである必要があります。', + 'datagrid' => [ + 'id' => 'ID', + 'delete' => '削除', + 'edit' => '編集', + 'name' => '名前', + 'entity-type' => 'エンティティタイプ', + 'end-point' => 'エンドポイント', + ], + ], + 'create' => [ + 'title' => 'Webhookを作成', + 'save-btn' => 'Webhookを保存', + 'info' => 'Webhookの詳細を入力します', + 'url-and-parameters' => 'URLとパラメータ', + 'method' => 'メソッド', + 'post' => 'POST', + 'put' => 'PUT', + 'url-endpoint' => 'URLエンドポイント', + 'parameters' => 'パラメータ', + 'add-new-parameter' => 'パラメータを追加', + 'url-preview' => 'URLプレビュー:', + 'headers' => 'ヘッダー', + 'add-new-header' => 'ヘッダーを追加', + 'body' => 'ボディ', + 'default' => '既定', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'キーと値', + 'add-new-payload' => 'ペイロードを追加', + 'raw' => 'Raw', + 'general' => '一般', + 'name' => '名前', + 'entity-type' => 'エンティティタイプ', + 'insert-placeholder' => 'プレースホルダーを挿入', + 'description' => '説明', + 'json' => 'JSON', + 'text' => 'テキスト', + ], + 'edit' => [ + 'title' => 'Webhookを編集', + 'edit-btn' => 'Webhookを保存', + 'save-btn' => 'Webhookを保存', + 'info' => 'Webhookの詳細を入力します', + 'url-and-parameters' => 'URLとパラメータ', + 'method' => 'メソッド', + 'post' => 'POST', + 'put' => 'PUT', + 'url-endpoint' => 'URLエンドポイント', + 'parameters' => 'パラメータ', + 'add-new-parameter' => 'パラメータを追加', + 'url-preview' => 'URLプレビュー:', + 'headers' => 'ヘッダー', + 'add-new-header' => 'ヘッダーを追加', + 'body' => 'ボディ', + 'default' => '既定', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'キーと値', + 'add-new-payload' => 'ペイロードを追加', + 'raw' => 'Raw', + 'general' => '一般', + 'name' => '名前', + 'entity-type' => 'エンティティタイプ', + 'insert-placeholder' => 'プレースホルダーを挿入', + 'description' => '説明', + 'json' => 'JSON', + 'text' => 'テキスト', + ], + ], + 'warehouses' => [ + 'index' => [ + 'title' => '倉庫', + 'create-btn' => '倉庫を作成', + 'create-success' => '倉庫を作成しました。', + 'name-exists' => 'その倉庫名は既に存在します。', + 'update-success' => '倉庫を更新しました。', + 'delete-success' => '倉庫を削除しました。', + 'delete-failed' => '倉庫を削除できません。', + 'datagrid' => [ + 'id' => 'ID', + 'name' => '名称', + 'contact-name' => '担当者名', + 'delete' => '削除', + 'edit' => '編集', + 'view' => '表示', + 'created-at' => '作成日', + 'products' => '製品', + 'contact-emails' => '連絡先メールアドレス', + 'contact-numbers' => '連絡先電話番号', + ], + ], + 'create' => [ + 'title' => '倉庫を作成', + 'save-btn' => '倉庫を保存', + 'contact-info' => '連絡先情報', + ], + 'edit' => [ + 'title' => '倉庫を編集', + 'save-btn' => '倉庫を保存', + 'contact-info' => '連絡先情報', + ], + 'view' => [ + 'all' => 'すべて', + 'notes' => 'メモ', + 'files' => 'ファイル', + 'location' => '場所', + 'change-logs' => '変更履歴', + 'locations' => [ + 'action' => '操作', + 'add-location' => '場所を追加', + 'create-success' => '場所を作成しました。', + 'delete' => '削除', + 'delete-failed' => '場所を削除できません。', + 'delete-success' => '場所を削除しました。', + 'name' => '名称', + 'save-btn' => '保存', + ], + 'general-information' => [ + 'title' => '一般情報', + ], + 'contact-information' => [ + 'title' => '連絡先情報', + ], + ], + ], + 'attributes' => [ + 'index' => [ + 'title' => '属性', + 'create-btn' => '属性を作成', + 'create-success' => '属性を作成しました。', + 'update-success' => '属性を更新しました。', + 'delete-success' => '属性を削除しました。', + 'delete-failed' => '属性を削除できません。', + 'user-define-error' => 'システム属性は削除できません。', + 'mass-delete-failed' => 'システム属性は削除できません。', + 'datagrid' => [ + 'yes' => 'はい', + 'no' => 'いいえ', + 'id' => 'ID', + 'code' => 'コード', + 'name' => '名前', + 'entity-type' => 'エンティティタイプ', + 'type' => 'タイプ', + 'is-default' => '既定', + 'edit' => '編集', + 'delete' => '削除', + 'quick-add' => 'クイック追加', + 'entity-types' => [ + 'leads' => 'リード', + 'organizations' => '組織', + 'persons' => '個人', + 'products' => '製品', + 'quotes' => '見積', + 'warehouses' => '倉庫', + ], + 'types' => [ + 'text' => 'テキスト', + 'textarea' => 'テキストエリア', + 'price' => '価格', + 'boolean' => '真偽値', + 'select' => 'セレクト', + 'multiselect' => '複数選択', + 'checkbox' => 'チェックボックス', + 'email' => 'メール', + 'address' => '住所', + 'phone' => '電話', + 'lookup' => 'ルックアップ', + 'datetime' => '日時', + 'date' => '日付', + 'image' => '画像', + 'file' => 'ファイル', + ], + ], + ], + 'create' => [ + 'title' => '属性を作成', + 'save-btn' => '属性を保存', + 'code' => 'コード', + 'name' => '名前', + 'quick_add' => 'クイック追加', + 'entity-type' => 'エンティティタイプ', + 'type' => 'タイプ', + 'validations' => 'バリデーション', + 'is-required' => '必須', + 'input-validation' => '入力バリデーション', + 'is-unique' => '一意', + 'labels' => 'ラベル', + 'general' => '一般', + 'none' => 'なし', + 'numeric' => '数値', + 'decimal' => '小数', + 'url' => 'URL', + 'options' => 'オプション', + 'option-type' => 'オプションタイプ', + 'lookup-type' => 'ルックアップタイプ', + 'add-option' => 'オプションを追加', + 'save-option' => 'オプションを保存', + 'option-name' => 'オプション名', + 'add-attribute-options' => '属性オプションを追加', + 'text' => 'テキスト', + 'textarea' => 'テキストエリア', + 'price' => '価格', + 'boolean' => '真偽値', + 'select' => 'セレクト', + 'multiselect' => '複数選択', + 'email' => 'メール', + 'address' => '住所', + 'phone' => '電話', + 'datetime' => '日時', + 'date' => '日付', + 'image' => '画像', + 'file' => 'ファイル', + 'lookup' => 'ルックアップ', + 'entity_type' => 'エンティティタイプ', + 'checkbox' => 'チェックボックス', + 'is_required' => '必須', + 'is_unique' => '一意', + 'actions' => '操作', + ], + 'edit' => [ + 'actions' => '操作', + 'add-attribute-options' => '属性オプションを追加', + 'add-option' => 'オプションを追加', + 'address' => '住所', + 'boolean' => '真偽値', + 'checkbox' => 'チェックボックス', + 'code' => 'コード', + 'date' => '日付', + 'datetime' => '日時', + 'decimal' => '小数', + 'email' => 'メール', + 'entity-type' => 'エンティティタイプ', + 'entity_type' => 'エンティティタイプ', + 'file' => 'ファイル', + 'general' => '一般', + 'image' => '画像', + 'input-validation' => '入力バリデーション', + 'is-required' => '必須', + 'is-unique' => '一意', + 'is_required' => '必須', + 'is_unique' => '一意', + 'labels' => 'ラベル', + 'lookup' => 'ルックアップ', + 'lookup-type' => 'ルックアップタイプ', + 'multiselect' => '複数選択', + 'name' => '名前', + 'quick_add' => 'クイック追加', + 'none' => 'なし', + 'numeric' => '数値', + 'option-deleted' => '属性オプションを削除しました', + 'option-name' => 'オプション名', + 'option-type' => 'オプションタイプ', + 'options' => 'オプション', + 'phone' => '電話', + 'price' => '価格', + 'save-btn' => '属性を保存', + 'save-option' => 'オプションを保存', + 'select' => 'セレクト', + 'text' => 'テキスト', + 'textarea' => 'テキストエリア', + 'title' => '属性を編集', + 'type' => 'タイプ', + 'url' => 'URL', + 'validations' => 'バリデーション', + ], + ], + 'data-transfer' => [ + 'importers' => [ + 'persons' => [ + 'title' => '個人', + 'validation' => [ + 'errors' => [ + 'duplicate-email' => 'メールアドレス「%s」がインポートファイル内に複数あります。', + 'duplicate-phone' => '電話番号「%s」がインポートファイル内に複数あります。', + 'email-not-found' => 'メールアドレス「%s」がシステムに見つかりません。', + 'invalid-date' => '日付:「%s」は有効な日付ではありません。', + ], + ], + ], + 'products' => [ + 'title' => '製品', + 'validation' => [ + 'errors' => [ + 'sku-not-found' => '指定されたSKUの製品が見つかりません', + ], + ], + ], + 'leads' => [ + 'title' => 'リード', + 'validation' => [ + 'errors' => [ + 'id-not-found' => 'ID「%s」がシステムに見つかりません。', + ], + ], + ], + ], + 'validation' => [ + 'errors' => [ + 'column-empty-headers' => '列番号「%s」のヘッダーが空です。', + 'column-name-invalid' => '不正な列名です:「%s」。', + 'column-not-found' => '必須の列が見つかりません: %s。', + 'column-numbers' => '列数がヘッダーの行数と一致しません。', + 'invalid-attribute' => 'ヘッダーに不正な属性が含まれています:「%s」。', + 'system' => '予期しないシステムエラーが発生しました。', + 'wrong-quotes' => 'ストレートクォートの代わりにカーリークォートが使われています。', + 'already-exists' => ':attribute は既に存在します。', + ], + ], + 'imports' => [ + 'create' => [ + 'action' => '操作', + 'allowed-errors' => '許容エラー数', + 'back-btn' => '戻る', + 'create-update' => '作成/更新', + 'delete' => '削除', + 'download-sample' => 'サンプルをダウンロード', + 'field-separator' => '区切り文字', + 'file' => 'ファイル', + 'general' => '一般', + 'images-directory' => '画像ディレクトリのパス', + 'process-in-queue' => 'キューで処理', + 'results' => '結果', + 'save-btn' => 'インポートを保存', + 'settings' => '設定', + 'skip-errors' => 'エラーをスキップ', + 'stop-on-errors' => 'エラーで停止', + 'title' => 'インポートを作成', + 'type' => 'タイプ', + 'validation-strategy' => 'バリデーション方針', + ], + 'edit' => [ + 'action' => '操作', + 'allowed-errors' => '許容エラー数', + 'back-btn' => '戻る', + 'create-update' => '作成/更新', + 'delete' => '削除', + 'download-sample' => 'サンプルをダウンロード', + 'field-separator' => '区切り文字', + 'file' => 'ファイル', + 'general' => '一般', + 'images-directory' => '画像ディレクトリのパス', + 'process-in-queue' => 'キューで処理', + 'results' => '結果', + 'save-btn' => 'インポートを保存', + 'settings' => '設定', + 'skip-errors' => 'エラーをスキップ', + 'stop-on-errors' => 'エラーで停止', + 'title' => 'インポートを編集', + 'type' => 'タイプ', + 'validation-strategy' => 'バリデーション方針', + ], + 'index' => [ + 'button-title' => 'インポートを作成', + 'title' => 'インポート', + 'datagrid' => [ + 'actions' => '操作', + 'completed-at' => '完了日時', + 'created' => '作成', + 'delete' => '削除', + 'deleted' => '削除済み', + 'edit' => '編集', + 'error-file' => 'エラーファイル', + 'id' => 'ID', + 'started-at' => '開始日時', + 'state' => '状態', + 'summary' => '概要', + 'type' => 'タイプ', + 'updated' => '更新', + 'uploaded-file' => 'アップロードファイル', + ], + ], + 'import' => [ + 'back-btn' => '戻る', + 'completed-batches' => '完了バッチ数:', + 'download-error-report' => '完全なレポートをダウンロード', + 'edit-btn' => '編集', + 'imported-info' => 'おめでとうございます!インポートが完了しました。', + 'importing-info' => 'インポート処理中', + 'indexing-info' => 'リソースのインデックス処理中(価格・在庫・検索)', + 'linking-info' => 'リソースのリンク処理中', + 'progress' => '進捗:', + 'title' => 'インポート', + 'total-batches' => '総バッチ数:', + 'total-created' => '作成レコード数:', + 'total-deleted' => '削除レコード数:', + 'total-errors' => 'エラー総数:', + 'total-invalid-rows' => '不正な行数:', + 'total-rows-processed' => '処理行数:', + 'total-updated' => '更新レコード数:', + 'validate' => '検証', + 'validate-info' => '「データを検証」をクリックしてインポート内容を確認してください。', + 'validating-info' => 'データの読み込みと検証を開始しました', + 'validation-failed-info' => 'インポート内容が不正です。以下のエラーを修正してからもう一度お試しください。', + 'validation-success-info' => 'インポート内容は有効です。「インポート」をクリックして処理を開始してください。', + 'importing-queue-hint' => 'このインポートが0%のままの場合、キューワーカーが実行されていない可能性があります。管理者に「php artisan queue:work」の実行を依頼するか、「キューで処理」をオフにして再度インポートしてください。', + ], + 'create-success' => 'インポートを作成しました。', + 'delete-failed' => 'インポートの削除に失敗しました。', + 'delete-success' => 'インポートを削除しました。', + 'not-valid' => 'インポート内容が不正です', + 'nothing-to-import' => 'インポートするリソースがありません。', + 'setup-queue-error' => 'インポートを開始するには、キュードライバーを「database」または「redis」に変更してください。', + 'queue-worker-not-running' => 'キューワーカーが実行されていません。このインポートを処理するには「php artisan queue:work」を実行するか、「キューで処理」をオフにして再度お試しください。', + 'update-success' => 'インポートを更新しました。', + ], + ], + ], + 'activities' => [ + 'index' => [ + 'title' => '活動', + 'datagrid' => [ + 'comment' => 'コメント', + 'created_at' => '作成日', + 'created_by' => '作成者', + 'edit' => '編集', + 'id' => 'ID', + 'done' => '完了', + 'not-done' => '未完了', + 'lead' => 'リード', + 'mass-delete' => '一括削除', + 'mass-update' => '一括更新', + 'schedule-from' => '開始日時', + 'schedule-to' => '終了日時', + 'schedule_from' => '開始日時', + 'schedule_to' => '終了日時', + 'title' => 'タイトル', + 'is_done' => '完了', + 'type' => 'タイプ', + 'update' => '更新', + 'call' => '電話', + 'meeting' => '会議', + 'lunch' => '会食', + ], + ], + 'edit' => [ + 'title' => '活動を編集', + 'back-btn' => '戻る', + 'save-btn' => '活動を保存', + 'type' => '活動タイプ', + 'call' => '電話', + 'meeting' => '会議', + 'lunch' => '会食', + 'schedule_to' => '終了日時', + 'schedule_from' => '開始日時', + 'location' => '場所', + 'comment' => 'コメント', + 'lead' => 'リード', + 'participants' => '参加者', + 'general' => '一般', + 'persons' => '個人', + 'no-result-found' => 'レコードが見つかりません。', + 'users' => 'ユーザー', + ], + 'updated' => ':attribute を更新しました', + 'created' => '作成しました', + 'duration-overlapping' => 'この時間帯に参加者の別の会議があります。続行しますか?', + 'create-success' => '活動を作成しました。', + 'update-success' => '活動を更新しました。', + 'overlapping-error' => 'この時間帯に参加者の別の会議があります。', + 'destroy-success' => '活動を削除しました。', + 'delete-failed' => '活動を削除できません。', + 'mass-update-success' => '活動を更新しました。', + 'mass-destroy-success' => '活動を削除しました。', + 'mass-delete-failed' => '活動を削除できません。', + ], + 'mail' => [ + 'index' => [ + 'compose' => '作成', + 'draft' => '下書き', + 'inbox' => '受信トレイ', + 'outbox' => '送信トレイ', + 'sent' => '送信済み', + 'trash' => 'ゴミ箱', + 'compose-mail-btn' => 'メールを作成', + 'btn' => 'メール', + 'mail' => [ + 'title' => 'メールの作成', + 'to' => '宛先', + 'enter-emails' => 'Enterキーでメールアドレスを追加', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => '件名', + 'send-btn' => '送信', + 'message' => 'メッセージ', + 'draft' => '下書き', + ], + 'datagrid' => [ + 'id' => 'ID', + 'from' => '差出人', + 'to' => '宛先', + 'subject' => '件名', + 'tags' => 'タグ', + 'content' => '本文', + 'attachments' => '添付ファイル', + 'date' => '日付', + 'move-to-inbox' => '受信トレイへ移動', + 'move-to-trash' => 'ゴミ箱へ移動', + 'edit' => '編集', + 'view' => '表示', + 'delete' => '削除', + ], + ], + 'create-success' => 'メールを送信しました。', + 'update-success' => 'メールを更新しました。', + 'mass-update-success' => 'メールを更新しました。', + 'delete-success' => 'メールを削除しました。', + 'delete-failed' => 'メールを削除できません。', + 'invalid-route' => 'メールのルートが不正です。', + 'unauthorized' => 'この操作は許可されていません。', + 'view' => [ + 'title' => 'メール', + 'subject' => ':subject', + 'link-mail' => 'メールを紐づける', + 'to' => '宛先', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'reply' => '返信', + 'reply-all' => '全員に返信', + 'forward' => '転送', + 'delete' => '削除', + 'enter-mails' => 'メールアドレスを入力', + 'rotten-days' => 'このリードは :days 日間停滞しています', + 'search-an-existing-lead' => '既存のリードを検索', + 'search-an-existing-contact' => '既存の連絡先を検索', + 'message' => 'メッセージ', + 'add-attachments' => '添付ファイルを追加', + 'discard' => '破棄', + 'send' => '送信', + 'no-result-found' => '結果が見つかりません', + 'add-new-contact' => '新しい連絡先を追加', + 'description' => '説明', + 'search' => '検索...', + 'add-new-lead' => '新しいリードを追加', + 'create-new-contact' => '新しい連絡先を作成', + 'save-contact' => '連絡先を保存', + 'create-lead' => 'リードを作成', + 'linked-contact' => '紐づいた連絡先', + 'link-to-contact' => '連絡先に紐づける', + 'link-to-lead' => 'リードに紐づける', + 'linked-lead' => '紐づいたリード', + 'lead-details' => 'リード詳細', + 'contact-person' => '担当者', + 'product' => '製品', + 'tags' => [ + 'create-success' => 'タグを作成しました。', + 'destroy-success' => 'タグを削除しました。', + ], + ], + ], + 'common' => [ + 'custom-attributes' => [ + 'add-more' => 'さらに追加', + 'address' => '住所', + 'city' => '市区町村', + 'contact' => '電話番号', + 'country' => '国', + 'email' => 'メールアドレス', + 'home' => '自宅', + 'postcode' => '郵便番号', + 'save' => '保存', + 'select' => '選択', + 'select-country' => '国を選択', + 'select-state' => '都道府県を選択', + 'state' => '都道府県', + 'update-contact-title' => '電話番号を更新', + 'update-emails-title' => 'メールアドレスを更新', + 'work' => '勤務先', + ], + ], + 'leads' => [ + 'create-success' => 'リードを作成しました。', + 'update-success' => 'リードを更新しました。', + 'update-failed' => 'リードを削除できません。', + 'destroy-success' => 'リードを削除しました。', + 'destroy-failed' => 'リードを削除できません。', + 'file' => [ + 'data-not-found' => 'データが見つかりません。', + 'empty-content' => 'PDFの内容が空か、抽出できませんでした。', + 'failed-extract' => 'ファイルからのテキスト抽出に失敗しました。', + 'insufficient-info' => 'データが不足しているため、現在リクエストを処理できません。', + 'invalid-base64' => 'base64の形式が不正です。', + 'invalid-format' => 'JSONの形式が不正です。', + 'invalid-response' => 'AIレスポンスの形式が不正です。', + 'missing-api-key' => 'APIキーまたはモデル設定がありません。', + 'not-found' => 'ファイルが見つかりません。', + 'recursive-call' => '再帰呼び出しを検出しました。', + 'text-generation-failed' => 'テキスト抽出に失敗しました。ファイルが空か読み取れない可能性があります。', + ], + 'index' => [ + 'title' => 'リード', + 'create-btn' => 'リードを作成', + 'datagrid' => [ + 'id' => 'ID', + 'sales-person' => '営業担当者', + 'subject' => '件名', + 'source' => 'ソース', + 'lead-value' => 'リードの見込み金額', + 'lead-type' => 'リードタイプ', + 'tag-name' => 'タグ名', + 'contact-person' => '担当者', + 'stage' => 'ステージ', + 'rotten-lead' => '停滞リード', + 'date-to' => '終了日', + 'created-at' => '作成日', + 'no' => 'いいえ', + 'yes' => 'はい', + 'delete' => '削除', + 'mass-delete' => '一括削除', + 'mass-update' => '一括更新', + ], + 'kanban' => [ + 'rotten-days' => 'このリードは :days 日間停滞しています', + 'empty-list' => 'リード一覧は空です', + 'empty-list-description' => 'リードを作成して目標を整理しましょう。', + 'create-lead-btn' => 'リードを作成', + 'columns' => [ + 'contact-person' => '担当者', + 'id' => 'ID', + 'lead-type' => 'リードタイプ', + 'lead-value' => 'リードの見込み金額', + 'sales-person' => '営業担当者', + 'source' => 'ソース', + 'title' => 'タイトル', + 'tags' => 'タグ', + 'expected-close-date' => '成約予定日', + 'date-to' => '終了日', + 'created-at' => '作成日', + ], + 'toolbar' => [ + 'search' => [ + 'title' => 'タイトルで検索', + ], + 'filters' => [ + 'apply-filters' => 'フィルターを適用', + 'clear-all' => 'すべてクリア', + 'filter' => 'フィルター', + 'filters' => 'フィルター', + 'from' => '開始', + 'select' => '選択', + 'to' => '終了', + ], + 'card-settings' => [ + 'title' => 'カードの設定', + 'contact-person' => '担当者', + 'rotten-indicator' => '停滞インジケーター', + 'sales-person' => '営業担当者', + 'estimated-lead-value' => '見積もりリード金額', + 'source' => 'ソース', + 'lead-type' => 'リードタイプ', + 'tags' => 'タグ', + ], + ], + 'stages' => [ + 'won' => '受注', + 'lost' => '失注', + 'need-more-info' => '詳細が必要', + 'closed-at' => 'クローズ日', + 'won-value' => '受注金額', + 'lost-reason' => '失注理由', + 'save-btn' => '保存', + ], + ], + 'view-switcher' => [ + 'all-pipelines' => 'すべてのパイプライン', + 'create-new-pipeline' => '新しいパイプラインを作成', + ], + 'upload' => [ + 'create-lead' => 'AIでリードを作成', + 'file' => 'ファイルアップロード', + 'file-info' => 'pdf・bmp・jpg・jpeg・png形式のファイルのみ利用できます。', + 'file-required' => '続行するには有効なファイルを1つ以上選択してください。', + 'save-btn' => '保存', + 'upload-file' => 'ファイルをアップロード', + ], + ], + 'create' => [ + 'title' => 'リードを作成', + 'save-btn' => '保存', + 'details' => '詳細', + 'details-info' => 'リードの基本情報を入力します', + 'contact-person' => '担当者', + 'contact-info' => '担当者に関する情報', + 'products' => '製品', + 'products-info' => '製品に関する情報', + ], + 'edit' => [ + 'title' => 'リードを編集', + 'save-btn' => '保存', + 'details' => '詳細', + 'details-info' => 'リードの基本情報を入力します', + 'contact-person' => '担当者', + 'contact-info' => '担当者に関する情報', + 'products' => '製品', + 'products-info' => '製品に関する情報', + ], + 'common' => [ + 'contact' => [ + 'name' => '氏名', + 'email' => 'メールアドレス', + 'name-search-placeholder' => '氏名・メール・電話番号で検索', + 'contact-number' => '電話番号', + 'organization' => '組織', + ], + 'products' => [ + 'product-name' => '製品名', + 'quantity' => '数量', + 'price' => '価格', + 'amount' => '金額', + 'action' => '操作', + 'add-more' => 'さらに追加', + 'total' => '合計', + ], + ], + 'view' => [ + 'title' => 'リード: :title', + 'rotten-days' => ':days 日', + 'tabs' => [ + 'description' => '説明', + 'products' => '製品', + 'quotes' => '見積', + ], + 'attributes' => [ + 'title' => 'リード情報', + ], + 'quotes' => [ + 'id' => 'ID', + 'subject' => '件名', + 'expired-at' => '有効期限', + 'sub-total' => '小計', + 'discount' => '値引', + 'tax' => '税', + 'adjustment' => '調整', + 'grand-total' => '総合計', + 'delete' => '削除', + 'edit' => '編集', + 'download' => 'ダウンロード', + 'destroy-success' => '見積を削除しました。', + 'empty-title' => '見積が見つかりません', + 'empty-info' => 'このリードには見積がありません', + 'add-btn' => '見積を追加', + 'mail' => 'メール', + 'quote-not-found' => 'このリードの見積が見つかりません。', + 'person-email-unavailable' => 'リードの担当者メールアドレスがありません。', + 'mail-subject' => '見積: :subject', + 'mail-body' => '添付の見積PDFをご確認ください。このメールはシステムにより自動送信されています。', + 'mail-send-failed' => '見積メールを送信できません。', + ], + 'products' => [ + 'product-name' => '製品名', + 'quantity' => '数量', + 'price' => '価格', + 'amount' => '金額', + 'action' => '操作', + 'add-more' => 'さらに追加', + 'total' => '合計', + 'empty-title' => '製品が見つかりません', + 'empty-info' => 'このリードには製品がありません', + 'add-product' => '製品を追加', + ], + 'persons' => [ + 'title' => '担当者情報', + 'job-title' => ':organization の :job_title', + 'no-person' => 'このリードには担当者が割り当てられていません。', + 'attach-btn' => '担当者を割り当てる', + 'change-btn' => '変更', + 'change-title' => '担当者を変更', + 'save-btn' => '保存', + 'cancel-btn' => 'キャンセル', + ], + 'stages' => [ + 'won' => '受注', + 'lost' => '失注', + 'need-more-info' => '詳細が必要', + 'closed-at' => 'クローズ日', + 'won-value' => '受注金額', + 'lost-reason' => '失注理由', + 'save-btn' => '保存', + ], + 'tags' => [ + 'create-success' => 'タグを作成しました。', + 'destroy-success' => 'タグを削除しました。', + ], + ], + ], + 'configuration' => [ + 'index' => [ + 'back' => '戻る', + 'delete' => '削除', + 'save-btn' => '設定を保存', + 'save-success' => '設定を保存しました。', + 'search' => '検索', + 'select-country' => '国を選択', + 'select-state' => '都道府県を選択', + 'title' => '環境設定', + 'general' => [ + 'title' => '一般', + 'info' => '一般設定', + 'general' => [ + 'title' => '一般', + 'info' => '一般設定をここで更新します。', + 'locale-settings' => [ + 'title' => '言語設定', + 'title-info' => '管理パネルの表示言語を設定します。ドロップダウンにはインストール済みのすべての言語が表示されます。', + ], + 'admin-logo' => [ + 'logo-image' => 'ロゴ画像', + 'title' => '管理パネルのロゴ', + 'title-info' => '管理パネルのロゴ画像を設定します。', + 'favicon-image' => 'ファビコン画像', + 'favicon-image-info' => '管理パネルのファビコン画像を設定します。', + ], + ], + 'settings' => [ + 'title' => '設定', + 'info' => '設定をここで更新します。', + 'footer' => [ + 'show' => 'Powered By を表示', + 'show-info' => '管理パネル下部の「Powered by」バーの表示・非表示を切り替えます。', + 'info' => 'Powered by セクションをここで設定できます。', + 'powered-by' => 'Powered by テキストエディタ', + 'title' => 'Powered by セクションの設定', + ], + 'menu' => [ + 'activities' => '活動', + 'configuration' => '環境設定', + 'contacts' => '連絡先', + 'dashboard' => 'ダッシュボード', + 'draft' => '下書き', + 'inbox' => '受信トレイ', + 'info' => 'メニュー項目の名前をここで設定できます。', + 'leads' => 'リード', + 'mail' => 'メール', + 'organizations' => '組織', + 'outbox' => '送信トレイ', + 'persons' => '個人', + 'products' => '製品', + 'quotes' => '見積', + 'sent' => '送信済み', + 'settings' => '設定', + 'title' => 'メニュー項目の設定', + 'trash' => 'ゴミ箱', + ], + 'dashboard' => [ + 'title' => 'ダッシュボードの設定', + 'info' => 'ダッシュボードの既定の期間をここで設定できます。', + 'date-range' => '既定の期間', + 'date-range-info' => 'ダッシュボードを開いたときに読み込む期間です。ダッシュボード上の日付ピッカーで一時的に変更もできます。', + 'custom-days' => 'カスタム期間(日数)', + 'custom-days-info' => '期間を「カスタム」にしたときに遡る日数です。', + 'ranges' => [ + '1-month' => '1か月', + '3-months' => '3か月', + '9-months' => '9か月', + '1-year' => '1年', + '2-years' => '2年', + 'custom' => 'カスタム', + ], + ], + 'menu-color' => [ + 'brand-color' => 'ブランドカラー', + 'info' => 'メニュー項目の色をここで変更できます。', + 'title' => 'メニュー項目の色設定', + ], + ], + ], + 'email' => [ + 'title' => 'メール設定', + 'info' => 'アプリケーションのメール設定です。', + 'imap' => [ + 'title' => 'IMAP設定', + 'info' => 'メール受信用のIMAP設定です。', + 'account' => [ + 'title' => 'IMAPアカウント', + 'title-info' => 'IMAPアカウントの設定をここで行います。', + 'host' => 'ホスト', + 'port' => 'ポート', + 'encryption' => '暗号化方式', + 'validate-cert' => 'SSL証明書を検証', + 'username' => 'IMAPユーザー名', + 'password' => 'IMAPパスワード', + ], + ], + ], + 'magic-ai' => [ + 'title' => 'Magic AI', + 'info' => 'アプリケーションのMagic AI設定です。', + 'settings' => [ + 'api-key' => 'APIキー', + 'api-key-info' => '各モデルにはOpenRouterのAPIキーを使用してください。セキュリティとパフォーマンスを高めるためのシンプルな一手間です。', + 'enable' => '有効化', + 'info' => 'OpenRouterのAPIキーでMagic AI体験を強化しましょう。設定を簡単にカスタマイズして、あなたに合ったAI活用を始められます。', + 'other' => 'その他のモデル', + 'other-model' => 'その他のモデルは、OpenRouterのモデルIDを使用してください。', + 'doc-generation' => 'DOC生成', + 'doc-generation-info' => 'DOC生成機能を有効にすると、DOCファイルからデータを自動抽出してテキスト形式に変換できます。ワークフローの効率化に役立ちます。', + 'title' => '一般設定', + 'models' => [ + 'deepseek-r1' => 'Deepseek R1 Distill-llama-8b', + 'gemini-2-0-flash-001' => 'Gemini 2.0 flash-001', + 'gpt-4o' => 'GPT-4.0', + 'gpt-4o-mini' => 'GPT-4.0 mini', + 'grok-2-1212' => 'Grok 2.12', + 'llama-3-2-3b-instruct' => 'Llama 3.2 3b Instruct', + 'title' => 'モデル', + ], + ], + ], + ], + ], + 'dashboard' => [ + 'index' => [ + 'title' => 'ダッシュボード', + 'start-date' => '開始日', + 'end-date' => '終了日', + 'revenue' => [ + 'lost-revenue' => '失注金額', + 'won-revenue' => '受注金額', + ], + 'over-all' => [ + 'average-lead-value' => '平均リード金額', + 'total-leads' => 'リード総数', + 'average-leads-per-day' => '1日あたり平均リード数', + 'total-quotations' => '見積総数', + 'total-persons' => '個人総数', + 'total-organizations' => '組織総数', + ], + 'total-leads' => [ + 'title' => 'リード', + 'total' => 'リード総数', + 'won' => '受注リード', + 'lost' => '失注リード', + ], + 'revenue-by-sources' => [ + 'title' => 'ソース別売上', + 'empty-title' => 'データがありません', + 'empty-info' => '選択した期間のデータがありません', + ], + 'revenue-by-types' => [ + 'title' => 'タイプ別売上', + 'empty-title' => 'データがありません', + 'empty-info' => '選択した期間のデータがありません', + ], + 'top-selling-products' => [ + 'title' => '上位製品', + 'empty-title' => '製品が見つかりません', + 'empty-info' => '選択した期間の製品がありません', + ], + 'top-persons' => [ + 'title' => '上位個人', + 'empty-title' => '個人が見つかりません', + 'empty-info' => '選択した期間の個人がありません', + ], + 'open-leads-by-states' => [ + 'title' => 'ステージ別オープンリード', + 'empty-title' => 'データがありません', + 'empty-info' => '選択した期間のデータがありません', + ], + 'export-pdf' => 'PDFエクスポート', + 'pdf' => [ + 'title' => 'ダッシュボードレポート', + 'filtered-date' => 'フィルター日付', + 'revenue' => '収益', + 'overview' => '概要', + 'total-leads' => 'リード総数', + 'metric' => '指標', + 'current' => '現在', + 'previous' => '前回', + 'progress' => '進捗', + 'date' => '日付', + 'all' => 'すべて', + 'total' => '合計', + 'won' => '受注', + 'lost' => '失注', + 'source' => 'ソース', + 'type' => 'タイプ', + 'stage' => 'ステージ', + 'product' => '製品', + 'person' => '個人', + 'price' => '価格', + 'revenue-by-sources' => 'ソース別収益', + 'revenue-by-types' => 'タイプ別収益', + 'open-leads-by-stages' => 'ステージ別オープンリード', + 'top-selling-products' => '売れ筋製品', + 'top-persons' => '上位個人', + ], + ], + ], + 'help' => [ + 'index' => [ + 'title' => 'ヘルプとリソース', + 'description' => 'Krayin管理パネルを最大限に活用するために——ホスティング、サポート、プロフェッショナルサービス、拡張機能、開発者ドキュメントをご案内します。', + 'contact-us' => 'お問い合わせ', + 'view-all' => 'すべて見る', + 'still-need-help-title' => 'まだお困りですか?', + 'still-need-help-description' => 'ホスティング、カスタム開発、その他何でもKrayinチームにご相談ください。', + + 'services' => [ + 'title' => 'サービスとリソース', + 'cloud-hosting' => [ + 'title' => 'クラウドホスティング', + 'description' => 'コスト効率の高いマネージドクラウドホスティング。最適化されたスケーラブルな環境で、数分でKrayinをクラウドに立ち上げられます。', + ], + 'support' => [ + 'title' => 'サポートと保守', + 'description' => '専任のテクニカルサポートと継続的な保守プランで、CRMを安全に、最新の状態で、安定して稼働させます。', + ], + 'extensions' => [ + 'title' => '拡張機能', + 'description' => '公式・コミュニティのアドオンを探して、新しいコネクタ・チャネル・機能でKrayinを拡張できます。', + ], + ], + + 'resources' => [ + 'title' => 'ドキュメント', + 'user-docs' => [ + 'title' => 'ユーザーガイド', + 'description' => 'モジュール統合、カスタマイズ、データ移行、バージョンアップ、受託開発の専門的なサポート情報です。', + ], + 'dev-docs' => [ + 'title' => '開発ドキュメントとブログ', + 'description' => '構築・設定・最新情報のキャッチアップに役立つ開発者ガイド、チュートリアル、最新記事です。', + ], + 'api-docs' => [ + 'title' => 'APIドキュメント', + 'description' => 'エンドポイント・認証・サンプルを網羅したREST APIリファレンスで、Krayinをお使いのシステムと連携できます。', + ], + ], + + 'community' => [ + 'forums' => [ + 'title' => 'コミュニティフォーラム', + 'description' => '世界中のKrayinユーザー・開発者とつながり、ヒントの共有や問題解決ができます。', + 'link' => 'コミュニティに参加', + ], + 'tutorials' => [ + 'title' => '動画チュートリアル', + 'description' => 'CRMのセットアップ、パイプラインの設定、ユーザー管理の手順を動画で学べます。', + 'link' => 'YouTubeチャンネルを見る', + ], + ], + ], + ], + 'layouts' => [ + 'app-version' => 'バージョン: :version', + 'dashboard' => 'ダッシュボード', + 'help' => 'ヘルプとリソース', + 'leads' => 'リード', + 'quotes' => '見積', + 'quote' => '見積', + 'mail' => [ + 'title' => 'メール', + 'compose' => '作成', + 'inbox' => '受信トレイ', + 'draft' => '下書き', + 'outbox' => '送信トレイ', + 'sent' => '送信済み', + 'trash' => 'ゴミ箱', + 'setting' => '設定', + ], + 'activities' => '活動', + 'contacts' => '連絡先', + 'persons' => '個人', + 'person' => '個人', + 'organizations' => '組織', + 'organization' => '組織', + 'products' => '製品', + 'product' => '製品', + 'settings' => '設定', + 'user' => 'ユーザー', + 'user-info' => 'CRMのすべてのユーザーと、その権限(何を許可するか)を管理します。', + 'groups' => 'グループ', + 'groups-info' => 'CRMのグループを追加・編集・削除します', + 'roles' => 'ロール', + 'role' => 'ロール', + 'roles-info' => 'CRMのロールを追加・編集・削除します', + 'users' => 'ユーザー', + 'users-info' => 'CRMのユーザーを追加・編集・削除します', + 'lead' => 'リード', + 'lead-info' => 'CRMのリード関連の設定を管理します', + 'sidebar' => [ + 'collapse' => '折りたたむ', + 'expand' => '展開する', + ], + 'quick-add' => [ + 'title' => 'クイック追加', + 'save' => '保存', + 'to' => '宛先', + 'subject' => '件名', + 'message' => 'メッセージ', + ], + 'pipelines' => 'パイプライン', + 'pipelines-info' => 'CRMのパイプラインを追加・編集・削除します', + 'sources' => 'ソース', + 'sources-info' => 'CRMのソースを追加・編集・削除します', + 'types' => 'タイプ', + 'types-info' => 'CRMのタイプを追加・編集・削除します', + 'automation' => '自動化', + 'automation-info' => 'CRMの自動化関連の設定を管理します', + 'attributes' => '属性', + 'attribute' => '属性', + 'attributes-info' => 'CRMの属性を追加・編集・削除します', + 'email-templates' => 'メールテンプレート', + 'email' => 'メール', + 'email-templates-info' => 'CRMのメールテンプレートを追加・編集・削除します', + 'events' => 'イベント', + 'events-info' => 'CRMのイベントを追加・編集・削除します', + 'campaigns' => 'キャンペーン', + 'campaigns-info' => 'CRMのキャンペーンを追加・編集・削除します', + 'workflows' => 'ワークフロー', + 'workflows-info' => 'CRMのワークフローを追加・編集・削除します', + 'webhooks' => 'Webhook', + 'webhooks-info' => 'CRMのWebhookを追加・編集・削除します', + 'other-settings' => 'その他の設定', + 'other-settings-info' => 'CRMのその他の設定を管理します', + 'tags' => 'タグ', + 'tags-info' => 'CRMのタグを追加・編集・削除します', + 'my-account' => 'マイアカウント', + 'sign-out' => 'サインアウト', + 'back' => '戻る', + 'name' => '名前', + 'configuration' => '環境設定', + 'howdy' => 'こんにちは!', + 'warehouses' => '倉庫', + 'warehouse' => '倉庫', + 'warehouses-info' => 'CRMの倉庫を追加・編集・削除します', + 'inventory' => '在庫', + 'inventory-info' => 'CRMの在庫関連の設定を管理します', + 'data_transfer' => 'データ転送', + 'data_transfer_info' => 'CRMの個人・製品・リードのデータ転送関連の設定を管理します', + ], + 'user' => [ + 'account' => [ + 'name' => '名前', + 'email' => 'メールアドレス', + 'password' => 'パスワード', + 'my_account' => 'マイアカウント', + 'update_details' => '詳細を更新', + 'current_password' => '現在のパスワード', + 'confirm_password' => 'パスワード(確認)', + 'password-match' => '現在のパスワードが一致しません。', + 'account-save' => 'アカウントの変更を保存しました。', + 'permission-denied' => '権限がありません', + 'remove-image' => '画像を削除', + 'upload_image_pix' => 'プロフィール画像(100px×100px)をアップロード', + 'upload_image_format' => 'PNGまたはJPG形式', + 'image_upload_message' => '画像(.jpeg、.jpg、.png など)のみ利用できます。', + ], + ], + 'emails' => [ + 'common' => [ + 'dear' => ':name 様', + 'cheers' => 'よろしくお願いいたします。
    :app_name チーム', + 'user' => [ + 'dear' => ':username 様', + 'create-subject' => 'メンバーとして追加されました。', + 'create-body' => 'おめでとうございます!あなたはチームのメンバーになりました。', + 'forget-password' => [ + 'subject' => 'パスワード再設定のご案内', + 'dear' => ':username 様', + 'reset-password' => 'パスワードを再設定', + 'info' => 'お客様のアカウントに対するパスワード再設定のリクエストを受け付けたため、このメールをお送りしています', + 'final-summary' => 'パスワード再設定をリクエストしていない場合、このメールへの対応は不要です', + 'thanks' => 'よろしくお願いいたします!', + ], + ], + ], + ], + 'validations' => [ + 'message' => [ + 'code' => 'このフィールドは有効なコードである必要があります。', + 'decimal' => ':attribute は小数である必要があります。', + ], + ], + 'errors' => [ + 'dashboard' => 'ダッシュボード', + 'go-back' => '戻る', + 'support' => '問題が解決しない場合は、:email までお問い合わせください。', + 'unauthorized' => 'この操作は許可されていません。', + 'role-exceeds-own' => '自分と同等以下の権限を持つロールのみ割り当てることができます。', + 'user-exceeds-own' => '自分と同等以下の権限を持つユーザーのみ管理できます。', + 'own-privileges' => '自分自身のロールやデータスコープは変更できません。', + 'scope-exceeds-own' => '自分よりも広いデータスコープを付与することはできません。', + 'primary-admin-protected' => 'プライマリ管理者アカウントは保護されており、ここでは変更できません。', + 'role-permissions-exceed-own' => '自分と同等以下の権限を持つロールのみ作成または編集できます。', + + '404' => [ + 'description' => 'お探しのページが見つかりませんでした。移動または削除された可能性があります。', + 'title' => '404 ページが見つかりません', + ], + '401' => [ + 'description' => 'このページへのアクセスは許可されていません。必要な認証情報が不足しているようです。', + 'title' => '401 認証エラー', + ], + '403' => [ + 'description' => 'このページは閲覧できません。このコンテンツを表示するために必要な権限がないようです。', + 'title' => '403 アクセス禁止', + ], + '500' => [ + 'description' => '問題が発生しました。お探しのページの読み込みに失敗しています。', + 'title' => '500 サーバー内部エラー', + ], + '503' => [ + 'description' => 'ただいまメンテナンスのため一時的に停止しています。しばらくしてからもう一度お試しください。', + 'title' => '503 サービス利用不可', + ], + ], + 'export' => [ + 'csv' => 'CSV', + 'download' => 'ダウンロード', + 'export' => 'エクスポート', + 'google-contacts' => 'Google連絡先', + 'google-contacts-in-progress' => 'Google連絡先へエクスポートしています…しばらくお待ちください。', + 'google-contacts-total' => '連絡先の総数:', + 'google-contacts-exported' => 'エクスポート済み:', + 'google-contacts-duplicate' => '既に存在:', + 'google-contacts-failed' => '失敗:', + 'google-contacts-summary' => 'エクスポート完了: :exported 件エクスポート、:duplicate 件は既に存在、:failed 件失敗。', + 'no-records' => 'エクスポートするものがありません', + 'xls' => 'XLS', + 'xlsx' => 'XLSX', + ], +]; diff --git a/packages/Webkul/Admin/src/Resources/lang/ko/app.php b/packages/Webkul/Admin/src/Resources/lang/ko/app.php new file mode 100644 index 0000000..da1dd47 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/lang/ko/app.php @@ -0,0 +1,2477 @@ + [ + 'help' => '도움말', + 'leads' => '리드', + 'lead' => '리드', + 'quotes' => '견적서', + 'mail' => '메일', + 'inbox' => '받은편지함', + 'draft' => '임시보관함', + 'outbox' => '발신함', + 'sent' => '보낸편지함', + 'trash' => '휴지통', + 'activities' => '활동', + 'webhook' => '웹훅', + 'contacts' => '연락처', + 'persons' => '개인', + 'organizations' => '조직', + 'products' => '제품', + 'settings' => '설정', + 'groups' => '그룹', + 'roles' => '역할', + 'users' => '사용자', + 'user' => '사용자', + 'automation' => '자동화', + 'attributes' => '속성', + 'pipelines' => '파이프라인', + 'sources' => '소스', + 'types' => '유형', + 'email-templates' => '이메일 템플릿', + 'workflows' => '워크플로우', + 'other-settings' => '기타 설정', + 'tags' => '태그', + 'configuration' => '구성', + 'create' => '생성', + 'quick_add' => '빠른 추가', + 'edit' => '수정', + 'view' => '보기', + 'print' => '인쇄', + 'delete' => '삭제', + 'export' => '내보내기', + 'mass-delete' => '일괄 삭제', + 'data-transfer' => '데이터 전송', + 'imports' => '가져오기', + 'import' => '가져오기', + 'event' => '이벤트', + 'campaigns' => '캠페인', + 'warehouses' => '창고', + 'inventory' => '재고', + ], + 'users' => [ + 'activate-warning' => '고객님의 계정이 아직 활성화되지 않았습니다. 관리자에게 문의해 주세요.', + 'login-error' => '입력하신 정보가 저희 기록과 일치하지 않습니다.', + 'not-permission' => '관리자 패널에 접근할 권한이 없습니다.', + 'login' => [ + 'email' => '이메일 주소', + 'forget-password-link' => '비밀번호를 잊으셨나요?', + 'password' => '비밀번호', + 'submit-btn' => '로그인', + 'title' => '로그인', + ], + 'forget-password' => [ + 'create' => [ + 'email' => '등록된 이메일', + 'email-not-exist' => '존재하지 않는 이메일입니다', + 'page-title' => '비밀번호 찾기', + 'reset-link-sent' => '비밀번호 재설정 링크가 전송되었습니다', + 'sign-in-link' => '로그인 화면으로 돌아가기', + 'submit-btn' => '재설정', + 'title' => '비밀번호 복구', + ], + ], + 'reset-password' => [ + 'back-link-title' => '로그인 화면으로 돌아가기', + 'confirm-password' => '비밀번호 확인', + 'email' => '등록된 이메일', + 'password' => '비밀번호', + 'submit-btn' => '비밀번호 재설정', + 'title' => '비밀번호 재설정', + ], + ], + 'account' => [ + 'edit' => [ + 'back-btn' => '뒤로', + 'change-password' => '비밀번호 변경', + 'confirm-password' => '비밀번호 확인', + 'current-password' => '현재 비밀번호', + 'email' => '이메일', + 'general' => '일반', + 'invalid-password' => '입력하신 현재 비밀번호가 올바르지 않습니다.', + 'name' => '이름', + 'password' => '비밀번호', + 'profile-image' => '프로필 이미지', + 'save-btn' => '계정 저장', + 'title' => '내 계정', + 'update-success' => '계정이 성공적으로 업데이트되었습니다', + 'upload-image-info' => '프로필 이미지를 업로드하세요 (110px X 110px, PNG 또는 JPG 형식)', + ], + ], + 'components' => [ + 'activities' => [ + 'actions' => [ + 'mail' => [ + 'btn' => '메일', + 'title' => '메일 작성', + 'to' => '받는사람', + 'enter-emails' => '이메일을 추가하려면 Enter 키를 누르세요', + 'cc' => '참조', + 'bcc' => '숨은참조', + 'subject' => '제목', + 'send-btn' => '보내기', + 'message' => '메시지', + ], + 'file' => [ + 'btn' => '파일', + 'title' => '파일 추가', + 'title-control' => '제목', + 'name' => '이름', + 'description' => '설명', + 'file' => '파일', + 'save-btn' => '파일 저장', + ], + 'note' => [ + 'btn' => '메모', + 'title' => '메모 추가', + 'comment' => '댓글', + 'save-btn' => '메모 저장', + ], + 'activity' => [ + 'btn' => '활동', + 'title' => '활동 추가', + 'title-control' => '제목', + 'description' => '설명', + 'schedule-from' => '시작 일정', + 'schedule-to' => '종료 일정', + 'location' => '위치', + 'call' => '통화', + 'meeting' => '미팅', + 'lunch' => '점심 식사', + 'save-btn' => '활동 저장', + 'participants' => [ + 'title' => '참가자', + 'placeholder' => '참가자를 검색하려면 입력하세요', + 'users' => '사용자', + 'persons' => '개인', + 'no-results' => '결과가 없습니다...', + ], + ], + ], + 'index' => [ + 'all' => '전체', + 'bcc' => '숨은참조', + 'by-user' => ':user 님이 작성', + 'calls' => '통화', + 'cc' => '참조', + 'change-log' => '변경 로그', + 'delete' => '삭제', + 'edit' => '수정', + 'emails' => '이메일', + 'empty' => '비어 있음', + 'files' => '파일', + 'from' => '보낸사람', + 'location' => '위치', + 'lunches' => '점심 식사', + 'mark-as-done' => '완료로 표시', + 'meetings' => '미팅', + 'notes' => '메모', + 'participants' => '참가자', + 'planned' => '예정됨', + 'quotes' => '견적서', + 'scheduled-on' => '예정일', + 'system' => '시스템', + 'to' => '받는사람', + 'unlink' => '연결 해제', + 'view' => '보기', + 'empty-placeholders' => [ + 'all' => [ + 'title' => '활동이 없습니다', + 'description' => '이에 대한 활동이 없습니다. 왼쪽 패널의 활동 버튼을 클릭하여 활동을 추가할 수 있습니다.', + ], + 'planned' => [ + 'title' => '예정된 활동이 없습니다', + 'description' => '이에 대한 예정된 활동이 없습니다. 왼쪽 패널의 활동 버튼을 클릭하여 예정된 활동을 추가할 수 있습니다.', + ], + 'notes' => [ + 'title' => '메모가 없습니다', + 'description' => '이에 대한 메모가 없습니다. 왼쪽 패널의 메모 버튼을 클릭하여 메모를 추가할 수 있습니다.', + ], + 'calls' => [ + 'title' => '통화 기록이 없습니다', + 'description' => '이에 대한 통화 기록이 없습니다. 왼쪽 패널의 활동 버튼을 클릭한 후 통화 유형을 선택하여 통화를 추가할 수 있습니다.', + ], + 'meetings' => [ + 'title' => '미팅이 없습니다', + 'description' => '이에 대한 미팅이 없습니다. 왼쪽 패널의 활동 버튼을 클릭한 후 미팅 유형을 선택하여 미팅을 추가할 수 있습니다.', + ], + 'lunches' => [ + 'title' => '점심 식사 일정이 없습니다', + 'description' => '이에 대한 점심 식사 일정이 없습니다. 왼쪽 패널의 활동 버튼을 클릭한 후 점심 식사 유형을 선택하여 점심 식사 일정을 추가할 수 있습니다.', + ], + 'files' => [ + 'title' => '파일이 없습니다', + 'description' => '이에 대한 파일이 없습니다. 왼쪽 패널의 파일 버튼을 클릭하여 파일을 추가할 수 있습니다.', + ], + 'emails' => [ + 'title' => '이메일이 없습니다', + 'description' => '이에 대한 이메일이 없습니다. 왼쪽 패널의 메일 버튼을 클릭하여 이메일을 추가할 수 있습니다.', + ], + 'system' => [ + 'title' => '변경 로그가 없습니다', + 'description' => '이에 대한 변경 로그가 없습니다.', + ], + ], + ], + ], + 'media' => [ + 'images' => [ + 'add-image-btn' => '이미지 추가', + 'ai-add-image-btn' => 'Magic AI', + 'allowed-types' => 'png, jpeg, jpg', + 'not-allowed-error' => '이미지 파일(.jpeg, .jpg, .png 등)만 허용됩니다.', + 'placeholders' => [ + 'front' => '앞면', + 'next' => '다음', + 'size' => '크기', + 'use-cases' => '사용 사례', + 'zoom' => '확대', + ], + ], + 'videos' => [ + 'add-video-btn' => '동영상 추가', + 'allowed-types' => 'mp4, webm, mkv', + 'not-allowed-error' => '동영상 파일(.mp4, .mov, .ogg 등)만 허용됩니다.', + ], + ], + 'datagrid' => [ + 'index' => [ + 'no-records-selected' => '선택된 레코드가 없습니다.', + 'must-select-a-mass-action-option' => '일괄 작업의 옵션을 선택해야 합니다.', + 'must-select-a-mass-action' => '일괄 작업을 선택해야 합니다.', + ], + 'toolbar' => [ + 'length-of' => ':length /', + 'of' => '/', + 'per-page' => '페이지당', + 'results' => '총 :total개 결과', + 'delete' => '삭제', + 'selected' => ':total개 항목 선택됨', + 'mass-actions' => [ + 'submit' => '제출', + 'select-option' => '옵션 선택', + 'select-action' => '작업 선택', + ], + 'filter' => [ + 'apply-filters-btn' => '필터 적용', + 'back-btn' => '뒤로', + 'create-new-filter' => '새 필터 생성', + 'custom-filters' => '사용자 지정 필터', + 'delete-error' => '필터를 삭제하는 중 문제가 발생했습니다. 다시 시도해 주세요.', + 'delete-success' => '필터가 성공적으로 삭제되었습니다.', + 'empty-description' => '저장할 수 있는 선택된 필터가 없습니다. 저장할 필터를 선택해 주세요.', + 'empty-title' => '저장할 필터 추가', + 'name' => '이름', + 'quick-filters' => '빠른 필터', + 'save-btn' => '저장', + 'save-filter' => '필터 저장', + 'saved-success' => '필터가 성공적으로 저장되었습니다.', + 'selected-filters' => '선택된 필터', + 'title' => '필터', + 'update' => '수정', + 'update-filter' => '필터 수정', + 'updated-success' => '필터가 성공적으로 수정되었습니다.', + ], + 'search' => [ + 'title' => '검색', + ], + 'column-settings' => [ + 'title' => '열 설정', + ], + ], + 'filters' => [ + 'select' => '선택', + 'title' => '필터', + 'dropdown' => [ + 'searchable' => [ + 'at-least-two-chars' => '최소 2자 이상 입력하세요...', + 'no-results' => '결과가 없습니다...', + ], + ], + 'custom-filters' => [ + 'clear-all' => '전체 지우기', + 'title' => '사용자 지정 필터', + ], + 'boolean-options' => [ + 'false' => '거짓', + 'true' => '참', + ], + 'date-options' => [ + 'last-month' => '지난달', + 'last-six-months' => '지난 6개월', + 'last-three-months' => '지난 3개월', + 'this-month' => '이번 달', + 'this-week' => '이번 주', + 'this-year' => '올해', + 'today' => '오늘', + 'yesterday' => '어제', + ], + ], + 'table' => [ + 'actions' => '작업', + 'no-records-available' => '사용 가능한 레코드가 없습니다.', + ], + ], + 'modal' => [ + 'confirm' => [ + 'agree-btn' => '동의', + 'disagree-btn' => '동의 안 함', + 'message' => '이 작업을 수행하시겠습니까?', + 'title' => '확인하시겠습니까?', + ], + ], + 'tags' => [ + 'index' => [ + 'title' => '태그', + 'added-tags' => '추가된 태그', + 'save-btn' => '태그 저장', + 'placeholder' => '태그를 검색하려면 입력하세요', + 'add-tag' => '\\":term\\" 추가...', + 'aquarelle-red' => '아쿠아렐 레드', + 'crushed-cashew' => '크러쉬드 캐슈', + 'beeswax' => '비즈왁스', + 'lemon-chiffon' => '레몬 쉬폰', + 'snow-flurry' => '스노우 플러리', + 'honeydew' => '허니듀', + ], + ], + 'layouts' => [ + 'powered-by' => [ + 'description' => ':krayin이(가) 제공하는 :webkul의 오픈소스 프로젝트입니다.', + ], + 'header' => [ + 'mega-search' => [ + 'title' => '메가 검색', + 'tabs' => [ + 'leads' => '리드', + 'quotes' => '견적서', + 'persons' => '개인', + 'products' => '제품', + 'settings' => '설정', + 'configurations' => '구성', + ], + 'explore-all-products' => '모든 제품 보기', + 'explore-all-leads' => '모든 리드 보기', + 'explore-all-contacts' => '모든 연락처 보기', + 'explore-all-quotes' => '모든 견적서 보기', + 'explore-all-settings' => '모든 설정 보기', + 'explore-all-configurations' => '모든 구성 보기', + 'explore-all-matching-products' => '\\":query\\"와(과) 일치하는 모든 제품 보기 (:count)', + 'explore-all-matching-leads' => '\\":query\\"와(과) 일치하는 모든 리드 보기 (:count)', + 'explore-all-matching-contacts' => '\\":query\\"와(과) 일치하는 모든 연락처 보기 (:count)', + 'explore-all-matching-quotes' => '\\":query\\"와(과) 일치하는 모든 견적서 보기 (:count)', + ], + ], + ], + 'attributes' => [ + 'edit' => [ + 'delete' => '삭제', + ], + 'lookup' => [ + 'click-to-add' => '클릭하여 추가', + 'search' => '검색...', + 'no-result-found' => '결과가 없습니다', + ], + ], + 'lookup' => [ + 'click-to-add' => '클릭하여 추가', + 'no-results' => '결과를 찾을 수 없습니다', + 'add-as-new' => '새로 추가', + 'search' => '검색...', + ], + 'flash-group' => [ + 'success' => '성공', + 'error' => '오류', + 'warning' => '경고', + 'info' => '정보', + ], + 'tiny-mce' => [ + 'http-error' => 'HTTP 오류', + 'invalid-json' => '서버로부터 잘못된 JSON 응답을 받았습니다.', + 'upload-failed' => '파일 업로드에 실패했습니다. 다시 시도해 주세요.', + ], + ], + 'quotes' => [ + 'index' => [ + 'title' => '견적서', + 'create-btn' => '견적서 생성', + 'create-success' => '견적서가 생성되었습니다.', + 'update-success' => '견적서가 수정되었습니다.', + 'delete-success' => '견적서가 삭제되었습니다.', + 'delete-failed' => '견적서를 삭제할 수 없습니다.', + 'datagrid' => [ + 'subject' => '제목', + 'sales-person' => '영업 담당자', + 'expired-at' => '만료일', + 'created-at' => '생성일', + 'person' => '개인', + 'subtotal' => '소계', + 'discount' => '할인', + 'tax' => '세금', + 'adjustment' => '조정', + 'grand-total' => '총합계', + 'edit' => '수정', + 'delete' => '삭제', + 'print' => '인쇄', + ], + 'pdf' => [ + 'adjustment' => '조정', + 'amount' => '금액', + 'billing-address' => '청구지 주소', + 'date' => '날짜', + 'discount' => '할인', + 'description' => '설명', + 'expired-at' => '만료일', + 'grand-total' => '총합계', + 'person' => '개인', + 'price' => '가격', + 'product-name' => '제품명', + 'quantity' => '수량', + 'quote-id' => '견적서 ID', + 'sales-person' => '영업 담당자', + 'shipping-address' => '배송지 주소', + 'sku' => 'SKU', + 'sub-total' => '소계', + 'subject' => '제목', + 'tax' => '세금', + 'title' => '견적서', + ], + ], + 'create' => [ + 'title' => '견적서 생성', + 'save-btn' => '견적서 저장', + 'quote-info' => '견적서 정보', + 'quote-info-info' => '견적서의 기본 정보를 입력하세요.', + 'address-info' => '주소 정보', + 'address-info-info' => '견적서와 관련된 주소 정보입니다.', + 'same-as-billing' => '배송 주소가 청구 주소와 동일합니다', + 'quote-items' => '견적서 항목', + 'search-products' => '제품 검색', + 'link-to-lead' => '리드에 연결', + 'quote-item-info' => '이 견적서에 대한 제품 요청을 추가하세요.', + 'quote-name' => '견적서 이름', + 'quantity' => '수량', + 'price' => '가격', + 'discount' => '할인', + 'tax' => '세금', + 'total' => '합계', + 'amount' => '금액', + 'add-item' => '+ 항목 추가', + 'sub-total' => '소계 (:symbol)', + 'total-discount' => '할인 (:symbol)', + 'total-tax' => '세금 (:symbol)', + 'total-adjustment' => '조정 (:symbol)', + 'grand-total' => '총합계 (:symbol)', + 'discount-amount' => '할인 금액', + 'tax-amount' => '세금 금액', + 'adjustment-amount' => '조정 금액', + 'product-name' => '제품명', + 'action' => '작업', + ], + 'edit' => [ + 'title' => '견적서 수정', + 'save-btn' => '견적서 저장', + 'quote-info' => '견적서 정보', + 'quote-info-info' => '견적서의 기본 정보를 입력하세요.', + 'address-info' => '주소 정보', + 'address-info-info' => '견적서와 관련된 주소 정보입니다.', + 'quote-items' => '견적서 항목', + 'link-to-lead' => '리드에 연결', + 'quote-item-info' => '이 견적서에 대한 제품 요청을 추가하세요.', + 'quote-name' => '견적서 이름', + 'quantity' => '수량', + 'price' => '가격', + 'search-products' => '제품 검색', + 'discount' => '할인', + 'tax' => '세금', + 'total' => '합계', + 'amount' => '금액', + 'add-item' => '+ 항목 추가', + 'sub-total' => '소계 (:symbol)', + 'total-discount' => '할인 (:symbol)', + 'total-tax' => '세금 (:symbol)', + 'total-adjustment' => '조정 (:symbol)', + 'grand-total' => '총합계 (:symbol)', + 'discount-amount' => '할인 금액', + 'tax-amount' => '세금 금액', + 'adjustment-amount' => '조정 금액', + 'product-name' => '제품명', + 'action' => '작업', + ], + ], + 'contacts' => [ + 'persons' => [ + 'index' => [ + 'title' => '개인', + 'create-btn' => '개인 생성', + 'create-success' => '개인이 생성되었습니다.', + 'update-success' => '개인이 수정되었습니다.', + 'all-delete-success' => '선택한 모든 개인이 삭제되었습니다.', + 'partial-delete-warning' => '일부 개인은 삭제되었습니다. 나머지는 리드에 연결되어 있어 삭제할 수 없습니다.', + 'none-delete-warning' => '선택한 개인은 모두 리드에 연결되어 있어 삭제할 수 없습니다.', + 'no-selection' => '삭제할 개인이 선택되지 않았습니다.', + 'delete-failed' => '선택한 개인을 삭제하지 못했습니다.', + 'datagrid' => [ + 'contact-numbers' => '연락처 번호', + 'delete' => '삭제', + 'edit' => '수정', + 'emails' => '이메일', + 'id' => 'ID', + 'view' => '보기', + 'name' => '이름', + 'organization-name' => '조직명', + 'tag-name' => '태그 이름', + 'google-export-not-connected' => '설정 > 기타 설정 > Google 연락처에서 먼저 Google 계정을 연결해 주세요.', + 'google-export-started' => 'Google 연락처로 내보내기가 시작되었습니다.', + ], + ], + 'view' => [ + 'title' => ':name', + 'about-person' => '개인 정보', + 'about-organization' => '조직 정보', + 'activities' => [ + 'index' => [ + 'all' => '전체', + 'calls' => '통화', + 'meetings' => '미팅', + 'lunches' => '식사', + 'files' => '파일', + 'quotes' => '견적서', + 'notes' => '메모', + 'emails' => '이메일', + 'by-user' => ':user 님이 작성', + 'scheduled-on' => '예정일', + 'location' => '위치', + 'participants' => '참가자', + 'mark-as-done' => '완료로 표시', + 'delete' => '삭제', + 'edit' => '수정', + ], + 'actions' => [ + 'mail' => [ + 'btn' => '메일', + 'title' => '메일 작성', + 'to' => '받는사람', + 'cc' => '참조', + 'bcc' => '숨은참조', + 'subject' => '제목', + 'send-btn' => '보내기', + 'message' => '메시지', + ], + 'file' => [ + 'btn' => '파일', + 'title' => '파일 추가', + 'title-control' => '제목', + 'name' => '파일명', + 'description' => '설명', + 'file' => '파일', + 'save-btn' => '파일 저장', + ], + 'note' => [ + 'btn' => '메모', + 'title' => '메모 추가', + 'comment' => '코멘트', + 'save-btn' => '메모 저장', + ], + 'activity' => [ + 'btn' => '활동', + 'title' => '활동 추가', + 'title-control' => '제목', + 'description' => '설명', + 'schedule-from' => '시작 일정', + 'schedule-to' => '종료 일정', + 'location' => '위치', + 'call' => '통화', + 'meeting' => '미팅', + 'lunch' => '식사', + 'save-btn' => '활동 저장', + ], + ], + ], + 'tags' => [ + 'create-success' => '태그가 생성되었습니다.', + 'destroy-success' => '태그가 삭제되었습니다.', + ], + ], + 'create' => [ + 'title' => '개인 생성', + 'save-btn' => '개인 저장', + ], + 'edit' => [ + 'title' => '개인 수정', + 'save-btn' => '개인 저장', + ], + ], + 'organizations' => [ + 'index' => [ + 'title' => '조직', + 'create-btn' => '조직 생성', + 'create-success' => '조직이 생성되었습니다.', + 'update-success' => '조직이 수정되었습니다.', + 'delete-success' => '조직이 삭제되었습니다.', + 'delete-failed' => '조직을 삭제할 수 없습니다.', + 'datagrid' => [ + 'delete' => '삭제', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + 'persons-count' => '개인 수', + ], + ], + 'create' => [ + 'title' => '조직 생성', + 'save-btn' => '조직 저장', + ], + 'edit' => [ + 'title' => '조직 수정', + 'save-btn' => '조직 저장', + ], + ], + ], + 'products' => [ + 'index' => [ + 'title' => '제품', + 'create-btn' => '제품 생성', + 'create-success' => '제품이 성공적으로 생성되었습니다.', + 'update-success' => '제품이 성공적으로 수정되었습니다.', + 'delete-success' => '제품이 성공적으로 삭제되었습니다.', + 'delete-failed' => '제품을 삭제할 수 없습니다.', + 'datagrid' => [ + 'allocated' => '할당됨', + 'delete' => '삭제', + 'edit' => '수정', + 'id' => 'ID', + 'in-stock' => '재고 있음', + 'name' => '이름', + 'on-hand' => '보유 수량', + 'tag-name' => '태그 이름', + 'price' => '가격', + 'sku' => 'SKU', + 'view' => '보기', + ], + ], + 'create' => [ + 'save-btn' => '제품 저장', + 'title' => '제품 생성', + 'general' => '일반', + 'price' => '가격', + ], + 'edit' => [ + 'title' => '제품 수정', + 'save-btn' => '제품 저장', + 'general' => '일반', + 'price' => '가격', + ], + 'view' => [ + 'sku' => 'SKU', + 'all' => '전체', + 'notes' => '메모', + 'files' => '파일', + 'inventories' => '재고', + 'change-logs' => '변경 이력', + 'attributes' => [ + 'about-product' => '제품 정보', + ], + 'inventory' => [ + 'warehouse' => '창고', + 'in-stock' => '재고 있음', + 'allocated' => '할당됨', + 'on-hand' => '보유 수량', + 'actions' => '작업', + 'assign' => '할당', + 'add-warehouse' => '창고 추가', + 'location' => '위치', + 'add-more' => '추가', + 'save' => '저장', + ], + ], + ], + 'settings' => [ + 'title' => '설정', + 'groups' => [ + 'index' => [ + 'create-btn' => '그룹 생성', + 'title' => '그룹', + 'create-success' => '그룹이 생성되었습니다.', + 'update-success' => '그룹이 수정되었습니다.', + 'destroy-success' => '그룹이 삭제되었습니다.', + 'delete-failed' => '그룹을 삭제할 수 없습니다.', + 'delete-failed-associated-users' => '이 그룹은 사용자에 의해 사용 중이므로 삭제할 수 없습니다.', + 'datagrid' => [ + 'delete' => '삭제', + 'description' => '설명', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + ], + 'edit' => [ + 'title' => '그룹 수정', + ], + 'create' => [ + 'name' => '이름', + 'title' => '그룹 생성', + 'description' => '설명', + 'save-btn' => '그룹 저장', + ], + ], + ], + 'roles' => [ + 'index' => [ + 'being-used' => '이 역할은 관리자 사용자에게 사용 중이므로 삭제할 수 없습니다.', + 'create-btn' => '역할 생성', + 'create-success' => '역할이 생성되었습니다.', + 'current-role-delete-error' => '현재 사용자에게 할당된 역할은 삭제할 수 없습니다.', + 'current-role-edit-error' => '자신의 계정에 할당된 역할은 편집할 수 없습니다.', + 'delete-failed' => '역할을 삭제할 수 없습니다.', + 'delete-success' => '역할이 삭제되었습니다.', + 'last-delete-error' => '최소 하나의 역할이 필요합니다.', + 'settings' => '설정', + 'title' => '역할', + 'update-success' => '역할이 수정되었습니다.', + 'user-define-error' => '시스템 역할은 삭제할 수 없습니다.', + 'datagrid' => [ + 'all' => '전체', + 'custom' => '사용자 지정', + 'delete' => '삭제', + 'description' => '설명', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + 'permission-type' => '권한 유형', + ], + ], + 'create' => [ + 'access-control' => '접근 제어', + 'all' => '전체', + 'back-btn' => '뒤로', + 'custom' => '사용자 지정', + 'description' => '설명', + 'general' => '일반', + 'name' => '이름', + 'permissions' => '권한', + 'save-btn' => '역할 저장', + 'title' => '역할 생성', + ], + 'edit' => [ + 'access-control' => '접근 제어', + 'all' => '전체', + 'back-btn' => '뒤로', + 'custom' => '사용자 지정', + 'description' => '설명', + 'general' => '일반', + 'name' => '이름', + 'permissions' => '권한', + 'save-btn' => '역할 저장', + 'title' => '역할 수정', + ], + ], + 'types' => [ + 'index' => [ + 'create-btn' => '유형 생성', + 'create-success' => '유형이 생성되었습니다.', + 'delete-failed' => '유형을 삭제할 수 없습니다.', + 'delete-success' => '유형이 삭제되었습니다.', + 'title' => '유형', + 'update-success' => '유형이 수정되었습니다.', + 'datagrid' => [ + 'delete' => '삭제', + 'description' => '설명', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + ], + 'create' => [ + 'name' => '이름', + 'save-btn' => '유형 저장', + 'title' => '유형 생성', + ], + 'edit' => [ + 'title' => '유형 수정', + ], + ], + ], + 'sources' => [ + 'index' => [ + 'title' => '소스', + 'create-btn' => '소스 생성', + 'create-success' => '소스가 생성되었습니다.', + 'delete-failed' => '소스를 삭제할 수 없습니다.', + 'delete-success' => '소스가 삭제되었습니다.', + 'update-success' => '소스가 수정되었습니다.', + 'delete-failed-associated-leads' => '이 소스는 기존 리드와 연결되어 있어 삭제할 수 없습니다. 삭제하기 전에 해당 리드를 분리하거나 수정해 주세요.', + 'datagrid' => [ + 'delete' => '삭제', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + ], + 'create' => [ + 'name' => '이름', + 'save-btn' => '소스 저장', + 'title' => '소스 생성', + ], + 'edit' => [ + 'title' => '소스 수정', + ], + ], + ], + 'workflows' => [ + 'index' => [ + 'title' => '워크플로우', + 'create-btn' => '워크플로우 생성', + 'create-success' => '워크플로우가 생성되었습니다.', + 'update-success' => '워크플로우가 수정되었습니다.', + 'delete-success' => '워크플로우가 삭제되었습니다.', + 'delete-failed' => '워크플로우를 삭제할 수 없습니다.', + 'datagrid' => [ + 'delete' => '삭제', + 'description' => '설명', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + ], + ], + 'helpers' => [ + 'update-related-leads' => '관련 리드 수정', + 'send-email-to-sales-owner' => '영업 담당자에게 이메일 보내기', + 'send-email-to-participants' => '참가자에게 이메일 보내기', + 'add-webhook' => '웹훅 추가', + 'update-lead' => '리드 수정', + 'update-person' => '개인 수정', + 'send-email-to-person' => '개인에게 이메일 보내기', + 'add-tag' => '태그 추가', + 'add-note-as-activity' => '메모를 활동으로 추가', + 'update-quote' => '견적 수정', + ], + 'create' => [ + 'title' => '워크플로우 생성', + 'event' => '이벤트', + 'back-btn' => '뒤로', + 'save-btn' => '워크플로우 저장', + 'name' => '이름', + 'basic-details' => '기본 정보', + 'description' => '설명', + 'actions' => '액션', + 'basic-details-info' => '워크플로우의 기본 정보를 입력하세요.', + 'event-info' => '이벤트가 트리거되면 조건을 확인하고 사전에 정의된 액션을 수행합니다.', + 'conditions' => '조건', + 'conditions-info' => '조건은 특정 상황에서 트리거되어 시나리오를 확인하는 규칙입니다.', + 'actions-info' => '액션은 업무량을 줄여줄 뿐만 아니라 CRM 자동화를 훨씬 더 쉽게 만들어 줍니다.', + 'value' => '값', + 'condition-type' => '조건 유형', + 'all-condition-are-true' => '모든 조건이 참일 때', + 'any-condition-are-true' => '하나 이상의 조건이 참일 때', + 'add-condition' => '조건 추가', + 'add-action' => '액션 추가', + 'no-webhook-found' => '웹훅을 찾을 수 없습니다.', + 'create-webhook' => '새 웹훅 만들기.', + 'yes' => '예', + 'no' => '아니요', + 'email' => '이메일', + 'is-equal-to' => '같음', + 'is-not-equal-to' => '같지 않음', + 'equals-or-greater-than' => '크거나 같음', + 'equals-or-less-than' => '작거나 같음', + 'greater-than' => '초과', + 'less-than' => '미만', + 'type' => '유형', + 'contain' => '포함', + 'contains' => '포함', + 'does-not-contain' => '포함하지 않음', + ], + 'edit' => [ + 'title' => '워크플로우 수정', + 'event' => '이벤트', + 'back-btn' => '뒤로', + 'save-btn' => '워크플로우 저장', + 'name' => '이름', + 'basic-details' => '기본 정보', + 'description' => '설명', + 'actions' => '액션', + 'type' => '유형', + 'basic-details-info' => '워크플로우의 기본 정보를 입력하세요.', + 'event-info' => '이벤트가 트리거되면 조건을 확인하고 사전에 정의된 액션을 수행합니다.', + 'conditions' => '조건', + 'conditions-info' => '조건은 특정 상황에서 트리거되어 시나리오를 확인하는 규칙입니다.', + 'actions-info' => '액션은 업무량을 줄여줄 뿐만 아니라 CRM 자동화를 훨씬 더 쉽게 만들어 줍니다.', + 'value' => '값', + 'condition-type' => '조건 유형', + 'all-condition-are-true' => '모든 조건이 참일 때', + 'any-condition-are-true' => '하나 이상의 조건이 참일 때', + 'add-condition' => '조건 추가', + 'add-action' => '액션 추가', + 'yes' => '예', + 'no' => '아니요', + 'email' => '이메일', + 'is-equal-to' => '같음', + 'is-not-equal-to' => '같지 않음', + 'equals-or-greater-than' => '크거나 같음', + 'equals-or-less-than' => '작거나 같음', + 'greater-than' => '초과', + 'less-than' => '미만', + 'contain' => '포함', + 'contains' => '포함', + 'does-not-contain' => '포함하지 않음', + ], + ], + 'google-contacts' => [ + 'index' => [ + 'title' => 'Google 연락처', + 'not-connected-info' => 'CRM 연락처를 Google 연락처로 내보내려면 Google 계정을 연결하세요.', + 'connected-info' => ':email(으)로 연결됨.', + 'connect-btn' => 'Google 계정 연결', + 'disconnect-btn' => '연결 해제', + 'connect-success' => 'Google 계정이 성공적으로 연결되었습니다.', + 'connect-failed' => 'Google 계정을 연결할 수 없습니다. 다시 시도해 주세요.', + 'not-configured' => 'Google OAuth가 구성되지 않았습니다. 먼저 필요한 환경 변수를 설정하세요.', + 'disconnect-success' => 'Google 계정 연결이 해제되었습니다.', + ], + ], + 'webforms' => [ + 'index' => [ + 'title' => '웹 폼', + 'create-btn' => '웹 폼 생성', + 'create-success' => '웹 폼이 생성되었습니다.', + 'update-success' => '웹 폼이 수정되었습니다.', + 'delete-success' => '웹 폼이 삭제되었습니다.', + 'delete-failed' => '웹 폼을 삭제할 수 없습니다.', + 'datagrid' => [ + 'id' => 'ID', + 'title' => '제목', + 'edit' => '수정', + 'delete' => '삭제', + ], + ], + 'create' => [ + 'title' => '웹 폼 생성', + 'add-attribute-btn' => '속성 추가 버튼', + 'attribute-label-color' => '속성 레이블 색상', + 'attributes' => '속성', + 'attributes-info' => '폼에 사용자 지정 속성을 추가합니다.', + 'background-color' => '배경 색상', + 'create-lead' => '리드 생성', + 'customize-webform' => '웹 폼 사용자 지정', + 'customize-webform-info' => '원하는 요소 색상으로 웹 폼을 사용자 지정하세요.', + 'description' => '설명', + 'display-custom-message' => '사용자 지정 메시지 표시', + 'form-background-color' => '폼 배경 색상', + 'form-submit-btn-color' => '폼 제출 버튼 색상', + 'form-submit-button-color' => '폼 제출 버튼 색상', + 'form-title-color' => '폼 제목 색상', + 'general' => '일반', + 'leads' => '리드', + 'person' => '개인', + 'pipeline' => '파이프라인', + 'save-btn' => '웹 폼 저장', + 'submit-button-label' => '제출 버튼 레이블', + 'submit-success-action' => '제출 성공 동작', + 'redirect-to-url' => 'URL로 리디렉션', + 'choose-value' => '값 선택', + 'select-file' => '파일 선택', + 'select-image' => '이미지 선택', + 'enter-value' => '값 입력', + ], + 'edit' => [ + 'add-attribute-btn' => '속성 추가 버튼', + 'attribute-label-color' => '속성 레이블 색상', + 'attributes' => '속성', + 'attributes-info' => '폼에 사용자 지정 속성을 추가합니다.', + 'background-color' => '배경 색상', + 'choose-value' => '값 선택', + 'code-snippet' => '코드 스니펫', + 'copied' => '복사됨', + 'copy' => '복사', + 'create-lead' => '리드 생성', + 'customize-webform' => '웹 폼 사용자 지정', + 'customize-webform-info' => '원하는 요소 색상으로 웹 폼을 사용자 지정하세요.', + 'description' => '설명', + 'display-custom-message' => '사용자 지정 메시지 표시', + 'embed' => '삽입', + 'enter-value' => '값 입력', + 'form-background-color' => '폼 배경 색상', + 'form-submit-btn-color' => '폼 제출 버튼 색상', + 'form-submit-button-color' => '폼 제출 버튼 색상', + 'form-title-color' => '폼 제목 색상', + 'general' => '일반', + 'leads' => '리드', + 'person' => '개인', + 'pipeline' => '파이프라인', + 'preview' => '미리보기', + 'public-url' => '공개 URL', + 'redirect-to-url' => 'URL로 리디렉션', + 'save-btn' => '웹 폼 저장', + 'select-file' => '파일 선택', + 'select-image' => '이미지 선택', + 'submit-button-label' => '제출 버튼 레이블', + 'submit-success-action' => '제출 성공 동작', + 'title' => '웹 폼 수정', + ], + ], + 'email-template' => [ + 'index' => [ + 'create-btn' => '이메일 템플릿 생성', + 'title' => '이메일 템플릿', + 'create-success' => '이메일 템플릿이 생성되었습니다.', + 'update-success' => '이메일 템플릿이 수정되었습니다.', + 'delete-success' => '이메일 템플릿이 삭제되었습니다.', + 'delete-failed' => '이메일 템플릿을 삭제할 수 없습니다.', + 'datagrid' => [ + 'delete' => '삭제', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + 'subject' => '제목', + ], + ], + 'create' => [ + 'title' => '이메일 템플릿 생성', + 'save-btn' => '이메일 템플릿 저장', + 'email-template' => '이메일 템플릿', + 'subject' => '제목', + 'content' => '콘텐츠', + 'subject-placeholders' => '제목 플레이스홀더', + 'general' => '일반', + 'name' => '이름', + ], + 'edit' => [ + 'title' => '이메일 템플릿 수정', + 'save-btn' => '이메일 템플릿 저장', + 'email-template' => '이메일 템플릿', + 'subject' => '제목', + 'content' => '콘텐츠', + 'subject-placeholders' => '제목 플레이스홀더', + 'general' => '일반', + 'name' => '이름', + ], + ], + 'marketing' => [ + 'events' => [ + 'index' => [ + 'create-btn' => '이벤트 생성', + 'title' => '이벤트', + 'create-success' => '이벤트가 생성되었습니다.', + 'update-success' => '이벤트가 수정되었습니다.', + 'delete-success' => '이벤트가 삭제되었습니다.', + 'delete-failed' => '이벤트를 삭제할 수 없습니다.', + 'mass-delete-success' => '이벤트가 삭제되었습니다', + 'delete-failed-associated-campaigns' => '기존 캠페인과 연결되어 있어 이벤트를 삭제할 수 없습니다. 삭제하기 전에 해당 캠페인의 연결을 해제하거나 수정해 주세요.', + 'datagrid' => [ + 'delete' => '삭제', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + 'description' => '설명', + 'date' => '날짜', + ], + 'create' => [ + 'title' => '이벤트 생성', + 'name' => '이름', + 'date' => '날짜', + 'description' => '설명', + 'save-btn' => '이벤트 저장', + ], + 'edit' => [ + 'title' => '이벤트 수정', + ], + ], + ], + 'campaigns' => [ + 'index' => [ + 'create-btn' => '캠페인 생성', + 'title' => '캠페인', + 'create-success' => '캠페인이 생성되었습니다.', + 'update-success' => '캠페인이 수정되었습니다.', + 'delete-success' => '캠페인이 삭제되었습니다.', + 'delete-failed' => '캠페인을 삭제할 수 없습니다.', + 'mass-delete-success' => '캠페인이 삭제되었습니다.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => '이름', + 'subject' => '제목', + 'status' => '상태', + 'active' => '활성', + 'inactive' => '비활성', + 'edit' => '수정', + 'delete' => '삭제', + ], + 'create' => [ + 'title' => '캠페인 생성', + 'name' => '이름', + 'type' => '유형', + 'subject' => '제목', + 'event' => '이벤트', + 'email-template' => '이메일 템플릿', + 'status' => '상태', + ], + 'edit' => [ + 'title' => '캠페인 수정', + ], + ], + ], + ], + 'tags' => [ + 'index' => [ + 'create-btn' => '태그 생성', + 'title' => '태그', + 'create-success' => '태그가 생성되었습니다.', + 'update-success' => '태그가 수정되었습니다.', + 'delete-success' => '태그가 삭제되었습니다.', + 'delete-failed' => '태그를 삭제할 수 없습니다.', + 'datagrid' => [ + 'delete' => '삭제', + 'edit' => '수정', + 'id' => 'ID', + 'name' => '이름', + 'users' => '사용자', + 'created-at' => '생성일', + ], + 'create' => [ + 'name' => '이름', + 'save-btn' => '태그 저장', + 'title' => '태그 생성', + 'color' => '색상', + ], + 'edit' => [ + 'title' => '태그 수정', + ], + ], + ], + 'users' => [ + 'index' => [ + 'create-btn' => '사용자 생성', + 'create-success' => '사용자가 생성되었습니다.', + 'delete-failed' => '사용자를 삭제할 수 없습니다.', + 'delete-success' => '사용자가 삭제되었습니다.', + 'last-delete-error' => '사용자가 최소 한 명 이상 필요합니다.', + 'mass-delete-failed' => '사용자를 삭제할 수 없습니다.', + 'mass-delete-success' => '사용자가 삭제되었습니다.', + 'mass-update-failed' => '사용자를 수정할 수 없습니다.', + 'mass-update-success' => '사용자가 수정되었습니다.', + 'title' => '사용자', + 'update-success' => '사용자가 수정되었습니다.', + 'user-define-error' => '시스템 사용자는 삭제할 수 없습니다.', + 'active' => '활성', + 'inactive' => '비활성', + 'datagrid' => [ + 'active' => '활성', + 'created-at' => '생성일', + 'delete' => '삭제', + 'edit' => '수정', + 'email' => '이메일', + 'group' => '연결된 그룹', + 'id' => 'ID', + 'inactive' => '비활성', + 'name' => '이름', + 'no-group' => '그룹 없음', + 'status' => '상태', + 'update-status' => '상태 수정', + 'users' => '사용자', + ], + 'create' => [ + 'confirm-password' => '비밀번호 확인', + 'email' => '이메일', + 'general' => '일반', + 'global' => '전체', + 'group' => '그룹', + 'individual' => '개별', + 'name' => '이름', + 'password' => '비밀번호', + 'permission' => '권한', + 'role' => '역할', + 'save-btn' => '사용자 저장', + 'status' => '상태', + 'title' => '사용자 생성', + 'view-permission' => '조회 권한', + 'select-at-lest-one-group' => '그룹을 하나 이상 선택하세요', + ], + 'edit' => [ + 'title' => '사용자 수정', + ], + ], + ], + 'pipelines' => [ + 'index' => [ + 'title' => '파이프라인', + 'create-btn' => '파이프라인 생성', + 'create-success' => '파이프라인이 생성되었습니다.', + 'update-success' => '파이프라인이 수정되었습니다.', + 'default-required' => '기본 파이프라인이 최소 하나 이상 필요합니다.', + 'delete-success' => '파이프라인이 삭제되었습니다.', + 'delete-failed' => '파이프라인을 삭제할 수 없습니다.', + 'default-delete-error' => '기본 파이프라인은 삭제할 수 없습니다.', + 'datagrid' => [ + 'delete' => '삭제', + 'edit' => '수정', + 'id' => 'ID', + 'is-default' => '기본 여부', + 'name' => '이름', + 'no' => '아니요', + 'rotten-days' => '정체 기간(일)', + 'yes' => '예', + ], + ], + 'create' => [ + 'title' => '파이프라인 생성', + 'save-btn' => '파이프라인 저장', + 'name' => '이름', + 'rotten-days' => '정체 기간(일)', + 'mark-as-default' => '기본값으로 지정', + 'general' => '일반', + 'probability' => '확률(%)', + 'new-stage' => '신규', + 'won-stage' => '성사', + 'lost-stage' => '실패', + 'stage-btn' => '단계 추가', + 'stages' => '단계', + 'duplicate-name' => '이름 필드는 중복될 수 없습니다', + 'delete-stage' => '단계 삭제', + 'add-new-stages' => '새 단계 추가', + 'add-stage-info' => '파이프라인에 새 단계를 추가하세요', + 'newly-added' => '새로 추가됨', + 'stage-delete-success' => '단계가 삭제되었습니다', + ], + 'edit' => [ + 'title' => '파이프라인 수정', + 'save-btn' => '파이프라인 저장', + 'name' => '이름', + 'rotten-days' => '정체 기간(일)', + 'mark-as-default' => '기본값으로 지정', + 'general' => '일반', + 'probability' => '확률(%)', + 'new-stage' => '신규', + 'won-stage' => '성사', + 'lost-stage' => '실패', + 'stage-btn' => '단계 추가', + 'stages' => '단계', + 'duplicate-name' => '이름 필드는 중복될 수 없습니다', + 'delete-stage' => '단계 삭제', + 'add-new-stages' => '새 단계 추가', + 'add-stage-info' => '파이프라인에 새 단계를 추가하세요', + 'stage-delete-success' => '단계가 삭제되었습니다', + ], + ], + 'webhooks' => [ + 'index' => [ + 'title' => '웹훅', + 'create-btn' => '웹훅 생성', + 'create-success' => '웹훅이 생성되었습니다.', + 'update-success' => '웹훅이 수정되었습니다.', + 'delete-success' => '웹훅이 삭제되었습니다.', + 'delete-failed' => '웹훅을 삭제할 수 없습니다.', + 'invalid-endpoint' => '웹훅 엔드포인트 URL은 공개된 HTTP 또는 HTTPS 주소여야 합니다.', + 'datagrid' => [ + 'id' => 'ID', + 'delete' => '삭제', + 'edit' => '수정', + 'name' => '이름', + 'entity-type' => '엔티티 유형', + 'end-point' => '엔드포인트', + ], + ], + 'create' => [ + 'title' => '웹훅 생성', + 'save-btn' => '웹훅 저장', + 'info' => '웹훅의 세부 정보를 입력하세요', + 'url-and-parameters' => 'URL 및 매개변수', + 'method' => '메서드', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'URL 엔드포인트', + 'parameters' => '매개변수', + 'add-new-parameter' => '새 매개변수 추가', + 'url-preview' => 'URL 미리보기:', + 'headers' => '헤더', + 'add-new-header' => '새 헤더 추가', + 'body' => '본문', + 'default' => '기본값', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => '키와 값', + 'add-new-payload' => '새 페이로드 추가', + 'raw' => 'Raw', + 'general' => '일반', + 'name' => '이름', + 'entity-type' => '엔티티 유형', + 'insert-placeholder' => '자리표시자 삽입', + 'description' => '설명', + 'json' => 'Json', + 'text' => '텍스트', + ], + 'edit' => [ + 'title' => '웹훅 수정', + 'edit-btn' => '웹훅 저장', + 'save-btn' => '웹훅 저장', + 'info' => '웹훅의 세부 정보를 입력하세요', + 'url-and-parameters' => 'URL 및 매개변수', + 'method' => '메서드', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'URL 엔드포인트', + 'parameters' => '매개변수', + 'add-new-parameter' => '새 매개변수 추가', + 'url-preview' => 'URL 미리보기:', + 'headers' => '헤더', + 'add-new-header' => '새 헤더 추가', + 'body' => '본문', + 'default' => '기본값', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => '키와 값', + 'add-new-payload' => '새 페이로드 추가', + 'raw' => 'Raw', + 'general' => '일반', + 'name' => '이름', + 'entity-type' => '엔티티 유형', + 'insert-placeholder' => '자리표시자 삽입', + 'description' => '설명', + 'json' => 'Json', + 'text' => '텍스트', + ], + ], + 'warehouses' => [ + 'index' => [ + 'title' => '창고', + 'create-btn' => '창고 생성', + 'create-success' => '창고가 생성되었습니다.', + 'name-exists' => '이미 존재하는 창고 이름입니다.', + 'update-success' => '창고가 수정되었습니다.', + 'delete-success' => '창고가 삭제되었습니다.', + 'delete-failed' => '창고를 삭제할 수 없습니다.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => '이름', + 'contact-name' => '담당자 이름', + 'delete' => '삭제', + 'edit' => '수정', + 'view' => '보기', + 'created-at' => '생성일', + 'products' => '제품', + 'contact-emails' => '담당자 이메일', + 'contact-numbers' => '담당자 연락처', + ], + ], + 'create' => [ + 'title' => '창고 생성', + 'save-btn' => '창고 저장', + 'contact-info' => '연락처 정보', + ], + 'edit' => [ + 'title' => '창고 수정', + 'save-btn' => '창고 저장', + 'contact-info' => '연락처 정보', + ], + 'view' => [ + 'all' => '전체', + 'notes' => '메모', + 'files' => '파일', + 'location' => '위치', + 'change-logs' => '변경 이력', + 'locations' => [ + 'action' => '작업', + 'add-location' => '위치 추가', + 'create-success' => '위치가 생성되었습니다.', + 'delete' => '삭제', + 'delete-failed' => '위치를 삭제할 수 없습니다.', + 'delete-success' => '위치가 삭제되었습니다.', + 'name' => '이름', + 'save-btn' => '저장', + ], + 'general-information' => [ + 'title' => '일반 정보', + ], + 'contact-information' => [ + 'title' => '연락처 정보', + ], + ], + ], + 'attributes' => [ + 'index' => [ + 'title' => '속성', + 'create-btn' => '속성 생성', + 'create-success' => '속성이 생성되었습니다.', + 'update-success' => '속성이 수정되었습니다.', + 'delete-success' => '속성이 삭제되었습니다.', + 'delete-failed' => '속성을 삭제할 수 없습니다.', + 'user-define-error' => '시스템 속성은 삭제할 수 없습니다.', + 'mass-delete-failed' => '시스템 속성은 삭제할 수 없습니다.', + 'datagrid' => [ + 'yes' => '예', + 'no' => '아니요', + 'id' => 'ID', + 'code' => '코드', + 'name' => '이름', + 'entity-type' => '엔티티 유형', + 'type' => '유형', + 'is-default' => '기본값 여부', + 'edit' => '수정', + 'delete' => '삭제', + 'quick-add' => '빠른 추가', + 'entity-types' => [ + 'leads' => '리드', + 'organizations' => '조직', + 'persons' => '개인', + 'products' => '제품', + 'quotes' => '견적서', + 'warehouses' => '창고', + ], + 'types' => [ + 'text' => '텍스트', + 'textarea' => '텍스트 영역', + 'price' => '가격', + 'boolean' => '불리언', + 'select' => '선택', + 'multiselect' => '다중 선택', + 'checkbox' => '체크박스', + 'email' => '이메일', + 'address' => '주소', + 'phone' => '전화번호', + 'lookup' => '조회', + 'datetime' => '날짜시간', + 'date' => '날짜', + 'image' => '이미지', + 'file' => '파일', + ], + ], + ], + 'create' => [ + 'title' => '속성 생성', + 'save-btn' => '속성 저장', + 'code' => '코드', + 'name' => '이름', + 'quick_add' => '빠른 추가', + 'entity-type' => '엔티티 유형', + 'type' => '유형', + 'validations' => '유효성 검사', + 'is-required' => '필수 여부', + 'input-validation' => '입력값 유효성 검사', + 'is-unique' => '고유값 여부', + 'labels' => '레이블', + 'general' => '일반', + 'none' => '없음', + 'numeric' => '숫자', + 'decimal' => '소수', + 'url' => 'URL', + 'options' => '옵션', + 'option-type' => '옵션 유형', + 'lookup-type' => '조회 유형', + 'add-option' => '옵션 추가', + 'save-option' => '옵션 저장', + 'option-name' => '옵션 이름', + 'add-attribute-options' => '속성 옵션 추가', + 'text' => '텍스트', + 'textarea' => '텍스트 영역', + 'price' => '가격', + 'boolean' => '불리언', + 'select' => '선택', + 'multiselect' => '다중 선택', + 'email' => '이메일', + 'address' => '주소', + 'phone' => '전화번호', + 'datetime' => '날짜시간', + 'date' => '날짜', + 'image' => '이미지', + 'file' => '파일', + 'lookup' => '조회', + 'entity_type' => '엔티티 유형', + 'checkbox' => '체크박스', + 'is_required' => '필수 여부', + 'is_unique' => '고유값 여부', + 'actions' => '작업', + ], + 'edit' => [ + 'actions' => '작업', + 'add-attribute-options' => '속성 옵션 추가', + 'add-option' => '옵션 추가', + 'address' => '주소', + 'boolean' => '불리언', + 'checkbox' => '체크박스', + 'code' => '코드', + 'date' => '날짜', + 'datetime' => '날짜시간', + 'decimal' => '소수', + 'email' => '이메일', + 'entity-type' => '엔티티 유형', + 'entity_type' => '엔티티 유형', + 'file' => '파일', + 'general' => '일반', + 'image' => '이미지', + 'input-validation' => '입력값 유효성 검사', + 'is-required' => '필수 여부', + 'is-unique' => '고유값 여부', + 'is_required' => '필수 여부', + 'is_unique' => '고유값 여부', + 'labels' => '레이블', + 'lookup' => '조회', + 'lookup-type' => '조회 유형', + 'multiselect' => '다중 선택', + 'name' => '이름', + 'quick_add' => '빠른 추가', + 'none' => '없음', + 'numeric' => '숫자', + 'option-deleted' => '속성 옵션이 삭제되었습니다', + 'option-name' => '옵션 이름', + 'option-type' => '옵션 유형', + 'options' => '옵션', + 'phone' => '전화번호', + 'price' => '가격', + 'save-btn' => '속성 저장', + 'save-option' => '옵션 저장', + 'select' => '선택', + 'text' => '텍스트', + 'textarea' => '텍스트 영역', + 'title' => '속성 수정', + 'type' => '유형', + 'url' => 'URL', + 'validations' => '유효성 검사', + ], + ], + 'data-transfer' => [ + 'importers' => [ + 'persons' => [ + 'title' => '담당자', + 'validation' => [ + 'errors' => [ + 'duplicate-email' => '이메일 : \'%s\'가 가져오기 파일에 두 번 이상 존재합니다.', + 'duplicate-phone' => '전화번호 : \'%s\'가 가져오기 파일에 두 번 이상 존재합니다.', + 'email-not-found' => '이메일 : \'%s\'를 시스템에서 찾을 수 없습니다.', + 'invalid-date' => '날짜 : \'%s\'은(는) 유효한 날짜가 아닙니다.', + ], + ], + ], + 'products' => [ + 'title' => '상품', + 'validation' => [ + 'errors' => [ + 'sku-not-found' => '지정한 SKU를 가진 상품을 찾을 수 없습니다', + ], + ], + ], + 'leads' => [ + 'title' => '리드', + 'validation' => [ + 'errors' => [ + 'id-not-found' => 'ID : \'%s\'를 시스템에서 찾을 수 없습니다.', + ], + ], + ], + ], + 'validation' => [ + 'errors' => [ + 'column-empty-headers' => '열 번호 "%s"에 빈 헤더가 있습니다.', + 'column-name-invalid' => '잘못된 열 이름입니다: "%s".', + 'column-not-found' => '필수 열을 찾을 수 없습니다: %s.', + 'column-numbers' => '열의 개수가 헤더의 행 개수와 일치하지 않습니다.', + 'invalid-attribute' => '헤더에 잘못된 속성이 포함되어 있습니다: "%s".', + 'system' => '예기치 않은 시스템 오류가 발생했습니다.', + 'wrong-quotes' => '직선 따옴표 대신 곡선 따옴표가 사용되었습니다.', + 'already-exists' => ':attribute이(가) 이미 존재합니다.', + ], + ], + 'imports' => [ + 'create' => [ + 'action' => '작업', + 'allowed-errors' => '허용 오류 수', + 'back-btn' => '뒤로', + 'create-update' => '생성/수정', + 'delete' => '삭제', + 'download-sample' => '샘플 다운로드', + 'field-separator' => '필드 구분자', + 'file' => '파일', + 'general' => '일반', + 'images-directory' => '이미지 디렉터리 경로', + 'process-in-queue' => '큐에서 처리', + 'results' => '결과', + 'save-btn' => '가져오기 저장', + 'settings' => '설정', + 'skip-errors' => '오류 건너뛰기', + 'stop-on-errors' => '오류 시 중지', + 'title' => '가져오기 생성', + 'type' => '유형', + 'validation-strategy' => '유효성 검사 전략', + ], + 'edit' => [ + 'action' => '작업', + 'allowed-errors' => '허용 오류 수', + 'back-btn' => '뒤로', + 'create-update' => '생성/수정', + 'delete' => '삭제', + 'download-sample' => '샘플 다운로드', + 'field-separator' => '필드 구분자', + 'file' => '파일', + 'general' => '일반', + 'images-directory' => '이미지 디렉터리 경로', + 'process-in-queue' => '큐에서 처리', + 'results' => '결과', + 'save-btn' => '가져오기 저장', + 'settings' => '설정', + 'skip-errors' => '오류 건너뛰기', + 'stop-on-errors' => '오류 시 중지', + 'title' => '가져오기 수정', + 'type' => '유형', + 'validation-strategy' => '유효성 검사 전략', + ], + 'index' => [ + 'button-title' => '가져오기 생성', + 'title' => '가져오기', + 'datagrid' => [ + 'actions' => '작업', + 'completed-at' => '완료 시각', + 'created' => '생성됨', + 'delete' => '삭제', + 'deleted' => '삭제됨', + 'edit' => '수정', + 'error-file' => '오류 파일', + 'id' => 'ID', + 'started-at' => '시작 시각', + 'state' => '상태', + 'summary' => '요약', + 'type' => '유형', + 'updated' => '수정됨', + 'uploaded-file' => '업로드된 파일', + ], + ], + 'import' => [ + 'back-btn' => '뒤로', + 'completed-batches' => '완료된 배치 수:', + 'download-error-report' => '전체 보고서 다운로드', + 'edit-btn' => '수정', + 'imported-info' => '축하합니다! 가져오기가 성공적으로 완료되었습니다.', + 'importing-info' => '가져오기 처리 중', + 'importing-queue-hint' => '이 가져오기가 0%에 머무르면 큐 워커가 실행되지 않았을 수 있습니다. 관리자에게 \'php artisan queue:work\' 실행을 요청하거나 \'큐에서 처리\'를 끄고 다시 가져오세요.', + 'indexing-info' => '리소스 인덱싱(가격, 재고, Elastic Search) 진행 중', + 'linking-info' => '리소스 연결 진행 중', + 'progress' => '진행률:', + 'title' => '가져오기', + 'total-batches' => '전체 배치 수:', + 'total-created' => '생성된 전체 레코드 수:', + 'total-deleted' => '삭제된 전체 레코드 수:', + 'total-errors' => '전체 오류 수:', + 'total-invalid-rows' => '전체 유효하지 않은 행 수:', + 'total-rows-processed' => '처리된 전체 행 수:', + 'total-updated' => '수정된 전체 레코드 수:', + 'validate' => '유효성 검사', + 'validate-info' => '가져오기 내용을 확인하려면 데이터 유효성 검사를 클릭하세요.', + 'validating-info' => '데이터 읽기 및 유효성 검사를 시작했습니다.', + 'validation-failed-info' => '가져오기가 유효하지 않습니다. 다음 오류를 수정한 후 다시 시도해 주세요.', + 'validation-success-info' => '가져오기 내용이 유효합니다. 가져오기 프로세스를 시작하려면 가져오기를 클릭하세요.', + ], + 'create-success' => '가져오기가 생성되었습니다.', + 'delete-failed' => '가져오기 삭제 중 예기치 않은 오류가 발생했습니다.', + 'delete-success' => '가져오기가 삭제되었습니다.', + 'not-valid' => '가져오기가 유효하지 않습니다', + 'nothing-to-import' => '가져올 리소스가 없습니다.', + 'queue-worker-not-running' => '큐 워커가 실행되고 있지 않습니다. 이 가져오기를 처리하려면 php artisan queue:work 를 실행하거나, 큐에서 처리 옵션을 끄고 다시 시도하세요.', + 'setup-queue-error' => '가져오기 프로세스를 시작하려면 큐 드라이버를 "database" 또는 "redis"로 변경해 주세요.', + 'update-success' => '가져오기가 수정되었습니다.', + ], + ], + ], + 'activities' => [ + 'index' => [ + 'title' => '활동', + 'datagrid' => [ + 'comment' => '댓글', + 'created_at' => '생성일', + 'created_by' => '생성자', + 'edit' => '수정', + 'id' => 'ID', + 'done' => '완료 여부', + 'not-done' => '미완료', + 'lead' => '리드', + 'mass-delete' => '일괄 삭제', + 'mass-update' => '일괄 수정', + 'schedule-from' => '일정 시작', + 'schedule-to' => '일정 종료', + 'schedule_from' => '일정 시작', + 'schedule_to' => '일정 종료', + 'title' => '제목', + 'is_done' => '완료 여부', + 'type' => '유형', + 'update' => '수정', + 'call' => '전화', + 'meeting' => '회의', + 'lunch' => '점심 식사', + ], + ], + 'edit' => [ + 'title' => '활동 수정', + 'back-btn' => '뒤로', + 'save-btn' => '활동 저장', + 'type' => '활동 유형', + 'call' => '전화', + 'meeting' => '회의', + 'lunch' => '점심 식사', + 'schedule_to' => '일정 종료', + 'schedule_from' => '일정 시작', + 'location' => '위치', + 'comment' => '댓글', + 'lead' => '리드', + 'participants' => '참가자', + 'general' => '일반', + 'persons' => '개인', + 'no-result-found' => '레코드를 찾을 수 없습니다.', + 'users' => '사용자', + ], + 'updated' => ':attribute 수정됨', + 'created' => '생성됨', + 'duration-overlapping' => '참가자에게 이 시간에 예정된 다른 회의가 있습니다. 계속하시겠습니까?', + 'create-success' => '활동이 성공적으로 생성되었습니다.', + 'update-success' => '활동이 성공적으로 수정되었습니다.', + 'overlapping-error' => '참가자에게 이 시간에 예정된 다른 회의가 있습니다.', + 'destroy-success' => '활동이 성공적으로 삭제되었습니다.', + 'delete-failed' => '활동을 삭제할 수 없습니다.', + 'mass-update-success' => '활동이 성공적으로 수정되었습니다.', + 'mass-destroy-success' => '활동이 성공적으로 삭제되었습니다.', + 'mass-delete-failed' => '활동을 삭제할 수 없습니다.', + ], + 'mail' => [ + 'index' => [ + 'compose' => '작성', + 'draft' => '임시보관함', + 'inbox' => '받은편지함', + 'outbox' => '발신함', + 'sent' => '보낸편지함', + 'trash' => '휴지통', + 'compose-mail-btn' => '메일 작성', + 'btn' => '메일', + 'mail' => [ + 'title' => '메일 작성', + 'to' => '받는사람', + 'enter-emails' => 'Enter 키를 눌러 이메일을 추가하세요', + 'cc' => '참조', + 'bcc' => '숨은참조', + 'subject' => '제목', + 'send-btn' => '보내기', + 'message' => '메시지', + 'draft' => '임시보관함', + ], + 'datagrid' => [ + 'id' => 'ID', + 'from' => '보낸사람', + 'to' => '받는사람', + 'subject' => '제목', + 'tags' => '태그', + 'content' => '내용', + 'attachments' => '첨부파일', + 'date' => '날짜', + 'move-to-inbox' => '받은편지함으로 이동됨', + 'move-to-trash' => '휴지통으로 이동됨', + 'edit' => '수정', + 'view' => '보기', + 'delete' => '삭제', + ], + ], + 'create-success' => '이메일이 성공적으로 발송되었습니다.', + 'update-success' => '이메일이 성공적으로 수정되었습니다.', + 'mass-update-success' => '이메일이 성공적으로 수정되었습니다.', + 'delete-success' => '이메일이 성공적으로 삭제되었습니다.', + 'delete-failed' => '이메일을 삭제할 수 없습니다.', + 'invalid-route' => '메일에 대한 잘못된 경로입니다.', + 'unauthorized' => '이 작업은 권한이 없습니다.', + 'view' => [ + 'title' => '메일', + 'subject' => ':subject', + 'link-mail' => '메일 연결', + 'to' => '받는사람', + 'cc' => '참조', + 'bcc' => '숨은참조', + 'reply' => '답장', + 'reply-all' => '전체 답장', + 'forward' => '전달', + 'delete' => '삭제', + 'enter-mails' => '이메일 ID를 입력하세요', + 'rotten-days' => '리드가 :days일 동안 방치되었습니다', + 'search-an-existing-lead' => '기존 리드 검색', + 'search-an-existing-contact' => '기존 연락처 검색', + 'message' => '메시지', + 'add-attachments' => '첨부파일 추가', + 'discard' => '삭제', + 'send' => '보내기', + 'no-result-found' => '검색 결과가 없습니다', + 'add-new-contact' => '새 연락처 추가', + 'description' => '설명', + 'search' => '검색...', + 'add-new-lead' => '새 리드 추가', + 'create-new-contact' => '새 연락처 생성', + 'save-contact' => '연락처 저장', + 'create-lead' => '리드 생성', + 'linked-contact' => '연결된 연락처', + 'link-to-contact' => '연락처에 연결', + 'link-to-lead' => '리드에 연결', + 'linked-lead' => '연결된 리드', + 'lead-details' => '리드 상세정보', + 'contact-person' => '담당자', + 'product' => '제품', + 'tags' => [ + 'create-success' => '태그가 성공적으로 생성되었습니다.', + 'destroy-success' => '태그가 성공적으로 삭제되었습니다.', + ], + ], + ], + 'common' => [ + 'custom-attributes' => [ + 'add-more' => '추가', + 'address' => '주소', + 'city' => '도시', + 'contact' => '연락처 번호', + 'country' => '국가', + 'email' => '이메일', + 'home' => '자택', + 'postcode' => '우편번호', + 'save' => '저장', + 'select' => '선택', + 'select-country' => '국가 선택', + 'select-state' => '주/도 선택', + 'state' => '주/도', + 'update-contact-title' => '연락처 번호 업데이트', + 'update-emails-title' => '연락처 이메일 업데이트', + 'work' => '직장', + ], + ], + 'leads' => [ + 'create-success' => '리드가 성공적으로 생성되었습니다.', + 'update-success' => '리드가 성공적으로 수정되었습니다.', + 'update-failed' => '리드를 삭제할 수 없습니다.', + 'destroy-success' => '리드가 성공적으로 삭제되었습니다.', + 'destroy-failed' => '리드를 삭제할 수 없습니다.', + 'file' => [ + 'data-not-found' => '데이터를 찾을 수 없습니다.', + 'empty-content' => 'PDF 내용이 비어 있거나 추출할 수 없습니다.', + 'failed-extract' => '파일에서 텍스트를 추출하는 데 실패했습니다.', + 'insufficient-info' => '데이터가 부족하여 현재 요청을 처리할 수 없습니다.', + 'invalid-base64' => '잘못된 base64 형식입니다.', + 'invalid-format' => '잘못된 JSON 형식입니다.', + 'invalid-response' => '잘못된 AI 응답 형식입니다.', + 'missing-api-key' => 'API 키 또는 모델 설정이 누락되었습니다.', + 'not-found' => '파일을 찾을 수 없습니다.', + 'recursive-call' => '재귀 호출이 감지되었습니다.', + 'text-generation-failed' => '텍스트 추출에 실패했습니다. 파일이 비어 있거나 읽을 수 없는 상태일 수 있습니다.', + ], + 'index' => [ + 'title' => '리드', + 'create-btn' => '리드 생성', + 'datagrid' => [ + 'id' => 'ID', + 'sales-person' => '영업 담당자', + 'subject' => '제목', + 'source' => '소스', + 'lead-value' => '예상 리드 금액', + 'lead-type' => '리드 유형', + 'tag-name' => '태그 이름', + 'contact-person' => '담당자', + 'stage' => '단계', + 'rotten-lead' => '방치된 리드', + 'date-to' => '종료일', + 'created-at' => '생성일', + 'no' => '아니요', + 'yes' => '예', + 'delete' => '삭제', + 'mass-delete' => '일괄 삭제', + 'mass-update' => '일괄 수정', + ], + 'kanban' => [ + 'rotten-days' => '리드가 :days일 동안 방치되었습니다', + 'empty-list' => '리드 목록이 비어 있습니다', + 'empty-list-description' => '목표를 관리하려면 리드를 생성하세요.', + 'create-lead-btn' => '리드 생성', + 'columns' => [ + 'contact-person' => '담당자', + 'id' => 'ID', + 'lead-type' => '리드 유형', + 'lead-value' => '예상 리드 금액', + 'sales-person' => '영업 담당자', + 'source' => '소스', + 'title' => '제목', + 'tags' => '태그', + 'expected-close-date' => '예상 종료일', + 'date-to' => '종료 날짜', + 'created-at' => '생성일', + ], + 'toolbar' => [ + 'search' => [ + 'title' => '제목으로 검색', + ], + 'filters' => [ + 'apply-filters' => '필터 적용', + 'clear-all' => '전체 지우기', + 'filter' => '필터', + 'filters' => '필터', + 'from' => '시작', + 'select' => '선택', + 'to' => '종료', + ], + 'card-settings' => [ + 'title' => '카드 설정', + 'contact-person' => '담당자', + 'rotten-indicator' => '정체 표시', + 'sales-person' => '영업 담당자', + 'estimated-lead-value' => '예상 리드 금액', + 'source' => '소스', + 'lead-type' => '리드 유형', + 'tags' => '태그', + ], + ], + 'stages' => [ + 'won' => '성사', + 'lost' => '실패', + 'need-more-info' => '추가 정보 필요', + 'closed-at' => '종료일', + 'won-value' => '성사 금액', + 'lost-reason' => '실패 사유', + 'save-btn' => '저장', + ], + ], + 'view-switcher' => [ + 'all-pipelines' => '전체 파이프라인', + 'create-new-pipeline' => '새 파이프라인 생성', + ], + 'upload' => [ + 'create-lead' => 'AI로 리드 생성', + 'file' => '파일 업로드', + 'file-info' => 'pdf, bmp, jpg, jpeg, png 형식의 파일만 허용됩니다.', + 'file-required' => '계속하려면 유효한 파일을 하나 이상 선택해 주세요.', + 'save-btn' => '저장', + 'upload-file' => '파일 업로드', + ], + ], + 'create' => [ + 'title' => '리드 생성', + 'save-btn' => '저장', + 'details' => '상세정보', + 'details-info' => '리드의 기본 정보를 입력하세요', + 'contact-person' => '담당자', + 'contact-info' => '담당자에 대한 정보', + 'products' => '제품', + 'products-info' => '제품에 대한 정보', + ], + 'edit' => [ + 'title' => '리드 수정', + 'save-btn' => '저장', + 'details' => '상세정보', + 'details-info' => '리드의 기본 정보를 입력하세요', + 'contact-person' => '담당자', + 'contact-info' => '담당자에 대한 정보', + 'products' => '제품', + 'products-info' => '제품에 대한 정보', + ], + 'common' => [ + 'contact' => [ + 'name' => '이름', + 'email' => '이메일', + 'name-search-placeholder' => '이름, 이메일, 번호로 검색', + 'contact-number' => '연락처 번호', + 'organization' => '조직', + ], + 'products' => [ + 'product-name' => '제품명', + 'quantity' => '수량', + 'price' => '가격', + 'amount' => '금액', + 'action' => '작업', + 'add-more' => '추가', + 'total' => '합계', + ], + ], + 'view' => [ + 'title' => '리드: :title', + 'rotten-days' => ':days일', + 'tabs' => [ + 'description' => '설명', + 'products' => '제품', + 'quotes' => '견적서', + ], + 'attributes' => [ + 'title' => '리드 정보', + ], + 'quotes' => [ + 'id' => 'ID', + 'subject' => '제목', + 'expired-at' => '만료일', + 'sub-total' => '소계', + 'discount' => '할인', + 'tax' => '세금', + 'adjustment' => '조정', + 'grand-total' => '총계', + 'delete' => '삭제', + 'edit' => '수정', + 'download' => '다운로드', + 'destroy-success' => '견적서가 성공적으로 삭제되었습니다.', + 'empty-title' => '견적서가 없습니다', + 'empty-info' => '이 리드에 대한 견적서가 없습니다', + 'add-btn' => '견적서 추가', + 'mail' => '메일', + 'quote-not-found' => '이 리드에 대한 견적서를 찾을 수 없습니다.', + 'person-email-unavailable' => '리드 담당자의 이메일을 사용할 수 없습니다.', + 'mail-subject' => '견적서: :subject', + 'mail-body' => '첨부된 견적서 PDF를 확인해 주세요. 시스템에서 생성된 메일입니다.', + 'mail-send-failed' => '견적서 이메일을 보낼 수 없습니다.', + ], + 'products' => [ + 'product-name' => '제품명', + 'quantity' => '수량', + 'price' => '가격', + 'amount' => '금액', + 'action' => '작업', + 'add-more' => '추가', + 'total' => '합계', + 'empty-title' => '제품이 없습니다', + 'empty-info' => '이 리드에 대한 제품이 없습니다', + 'add-product' => '제품 추가', + ], + 'persons' => [ + 'title' => '담당자 정보', + 'job-title' => ':organization의 :job_title', + 'no-person' => '이 리드에 연결된 담당자가 없습니다.', + 'attach-btn' => '담당자 연결', + 'change-btn' => '변경', + 'change-title' => '담당자 변경', + 'save-btn' => '저장', + 'cancel-btn' => '취소', + ], + 'stages' => [ + 'won' => '성사', + 'lost' => '실패', + 'need-more-info' => '추가 정보 필요', + 'closed-at' => '종료일', + 'won-value' => '성사 금액', + 'lost-reason' => '실패 사유', + 'save-btn' => '저장', + ], + 'tags' => [ + 'create-success' => '태그가 성공적으로 생성되었습니다.', + 'destroy-success' => '태그가 성공적으로 삭제되었습니다.', + ], + ], + ], + 'configuration' => [ + 'index' => [ + 'back' => '뒤로', + 'delete' => '삭제', + 'save-btn' => '설정 저장', + 'save-success' => '설정이 성공적으로 저장되었습니다.', + 'search' => '검색', + 'select-country' => '국가 선택', + 'select-state' => '주/도 선택', + 'title' => '구성', + 'general' => [ + 'title' => '일반', + 'info' => '일반 설정', + 'general' => [ + 'title' => '일반', + 'info' => '여기에서 일반 설정을 수정하세요.', + 'locale-settings' => [ + 'title' => '언어 설정', + 'title-info' => '관리자 패널 인터페이스에서 사용할 언어를 정의합니다. 드롭다운에는 설치된 모든 사용 가능한 언어가 표시됩니다.', + ], + 'admin-logo' => [ + 'logo-image' => '로고 이미지', + 'title' => '관리자 로고', + 'title-info' => '관리자 패널의 로고 이미지를 설정하세요.', + 'favicon-image' => '파비콘 이미지', + 'favicon-image-info' => '관리자 패널의 파비콘 이미지를 설정하세요.', + ], + ], + 'settings' => [ + 'title' => '설정', + 'info' => '여기에서 설정을 수정하세요.', + 'footer' => [ + 'show' => 'Powered By 표시', + 'show-info' => '관리자 패널 하단의 "Powered by" 표시줄을 표시하거나 숨깁니다.', + 'info' => '여기에서 \'Powered by\' 섹션을 설정할 수 있습니다.', + 'powered-by' => 'Powered by 텍스트 편집기', + 'title' => 'Powered by 섹션 설정', + ], + 'menu' => [ + 'activities' => '활동', + 'configuration' => '구성', + 'contacts' => '연락처', + 'dashboard' => '대시보드', + 'draft' => '임시보관함', + 'inbox' => '받은편지함', + 'info' => '여기에서 메뉴 항목 이름을 설정할 수 있습니다.', + 'leads' => '리드', + 'mail' => '메일', + 'organizations' => '조직', + 'outbox' => '발신함', + 'persons' => '개인', + 'products' => '제품', + 'quotes' => '견적서', + 'sent' => '보낸편지함', + 'settings' => '설정', + 'title' => '메뉴 항목 설정', + 'trash' => '휴지통', + ], + 'dashboard' => [ + 'title' => '대시보드 설정', + 'info' => '여기에서 대시보드의 기본 날짜 범위를 설정할 수 있습니다.', + 'date-range' => '기본 날짜 범위', + 'date-range-info' => '대시보드가 로드될 때 사용하는 기간입니다. 대시보드의 날짜 선택기는 일회성 조회를 위해 계속 변경할 수 있습니다.', + 'custom-days' => '사용자 지정 범위(일)', + 'custom-days-info' => '날짜 범위를 사용자 지정으로 설정한 경우 조회할 일수입니다.', + 'ranges' => [ + '1-month' => '1개월', + '3-months' => '3개월', + '9-months' => '9개월', + '1-year' => '1년', + '2-years' => '2년', + 'custom' => '사용자 지정', + ], + ], + 'menu-color' => [ + 'brand-color' => '브랜드 색상', + 'info' => '여기에서 메뉴 항목의 색상을 변경할 수 있습니다.', + 'title' => '메뉴 항목 색상 설정', + ], + ], + ], + 'email' => [ + 'title' => '이메일 설정', + 'info' => '애플리케이션의 이메일 설정입니다.', + 'imap' => [ + 'title' => 'IMAP 설정', + 'info' => '이메일 수신을 위한 IMAP 설정입니다.', + 'account' => [ + 'title' => 'IMAP 계정', + 'title-info' => '여기에서 IMAP 계정 설정을 구성하세요.', + 'host' => '호스트', + 'port' => '포트', + 'encryption' => '암호화 유형', + 'validate-cert' => 'SSL 인증서 유효성 검사', + 'username' => 'IMAP 사용자 이름', + 'password' => 'IMAP 비밀번호', + ], + ], + ], + 'magic-ai' => [ + 'title' => 'Magic AI', + 'info' => '애플리케이션의 Magic AI 설정입니다.', + 'settings' => [ + 'api-key' => 'API 키', + 'api-key-info' => '각 모델마다 OpenRouter API 키를 사용해야 함을 유의하세요. 보안과 성능을 높이는 간단한 방법입니다.', + 'enable' => '사용', + 'info' => 'OpenRouter API 키로 Magic AI 경험을 향상하세요. 지금 연동하여 나만을 위한 원활하고 맞춤화된 AI 경험을 시작해 보세요! 손쉽게 설정을 조정하고 AI 사용을 직접 관리할 수 있습니다.', + 'other' => '기타 모델', + 'other-model' => '다른 모델을 사용하려면 OpenRouter의 모델 ID를 입력하세요.', + 'doc-generation' => 'DOC 생성', + 'doc-generation-info' => 'DOC 생성 기능을 사용하면 DOC 파일에서 데이터를 자동으로 추출하여 텍스트 형식으로 변환할 수 있습니다. 이 기능을 사용하여 업무 효율성과 생산성을 높이세요.', + 'title' => '일반 설정', + 'models' => [ + 'deepseek-r1' => 'Deepseek R1 Distill-llama-8b', + 'gemini-2-0-flash-001' => 'Gemini 2.0 flash-001', + 'gpt-4o' => 'GPT-4.0', + 'gpt-4o-mini' => 'GPT-4.0 mini', + 'grok-2-1212' => 'Grok 2.12', + 'llama-3-2-3b-instruct' => 'Llama 3.2 3b Instruct', + 'title' => '모델', + ], + ], + ], + ], + ], + 'dashboard' => [ + 'index' => [ + 'title' => '대시보드', + 'start-date' => '시작일', + 'end-date' => '종료일', + 'export-pdf' => 'PDF 내보내기', + 'pdf' => [ + 'title' => '대시보드 보고서', + 'filtered-date' => '필터된 날짜', + 'revenue' => '매출', + 'overview' => '개요', + 'total-leads' => '전체 리드', + 'metric' => '지표', + 'current' => '현재', + 'previous' => '이전', + 'progress' => '진행률', + 'date' => '날짜', + 'all' => '전체', + 'total' => '합계', + 'won' => '획득', + 'lost' => '손실', + 'source' => '소스', + 'type' => '유형', + 'stage' => '단계', + 'product' => '상품', + 'person' => '개인', + 'price' => '가격', + 'revenue-by-sources' => '소스별 매출', + 'revenue-by-types' => '유형별 매출', + 'open-leads-by-stages' => '단계별 진행 중인 리드', + 'top-selling-products' => '인기 상품', + 'top-persons' => '상위 개인', + ], + 'revenue' => [ + 'lost-revenue' => '손실 매출', + 'won-revenue' => '획득 매출', + ], + 'over-all' => [ + 'average-lead-value' => '평균 리드 가치', + 'total-leads' => '전체 리드', + 'average-leads-per-day' => '일평균 리드 수', + 'total-quotations' => '전체 견적', + 'total-persons' => '전체 개인', + 'total-organizations' => '전체 조직', + ], + 'total-leads' => [ + 'title' => '리드', + 'total' => '전체 리드', + 'won' => '획득 리드', + 'lost' => '손실 리드', + ], + 'revenue-by-sources' => [ + 'title' => '소스별 매출', + 'empty-title' => '데이터가 없습니다', + 'empty-info' => '선택한 기간에 대한 데이터가 없습니다', + ], + 'revenue-by-types' => [ + 'title' => '유형별 매출', + 'empty-title' => '데이터가 없습니다', + 'empty-info' => '선택한 기간에 대한 데이터가 없습니다', + ], + 'top-selling-products' => [ + 'title' => '인기 상품', + 'empty-title' => '제품을 찾을 수 없습니다', + 'empty-info' => '선택한 기간에 대한 상품이 없습니다', + ], + 'top-persons' => [ + 'title' => '상위 개인', + 'empty-title' => '개인을 찾을 수 없습니다', + 'empty-info' => '선택한 기간에 대한 개인이 없습니다', + ], + 'open-leads-by-states' => [ + 'title' => '단계별 진행 중인 리드', + 'empty-title' => '데이터가 없습니다', + 'empty-info' => '선택한 기간에 대한 데이터가 없습니다', + ], + ], + ], + 'help' => [ + 'index' => [ + 'title' => '도움말 및 리소스', + 'description' => 'Krayin 관리자를 최대한 활용하는 데 필요한 모든 것 — 호스팅, 지원 및 전문 서비스는 물론 확장 기능과 개발자 문서까지 제공합니다.', + 'contact-us' => '문의하기', + 'view-all' => '전체 보기', + 'still-need-help-title' => '더 도움이 필요하신가요?', + 'still-need-help-description' => '호스팅, 맞춤 개발 등 무엇이든 Krayin 팀에 문의해 보세요.', + 'services' => [ + 'title' => '서비스 및 리소스', + 'cloud-hosting' => [ + 'title' => '클라우드 호스팅', + 'description' => '비용 효율적인 관리형 클라우드 호스팅 — 몇 분 만에 완전히 최적화되고 확장 가능한 클라우드 환경에서 Krayin을 체험하고 실행해 보세요.', + ], + 'support' => [ + 'title' => '지원 및 유지보수', + 'description' => 'CRM을 안전하고 최신 상태로 원활하게 운영할 수 있도록 전담 기술 지원과 지속적인 유지보수 플랜을 제공합니다.', + ], + 'extensions' => [ + 'title' => '확장 기능', + 'description' => '새로운 커넥터, 채널, 기능으로 Krayin을 확장할 수 있는 공식 및 커뮤니티 애드온을 살펴보세요.', + ], + ], + 'resources' => [ + 'title' => '문서', + 'user-docs' => [ + 'title' => '사용자 가이드', + 'description' => '모듈 통합, 커스터마이징, 데이터 마이그레이션, 버전 업그레이드 및 맞춤 개발에 대한 전문가의 도움을 받아보세요.', + ], + 'dev-docs' => [ + 'title' => '개발자 문서 및 블로그', + 'description' => '구축, 설정, 최신 정보 확인에 도움이 되는 개발자 가이드, 튜토리얼 및 최신 아티클을 제공합니다.', + ], + 'api-docs' => [ + 'title' => 'API 문서', + 'description' => 'Krayin을 사용 중인 스택과 통합할 수 있도록 엔드포인트, 인증, 예제를 포함한 전체 REST API 레퍼런스를 제공합니다.', + ], + ], + 'community' => [ + 'forums' => [ + 'title' => '커뮤니티 포럼', + 'description' => '수천 명의 Krayin 사용자 및 개발자와 소통하며 팁을 공유하고 문제를 해결해 보세요.', + 'link' => '커뮤니티 참여하기', + ], + 'tutorials' => [ + 'title' => '동영상 튜토리얼', + 'description' => 'CRM 설정, 파이프라인 구성, 사용자 관리에 대한 단계별 동영상 가이드를 시청해 보세요.', + 'link' => 'YouTube 채널 둘러보기', + ], + ], + ], + ], + 'layouts' => [ + 'app-version' => '버전: :version', + 'dashboard' => '대시보드', + 'help' => '도움말 및 리소스', + 'leads' => '리드', + 'quotes' => '견적서', + 'quote' => '견적서', + 'mail' => [ + 'title' => '메일', + 'compose' => '작성', + 'inbox' => '받은편지함', + 'draft' => '임시보관함', + 'outbox' => '발신함', + 'sent' => '보낸편지함', + 'trash' => '휴지통', + 'setting' => '설정', + ], + 'activities' => '활동', + 'contacts' => '연락처', + 'persons' => '개인', + 'person' => '개인', + 'organizations' => '조직', + 'organization' => '조직', + 'products' => '제품', + 'product' => '제품', + 'settings' => '설정', + 'user' => '사용자', + 'user-info' => 'CRM 내 모든 사용자와 권한, 즉 사용자가 수행할 수 있는 작업을 관리합니다.', + 'groups' => '그룹', + 'groups-info' => 'CRM에서 그룹을 추가, 수정 또는 삭제합니다', + 'roles' => '역할', + 'role' => '역할', + 'roles-info' => 'CRM에서 역할을 추가, 수정 또는 삭제합니다', + 'users' => '사용자', + 'users-info' => 'CRM에서 사용자를 추가, 수정 또는 삭제합니다', + 'lead' => '리드', + 'lead-info' => 'CRM 내 리드 관련 설정을 모두 관리합니다', + 'sidebar' => [ + 'collapse' => '접기', + 'expand' => '펼치기', + ], + 'quick-add' => [ + 'title' => '빠른 추가', + 'save' => '저장', + 'to' => '받는사람', + 'subject' => '제목', + 'message' => '메시지', + ], + 'pipelines' => '파이프라인', + 'pipelines-info' => 'CRM에서 파이프라인을 추가, 수정 또는 삭제합니다', + 'sources' => '소스', + 'sources-info' => 'CRM에서 소스를 추가, 수정 또는 삭제합니다', + 'types' => '유형', + 'types-info' => 'CRM에서 유형을 추가, 수정 또는 삭제합니다', + 'automation' => '자동화', + 'automation-info' => 'CRM 내 자동화 관련 설정을 모두 관리합니다', + 'attributes' => '속성', + 'attribute' => '속성', + 'attributes-info' => 'CRM에서 속성을 추가, 수정 또는 삭제합니다', + 'email-templates' => '이메일 템플릿', + 'email' => '이메일', + 'email-templates-info' => 'CRM에서 이메일 템플릿을 추가, 수정 또는 삭제합니다', + 'events' => '이벤트', + 'events-info' => 'CRM에서 이벤트를 추가, 수정 또는 삭제합니다', + 'campaigns' => '캠페인', + 'campaigns-info' => 'CRM에서 캠페인을 추가, 수정 또는 삭제합니다', + 'workflows' => '워크플로우', + 'workflows-info' => 'CRM에서 워크플로우를 추가, 수정 또는 삭제합니다', + 'webhooks' => '웹훅', + 'webhooks-info' => 'CRM에서 웹훅을 추가, 수정 또는 삭제합니다', + 'other-settings' => '기타 설정', + 'other-settings-info' => 'CRM 내 추가 설정을 모두 관리합니다', + 'tags' => '태그', + 'tags-info' => 'CRM에서 태그를 추가, 수정 또는 삭제합니다', + 'my-account' => '내 계정', + 'sign-out' => '로그아웃', + 'back' => '뒤로', + 'name' => '이름', + 'configuration' => '구성', + 'howdy' => '안녕하세요!', + 'warehouses' => '창고', + 'warehouse' => '창고', + 'warehouses-info' => 'CRM에서 창고를 추가, 수정 또는 삭제합니다', + 'inventory' => '재고', + 'inventory-info' => 'CRM 내 재고 관련 설정을 모두 관리합니다', + 'data_transfer' => '데이터 전송', + 'data_transfer_info' => 'CRM 내 담당자, 상품, 리드 데이터 전송 관련 설정을 관리합니다', + ], + 'user' => [ + 'account' => [ + 'name' => '이름', + 'email' => '이메일', + 'password' => '비밀번호', + 'my_account' => '내 계정', + 'update_details' => '정보 업데이트', + 'current_password' => '현재 비밀번호', + 'confirm_password' => '비밀번호 확인', + 'password-match' => '현재 비밀번호가 일치하지 않습니다.', + 'account-save' => '계정 변경 사항이 성공적으로 저장되었습니다.', + 'permission-denied' => '권한이 거부되었습니다', + 'remove-image' => '이미지 제거', + 'upload_image_pix' => '프로필 이미지를 업로드하세요 (100px x 100px)', + 'upload_image_format' => 'PNG 또는 JPG 형식', + 'image_upload_message' => '이미지 파일(.jpeg, .jpg, .png 등)만 업로드할 수 있습니다.', + ], + ], + 'emails' => [ + 'common' => [ + 'dear' => ':name 님께', + 'cheers' => '감사합니다,
    :app_name 팀', + 'user' => [ + 'dear' => ':username 님께', + 'create-subject' => '팀원으로 추가되셨습니다.', + 'create-body' => '축하합니다! 이제 저희 팀의 일원이 되셨습니다.', + 'forget-password' => [ + 'subject' => '고객 비밀번호 재설정', + 'dear' => ':username 님께', + 'reset-password' => '비밀번호 재설정', + 'info' => '계정에 대한 비밀번호 재설정 요청을 받아 이 이메일을 보내드립니다', + 'final-summary' => '비밀번호 재설정을 요청하지 않으셨다면 별도로 조치하실 필요가 없습니다', + 'thanks' => '감사합니다!', + ], + ], + ], + ], + 'validations' => [ + 'message' => [ + 'code' => '이 필드는 유효한 코드여야 합니다.', + 'decimal' => ':attribute 항목은 소수여야 합니다.', + ], + ], + 'errors' => [ + 'dashboard' => '대시보드', + 'go-back' => '뒤로 가기', + 'support' => '문제가 계속되면 :email(으)로 문의해 주세요.', + 'unauthorized' => '이 작업은 권한이 없습니다.', + 'role-exceeds-own' => 'You can only assign a role whose permissions are the same as or fewer than your own.', + 'user-exceeds-own' => 'You can only manage users whose permissions are the same as or fewer than your own.', + 'own-privileges' => 'You cannot change your own role or data scope.', + 'scope-exceeds-own' => 'You cannot grant a data scope wider than your own.', + 'primary-admin-protected' => 'The primary administrator account is protected and cannot be modified here.', + 'role-permissions-exceed-own' => 'You can only create or edit roles whose permissions are the same as or fewer than your own.', + '404' => [ + 'description' => '이런! 찾으시는 페이지가 자리를 비웠습니다. 요청하신 내용을 찾을 수 없습니다.', + 'title' => '404 페이지를 찾을 수 없습니다', + ], + '401' => [ + 'description' => '이런! 이 페이지에 접근할 권한이 없는 것 같습니다. 필요한 인증 정보가 없는 것으로 보입니다.', + 'title' => '401 인증되지 않음', + ], + '403' => [ + 'description' => '이런! 이 페이지는 접근이 제한되어 있습니다. 이 콘텐츠를 볼 수 있는 권한이 없는 것으로 보입니다.', + 'title' => '403 접근 금지', + ], + '500' => [ + 'description' => '이런! 문제가 발생했습니다. 요청하신 페이지를 불러오는 중 오류가 발생한 것 같습니다.', + 'title' => '500 내부 서버 오류', + ], + '503' => [ + 'description' => '이런! 현재 유지보수로 인해 서비스가 일시적으로 중단된 것 같습니다. 잠시 후 다시 확인해 주세요.', + 'title' => '503 서비스 이용 불가', + ], + ], + 'export' => [ + 'csv' => 'CSV', + 'download' => '다운로드', + 'export' => '내보내기', + 'google-contacts' => 'Google 연락처', + 'google-contacts-in-progress' => '연락처를 Google로 내보내는 중입니다... 잠시 시간이 걸릴 수 있습니다.', + 'google-contacts-total' => '전체 연락처:', + 'google-contacts-exported' => '내보냄:', + 'google-contacts-duplicate' => '이미 존재함:', + 'google-contacts-failed' => '실패:', + 'google-contacts-summary' => '내보내기 완료: :exported개 내보냄, :duplicate개 이미 존재함, :failed개 실패.', + 'no-records' => '내보낼 항목이 없습니다', + 'xls' => 'XLS', + 'xlsx' => 'XLSX', + ], +]; diff --git a/packages/Webkul/Admin/src/Resources/lang/pt_BR/app.php b/packages/Webkul/Admin/src/Resources/lang/pt_BR/app.php new file mode 100644 index 0000000..ba38d73 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/lang/pt_BR/app.php @@ -0,0 +1,2481 @@ + [ + 'help' => 'Ajuda', + 'leads' => 'Oportunidades', + 'lead' => 'Oportunidade', + 'quotes' => 'Cotações', + 'mail' => 'E-mail', + 'inbox' => 'Caixa de Entrada', + 'draft' => 'Rascunho', + 'outbox' => 'Caixa de Saída', + 'sent' => 'Enviados', + 'trash' => 'Lixeira', + 'activities' => 'Atividades', + 'webhook' => 'Webhook', + 'contacts' => 'Contatos', + 'persons' => 'Pessoas', + 'organizations' => 'Empresas', + 'products' => 'Produtos', + 'settings' => 'Configurações', + 'groups' => 'Grupos', + 'roles' => 'Funções', + 'users' => 'Usuários', + 'user' => 'Usuário', + 'automation' => 'Automação', + 'attributes' => 'Atributos', + 'pipelines' => 'Funil', + 'sources' => 'Origem', + 'types' => 'Tipos', + 'email-templates' => 'Modelos de E-mail', + 'workflows' => 'Fluxos de Trabalho', + 'other-settings' => 'Outras Configurações', + 'tags' => 'Tags', + 'configuration' => 'Configuração', + 'create' => 'Adicionar', + 'quick_add' => 'Adicionar Rápido', + 'edit' => 'Editar', + 'view' => 'Visualizar', + 'print' => 'Imprimir', + 'delete' => 'Excluir', + 'export' => 'Exportar', + 'mass-delete' => 'Exclusão em Massa', + 'data-transfer' => 'Data Transfer', + 'imports' => 'Imports', + 'import' => 'Import', + 'event' => 'Evento', + 'campaigns' => 'Campanhas', + 'warehouses' => 'Armazéns', + 'inventory' => 'Estoque', + ], + 'users' => [ + 'activate-warning' => 'Sua conta ainda não foi ativada. Por favor, entre em contato com o administrador.', + 'login-error' => 'As credenciais não correspondem aos nossos registros.', + 'not-permission' => 'You do not have permission to access the admin panel.', + 'login' => [ + 'email' => 'Endereço de E-mail', + 'forget-password-link' => 'Esqueceu a Senha?', + 'password' => 'Senha', + 'submit-btn' => 'Acessar', + 'title' => 'Acessar', + ], + 'forget-password' => [ + 'create' => [ + 'email' => 'E-mail Registrado', + 'email-not-exist' => 'E-mail Não Existe', + 'page-title' => 'Esqueceu a Senha', + 'reset-link-sent' => 'Link para redefinir a senha foi enviado', + 'sign-in-link' => 'Voltar para tela de acesso?', + 'submit-btn' => 'Redefinir', + 'title' => 'Recuperar Senha', + ], + ], + 'reset-password' => [ + 'back-link-title' => 'Voltar para tela de acesso?', + 'confirm-password' => 'Confirmar Senha', + 'email' => 'E-mail Registrado', + 'password' => 'Senha', + 'submit-btn' => 'Redefinir Senha', + 'title' => 'Redefinir Senha', + ], + ], + 'account' => [ + 'edit' => [ + 'back-btn' => 'Voltar', + 'change-password' => 'Alterar Senha', + 'confirm-password' => 'Confirmar Senha', + 'current-password' => 'Senha Atual', + 'email' => 'E-mail', + 'general' => 'Geral', + 'invalid-password' => 'A senha atual que você digitou está incorreta.', + 'name' => 'Nome', + 'password' => 'Senha', + 'profile-image' => 'Imagem de Perfil', + 'save-btn' => 'Salvar Conta', + 'title' => 'Minha Conta', + 'update-success' => 'Conta atualizada com sucesso', + 'upload-image-info' => 'Carregue uma imagem de perfil (110px X 110px) no formato PNG ou JPG', + ], + ], + 'components' => [ + 'activities' => [ + 'actions' => [ + 'mail' => [ + 'btn' => 'E-mail', + 'title' => 'Escrever e-mail', + 'to' => 'Para', + 'enter-emails' => 'Pressione Enter para adicionar e-mails', + 'cc' => 'Cópia', + 'bcc' => 'Cópia oculta', + 'subject' => 'Assunto', + 'send-btn' => 'Enviar', + 'message' => 'Mensagem', + ], + 'file' => [ + 'btn' => 'Arquivo', + 'title' => 'Adicionar Arquivo', + 'title-control' => 'Título', + 'name' => 'Nome', + 'description' => 'Descrição', + 'file' => 'Arquivo', + 'save-btn' => 'Salvar Arquivo', + ], + 'note' => [ + 'btn' => 'Nota', + 'title' => 'Adicionar Nota', + 'comment' => 'Comentário', + 'save-btn' => 'Salvar Nota', + ], + 'activity' => [ + 'btn' => 'Atividade', + 'title' => 'Adicionar Atividade', + 'title-control' => 'Título', + 'description' => 'Descrição', + 'schedule-from' => 'Agendar De', + 'schedule-to' => 'Agendar Até', + 'location' => 'Localização', + 'call' => 'Chamada', + 'meeting' => 'Reunião', + 'lunch' => 'Almoço', + 'save-btn' => 'Salvar Atividade', + 'participants' => [ + 'title' => 'Participantes', + 'placeholder' => 'Digite para pesquisar participantes', + 'users' => 'Usuários', + 'persons' => 'Pessoas', + 'no-results' => 'Nenhum resultado encontrado...', + ], + ], + ], + 'index' => [ + 'all' => 'Todos', + 'bcc' => 'Cópia oculta', + 'by-user' => 'Por :user', + 'calls' => 'Chamadas', + 'cc' => 'Cópia', + 'change-log' => 'Logs de Alterações', + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'emails' => 'E-mails', + 'empty' => 'Vazio', + 'files' => 'Arquivos', + 'from' => 'De', + 'location' => 'Localização', + 'lunches' => 'Almoços', + 'mark-as-done' => 'Marcar como Concluído', + 'meetings' => 'Reuniões', + 'notes' => 'Notas', + 'participants' => 'Participantes', + 'planned' => 'Planejado', + 'quotes' => 'Cotações', + 'scheduled-on' => 'Agendado em', + 'system' => 'Sistema', + 'to' => 'Para', + 'unlink' => 'Desvincular', + 'view' => 'Visualizar', + 'empty-placeholders' => [ + 'all' => [ + 'title' => 'Nenhuma atividade encontrada', + 'description' => 'Nenhuma atividade foi encontrada para este item. Você pode adicionar atividades clicando no botão Atividade no painel à esquerda.', + ], + 'planned' => [ + 'title' => 'Nenhuma atividade planejada encontrada', + 'description' => 'Nenhuma atividade planejada foi encontrada. Adicione uma clicando no botão Atividade no painel à esquerda.', + ], + 'notes' => [ + 'title' => 'Nenhuma nota encontrada', + 'description' => 'Nenhuma nota foi encontrada. Adicione notas clicando no botão Nota no painel à esquerda.', + ], + 'calls' => [ + 'title' => 'Nenhuma chamada encontrada', + 'description' => 'Nenhuma chamada foi encontrada. Adicione chamadas clicando no botão Atividade e selecionando o tipo Chamada.', + ], + 'meetings' => [ + 'title' => 'Nenhuma reunião encontrada', + 'description' => 'Nenhuma reunião foi encontrada. Adicione reuniões clicando no botão Atividade e selecionando o tipo Reunião.', + ], + 'lunches' => [ + 'title' => 'Nenhum almoço encontrado', + 'description' => 'Nenhum almoço foi encontrado. Adicione almoços clicando no botão Atividade e selecionando o tipo Almoço.', + ], + 'files' => [ + 'title' => 'Nenhum arquivo encontrado', + 'description' => 'Nenhum arquivo foi encontrado. Adicione arquivos clicando no botão Arquivo no painel à esquerda.', + ], + 'emails' => [ + 'title' => 'Nenhum e-mail encontrado', + 'description' => 'Nenhum e-mail foi encontrado. Adicione e-mails clicando no botão Correio no painel à esquerda.', + ], + 'system' => [ + 'title' => 'Nenhum registro de alteração encontrado', + 'description' => 'Nenhum registro de alteração foi encontrado.', + ], + ], + ], + ], + 'media' => [ + 'images' => [ + 'add-image-btn' => 'Adicionar Imagem', + 'ai-add-image-btn' => 'Mágica AI', + 'allowed-types' => 'png, jpeg, jpg', + 'not-allowed-error' => 'Apenas arquivos de imagem (.jpeg, .jpg, .png, ..) são permitidos.', + 'placeholders' => [ + 'front' => 'Frente', + 'next' => 'Próximo', + 'size' => 'Tamanho', + 'use-cases' => 'Casos de Uso', + 'zoom' => 'Zoom', + ], + ], + 'videos' => [ + 'add-video-btn' => 'Adicionar Vídeo', + 'allowed-types' => 'mp4, webm, mkv', + 'not-allowed-error' => 'Apenas arquivos de vídeo (.mp4, .mov, .ogg ..) são permitidos.', + ], + ], + 'datagrid' => [ + 'index' => [ + 'no-records-selected' => 'Nenhum registro foi selecionado.', + 'must-select-a-mass-action-option' => 'Você deve selecionar uma opção de ação em massa.', + 'must-select-a-mass-action' => 'Você deve selecionar uma ação em massa.', + ], + 'toolbar' => [ + 'length-of' => ':length de', + 'of' => 'de', + 'per-page' => 'Por Página', + 'results' => ':total Resultados', + 'delete' => 'Excluir', + 'selected' => ':total Itens Selecionados', + 'mass-actions' => [ + 'submit' => 'Enviar', + 'select-option' => 'Selecionar Opção', + 'select-action' => 'Selecionar Ação', + ], + 'filter' => [ + 'apply-filters-btn' => 'Aplicar Filtros', + 'back-btn' => 'Voltar', + 'create-new-filter' => 'Adicionar novo Filtro', + 'custom-filters' => 'Filtros Personalizados', + 'delete-error' => 'Algo deu errado ao excluir o filtro, por favor tente novamente.', + 'delete-success' => 'Filtro excluído com sucesso.', + 'empty-description' => 'Não há filtros selecionados disponíveis para salvar. Por favor, selecione filtros para salvar.', + 'empty-title' => 'Adicionar Filtros para Salvar', + 'name' => 'Nome', + 'quick-filters' => 'Filtros Rápidos', + 'save-btn' => 'Salvar', + 'save-filter' => 'Salvar Filtro', + 'saved-success' => 'Filtro salvo com sucesso.', + 'selected-filters' => 'Filtros Selecionados', + 'title' => 'Filtro', + 'update' => 'Atualizar', + 'update-filter' => 'Atualizar Filtro', + 'updated-success' => 'Filtro atualizado com sucesso.', + ], + 'search' => [ + 'title' => 'Pesquisar', + ], + 'column-settings' => [ + 'title' => 'Configurações de Colunas', + ], + ], + 'filters' => [ + 'select' => 'Selecionar', + 'title' => 'Filtros', + 'dropdown' => [ + 'searchable' => [ + 'at-least-two-chars' => 'Digite pelo menos 2 caracteres...', + 'no-results' => 'Nenhum resultado encontrado...', + ], + ], + 'custom-filters' => [ + 'clear-all' => 'Limpar Todos', + 'title' => 'Filtros Personalizados', + ], + 'boolean-options' => [ + 'false' => 'Falso', + 'true' => 'Verdadeiro', + ], + 'date-options' => [ + 'last-month' => 'Último Mês', + 'last-six-months' => 'Últimos 6 Meses', + 'last-three-months' => 'Últimos 3 Meses', + 'this-month' => 'Este Mês', + 'this-week' => 'Esta Semana', + 'this-year' => 'Este Ano', + 'today' => 'Hoje', + 'yesterday' => 'Ontem', + ], + ], + 'table' => [ + 'actions' => 'Ações', + 'no-records-available' => 'Nenhum Registro Disponível.', + ], + ], + 'modal' => [ + 'confirm' => [ + 'agree-btn' => 'Concordar', + 'disagree-btn' => 'Discordar', + 'message' => 'Você tem certeza de que deseja realizar esta ação?', + 'title' => 'Você tem certeza?', + ], + ], + 'tags' => [ + 'index' => [ + 'title' => 'Tags', + 'added-tags' => 'Tags Adicionadas', + 'save-btn' => 'Salvar Tag', + 'placeholder' => 'Digite para procurar tags', + 'add-tag' => 'Adicionar \":term\"...', + 'aquarelle-red' => 'Vermelho', + 'crushed-cashew' => 'Castanho', + 'beeswax' => 'Amarelado', + 'lemon-chiffon' => 'Limão', + 'snow-flurry' => 'Neve', + 'honeydew' => 'Melão', + ], + ], + 'layouts' => [ + 'powered-by' => [ + 'description' => 'Desenvolvido por :krayin, um projeto de código aberto da :webkul.', + ], + 'header' => [ + 'mega-search' => [ + 'title' => 'Busca rápida', + 'tabs' => [ + 'leads' => 'Oportunidades', + 'quotes' => 'Cotações', + 'persons' => 'Pessoas', + 'products' => 'Produtos', + 'settings' => 'Configurações', + 'configurations' => 'Configurações', + ], + 'explore-all-products' => 'Explorar todos os Produtos', + 'explore-all-leads' => 'Explorar todos as Oportunidades', + 'explore-all-contacts' => 'Explorar todos os Contatos', + 'explore-all-quotes' => 'Explorar todas as Cotações', + 'explore-all-settings' => 'Explorar todas as Configurações', + 'explore-all-configurations' => 'Explorar todas as Configurações', + 'explore-all-matching-products' => 'Explorar todos os produtos correspondentes a ":query" (:count)', + 'explore-all-matching-leads' => 'Explorar todos os negócios correspondentes a ":query" (:count)', + 'explore-all-matching-contacts' => 'Explorar todos os contatos correspondentes a ":query" (:count)', + 'explore-all-matching-quotes' => 'Explorar todas as cotações correspondentes a ":query" (:count)', + ], + ], + ], + 'attributes' => [ + 'edit' => [ + 'delete' => 'Excluir', + ], + 'lookup' => [ + 'click-to-add' => 'Clique para adicionar', + 'search' => 'Pesquisar...', + 'no-result-found' => 'Nenhum resultado encontrado', + ], + ], + 'lookup' => [ + 'click-to-add' => 'Clique para Adicionar', + 'no-results' => 'Nenhum Resultado Encontrado', + 'add-as-new' => 'Adicionar como Novo', + 'search' => 'Pesquisar...', + ], + 'flash-group' => [ + 'success' => 'Sucesso', + 'error' => 'Erro', + 'warning' => 'Aviso', + 'info' => 'Informação', + ], + 'tiny-mce' => [ + 'http-error' => 'Erro HTTP', + 'invalid-json' => 'Resposta JSON inválida do servidor.', + 'upload-failed' => 'Falha no upload do arquivo. Por favor, tente novamente.', + ], + ], + 'quotes' => [ + 'index' => [ + 'title' => 'Cotações', + 'create-btn' => 'Adicionar Cotação', + 'create-success' => 'Cotação adicionada com sucesso.', + 'update-success' => 'Cotação atualizada com sucesso.', + 'delete-success' => 'Cotação excluída com sucesso.', + 'delete-failed' => 'Não é possível excluir a cotação.', + 'datagrid' => [ + 'subject' => 'Assunto', + 'sales-person' => 'Vendedor', + 'expired-at' => 'Expirado em', + 'created-at' => 'Criado em', + 'person' => 'Pessoa', + 'subtotal' => 'Subtotal', + 'discount' => 'Desconto', + 'tax' => 'Imposto', + 'adjustment' => 'Ajuste', + 'grand-total' => 'Total Geral', + 'edit' => 'Editar', + 'delete' => 'Excluir', + 'print' => 'Imprimir', + ], + 'pdf' => [ + 'adjustment' => 'Ajuste', + 'amount' => 'Valor', + 'billing-address' => 'Endereço de Cobrança', + 'date' => 'Data', + 'discount' => 'Desconto', + 'description' => 'Descrição', + 'expired-at' => 'Expirado em', + 'grand-total' => 'Total Geral', + 'person' => 'Pessoa', + 'price' => 'Preço', + 'product-name' => 'Nome do Produto', + 'quantity' => 'Quantidade', + 'quote-id' => 'ID da Cotação', + 'sales-person' => 'Vendedor', + 'shipping-address' => 'Endereço de Envio', + 'sku' => 'Código', + 'sub-total' => 'Subtotal', + 'subject' => 'Assunto', + 'tax' => 'Imposto', + 'title' => 'Cotação', + ], + ], + 'create' => [ + 'title' => 'Adicionar Cotação', + 'save-btn' => 'Salvar Cotação', + 'quote-info' => 'Informações da Cotação', + 'quote-info-info' => 'Informe as informações básicas da cotação.', + 'address-info' => 'Informações de Endereço', + 'address-info-info' => 'Informações sobre o endereço relacionado à cotação.', + 'same-as-billing' => 'Endereço de entrega igual ao endereço de cobrança', + 'quote-items' => 'Itens da Cotação', + 'search-products' => 'Pesquisar Produtos', + 'link-to-lead' => 'Vincular a um negócio', + 'quote-item-info' => 'Adicionar solicitação de produto para esta cotação.', + 'quote-name' => 'Nome da Cotação', + 'quantity' => 'Quantidade', + 'price' => 'Preço', + 'discount' => 'Desconto', + 'tax' => 'Imposto', + 'total' => 'Total', + 'amount' => 'Valor', + 'add-item' => '+ Adicionar Item', + 'sub-total' => 'Subtotal (:symbol)', + 'total-discount' => 'Desconto (:symbol)', + 'total-tax' => 'Imposto (:symbol)', + 'total-adjustment' => 'Ajuste (:symbol)', + 'grand-total' => 'Total Geral (:symbol)', + 'discount-amount' => 'Valor do Desconto', + 'tax-amount' => 'Valor do Imposto', + 'adjustment-amount' => 'Valor do Ajuste', + 'product-name' => 'Nome do Produto', + 'action' => 'Ação', + ], + 'edit' => [ + 'title' => 'Editar Cotação', + 'save-btn' => 'Salvar Cotação', + 'quote-info' => 'Informações da Cotação', + 'quote-info-info' => 'Informe as informações básicas da cotação.', + 'address-info' => 'Informações de Endereço', + 'address-info-info' => 'Informações sobre o endereço relacionado à cotação.', + 'quote-items' => 'Itens da Cotação', + 'link-to-lead' => 'Vincular a um negócio', + 'quote-item-info' => 'Adicionar solicitação de produto para esta cotação.', + 'quote-name' => 'Nome da Cotação', + 'quantity' => 'Quantidade', + 'price' => 'Preço', + 'search-products' => 'Pesquisar Produtos', + 'discount' => 'Desconto', + 'tax' => 'Imposto', + 'total' => 'Total', + 'amount' => 'Valor', + 'add-item' => '+ Adicionar Item', + 'sub-total' => 'Subtotal (:symbol)', + 'total-discount' => 'Desconto (:symbol)', + 'total-tax' => 'Imposto (:symbol)', + 'total-adjustment' => 'Ajuste (:symbol)', + 'grand-total' => 'Total Geral (:symbol)', + 'discount-amount' => 'Valor do Desconto', + 'tax-amount' => 'Valor do Imposto', + 'adjustment-amount' => 'Valor do Ajuste', + 'product-name' => 'Nome do Produto', + 'action' => 'Ação', + ], + ], + 'contacts' => [ + 'persons' => [ + 'index' => [ + 'title' => 'Pessoas', + 'create-btn' => 'Adicionar Pessoa', + 'create-success' => 'Pessoa adicionada com sucesso.', + 'update-success' => 'Pessoa atualizada com sucesso.', + 'all-delete-success' => 'Todas as pessoas selecionadas foram excluídas com sucesso.', + 'partial-delete-warning' => 'Algumas pessoas foram excluídas com sucesso. Outras não puderam ser excluídas porque estão vinculadas a leads.', + 'none-delete-warning' => 'Nenhuma das pessoas selecionadas pôde ser excluída porque estão vinculadas a leads.', + 'no-selection' => 'Nenhuma pessoa foi selecionada para exclusão.', + 'delete-failed' => 'Falha ao excluir as pessoas selecionadas.', + 'datagrid' => [ + 'contact-numbers' => 'Números de Contato', + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'emails' => 'E-mails', + 'id' => 'ID', + 'view' => 'Visualizar', + 'name' => 'Nome', + 'organization-name' => 'Nome da Empresa', + 'tag-name' => 'Nome da Tag', + 'google-export-not-connected' => 'Conecte sua conta do Google primeiro em Configurações > Outras Configurações > Google Contacts.', + 'google-export-started' => 'Exportação para o Google Contacts iniciada.', + ], + ], + 'view' => [ + 'title' => ':name', + 'about-person' => 'Sobre a Pessoa', + 'about-organization' => 'Sobre a Empresa', + 'activities' => [ + 'index' => [ + 'all' => 'Todos', + 'calls' => 'Chamadas', + 'meetings' => 'Reuniões', + 'lunches' => 'Almoços', + 'files' => 'Arquivos', + 'quotes' => 'Cotações', + 'notes' => 'Notas', + 'emails' => 'E-mails', + 'by-user' => 'Por :user', + 'scheduled-on' => 'Agendado em', + 'location' => 'Localização', + 'participants' => 'Participantes', + 'mark-as-done' => 'Marcar como Concluído', + 'delete' => 'Excluir', + 'edit' => 'Editar', + ], + 'actions' => [ + 'mail' => [ + 'btn' => 'E-mail', + 'title' => 'Escrever e-mail', + 'to' => 'Para', + 'cc' => 'Cópia', + 'bcc' => 'Cópia oculta', + 'subject' => 'Assunto', + 'send-btn' => 'Enviar', + 'message' => 'Mensagem', + ], + 'file' => [ + 'btn' => 'Arquivo', + 'title' => 'Adicionar Arquivo', + 'title-control' => 'Título', + 'name' => 'Nome do Arquivo', + 'description' => 'Descrição', + 'file' => 'Arquivo', + 'save-btn' => 'Salvar Arquivo', + ], + 'note' => [ + 'btn' => 'Nota', + 'title' => 'Adicionar Nota', + 'comment' => 'Comentário', + 'save-btn' => 'Salvar Nota', + ], + 'activity' => [ + 'btn' => 'Atividade', + 'title' => 'Adicionar Atividade', + 'title-control' => 'Título', + 'description' => 'Descrição', + 'schedule-from' => 'Agendar De', + 'schedule-to' => 'Agendar Até', + 'location' => 'Localização', + 'call' => 'Chamada', + 'meeting' => 'Reunião', + 'lunch' => 'Almoço', + 'save-btn' => 'Salvar Atividade', + ], + ], + ], + 'tags' => [ + 'create-success' => 'Tag criada com sucesso.', + 'destroy-success' => 'Tag excluída com sucesso.', + ], + ], + 'create' => [ + 'title' => 'Adicionar Pessoa', + 'save-btn' => 'Salvar Pessoa', + ], + 'edit' => [ + 'title' => 'Editar Pessoa', + 'save-btn' => 'Salvar Pessoa', + ], + ], + 'organizations' => [ + 'index' => [ + 'title' => 'Empresas', + 'create-btn' => 'Adicionar Empresa', + 'create-success' => 'Empresa adicionada com sucesso.', + 'update-success' => 'Empresa atualizada com sucesso.', + 'delete-success' => 'Empresa excluída com sucesso.', + 'delete-failed' => 'Não foi possível excluir a empresa.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + 'persons-count' => 'Quantidade de Pessoas', + ], + ], + 'create' => [ + 'title' => 'Adicionar Empresa', + 'save-btn' => 'Salvar Empresa', + ], + 'edit' => [ + 'title' => 'Editar Empresa', + 'save-btn' => 'Salvar Empresa', + ], + ], + ], + 'products' => [ + 'index' => [ + 'title' => 'Produtos', + 'create-btn' => 'Adicionar Produto', + 'create-success' => 'Produto adicionado com sucesso.', + 'update-success' => 'Produto atualizado com sucesso.', + 'delete-success' => 'Produto excluído com sucesso.', + 'delete-failed' => 'Não foi possível excluir o produto.', + 'datagrid' => [ + 'allocated' => 'Alocado', + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'id' => 'ID', + 'in-stock' => 'Em Estoque', + 'name' => 'Nome', + 'on-hand' => 'Disponível', + 'tag-name' => 'Nome da Tag', + 'price' => 'Preço', + 'sku' => 'Código', + 'view' => 'Visualizar', + ], + ], + 'create' => [ + 'save-btn' => 'Salvar Produtos', + 'title' => 'Adicionar Produtos', + 'general' => 'Geral', + 'price' => 'Preço', + ], + 'edit' => [ + 'title' => 'Editar Produtos', + 'save-btn' => 'Salvar Produtos', + 'general' => 'Geral', + 'price' => 'Preço', + ], + 'view' => [ + 'sku' => 'Código', + 'all' => 'Todos', + 'notes' => 'Notas', + 'files' => 'Arquivos', + 'inventories' => 'Inventário', + 'change-logs' => 'Histórico de Alterações', + 'attributes' => [ + 'about-product' => 'Sobre o Produto', + ], + 'inventory' => [ + 'warehouse' => 'Armazém', + 'in-stock' => 'Em Estoque', + 'allocated' => 'Alocado', + 'on-hand' => 'Disponível', + 'actions' => 'Ações', + 'assign' => 'Atribuir', + 'add-warehouse' => 'Adicionar armazém', + 'location' => 'Localização', + 'add-more' => 'Adicionar Mais', + 'save' => 'Salvar', + ], + ], + ], + 'settings' => [ + 'title' => 'Configurações', + 'groups' => [ + 'index' => [ + 'create-btn' => 'Criar Grupo', + 'title' => 'Grupos', + 'create-success' => 'Grupo criado com sucesso.', + 'update-success' => 'Grupo atualizado com sucesso.', + 'destroy-success' => 'Grupo excluído com sucesso.', + 'delete-failed' => 'Não foi possível excluir o grupo.', + 'delete-failed-associated-users' => 'Não foi possível excluir o grupo, pois está sendo utilizado por usuários.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'description' => 'Descrição', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + ], + 'edit' => [ + 'title' => 'Editar Grupo', + ], + 'create' => [ + 'name' => 'Nome', + 'title' => 'Adicionar Grupo', + 'description' => 'Descrição', + 'save-btn' => 'Salvar Grupo', + ], + ], + ], + 'roles' => [ + 'index' => [ + 'being-used' => 'Não é possível excluir o cargo, pois está sendo usado por um usuário administrador.', + 'create-btn' => 'Adicionar Cargos', + 'create-success' => 'Cargo adicionado com sucesso.', + 'current-role-delete-error' => 'Não é possível excluir o cargo atribuído ao usuário atual.', + 'current-role-edit-error' => 'Não é possível editar o cargo atribuído à sua própria conta.', + 'delete-failed' => 'Não foi possível excluir o cargo.', + 'delete-success' => 'Cargo excluído com sucesso.', + 'last-delete-error' => 'É necessário pelo menos um cargo.', + 'settings' => 'Configurações', + 'title' => 'Cargos', + 'update-success' => 'Cargo atualizado com sucesso.', + 'user-define-error' => 'Não é possível excluir cargo do sistema.', + 'datagrid' => [ + 'all' => 'Todos', + 'custom' => 'Personalizado', + 'delete' => 'Excluir', + 'description' => 'Descrição', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + 'permission-type' => 'Tipo de Permissão', + ], + ], + 'create' => [ + 'access-control' => 'Controle de Acesso', + 'all' => 'Todos', + 'back-btn' => 'Voltar', + 'custom' => 'Personalizado', + 'description' => 'Descrição', + 'general' => 'Geral', + 'name' => 'Nome', + 'permissions' => 'Permissões', + 'save-btn' => 'Salvar Cargo', + 'title' => 'Adicionar Cargo', + ], + 'edit' => [ + 'access-control' => 'Controle de Acesso', + 'all' => 'Todos', + 'back-btn' => 'Voltar', + 'custom' => 'Personalizado', + 'description' => 'Descrição', + 'general' => 'Geral', + 'name' => 'Nome', + 'permissions' => 'Permissões', + 'save-btn' => 'Salvar Cargo', + 'title' => 'Editar Cargo', + ], + ], + 'types' => [ + 'index' => [ + 'create-btn' => 'Adicionar Tipo', + 'create-success' => 'Tipo adicionado com sucesso.', + 'delete-failed' => 'Não é possível excluir o tipo.', + 'delete-success' => 'Tipo excluído com sucesso.', + 'title' => 'Tipos', + 'update-success' => 'Tipo atualizado com sucesso.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'description' => 'Descrição', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + ], + 'create' => [ + 'name' => 'Nome', + 'save-btn' => 'Salvar Tipo', + 'title' => 'Adicionar Tipo', + ], + 'edit' => [ + 'title' => 'Editar Tipo', + ], + ], + ], + 'sources' => [ + 'index' => [ + 'title' => 'Fontes', + 'create-btn' => 'Criar Fonte', + 'create-success' => 'Fonte criada com sucesso.', + 'delete-failed' => 'Não foi possível excluir a fonte.', + 'delete-success' => 'Fonte excluída com sucesso.', + 'update-success' => 'Fonte atualizada com sucesso.', + 'delete-failed-associated-leads' => 'Não é possível excluir a fonte porque está associada a leads existentes. Por favor, desvincule ou atualize esses leads antes da exclusão.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + ], + 'create' => [ + 'name' => 'Nome', + 'save-btn' => 'Salvar Origem', + 'title' => 'Adicionar Origem', + ], + 'edit' => [ + 'title' => 'Editar Origem', + ], + ], + ], + 'workflows' => [ + 'index' => [ + 'title' => 'Workflows', + 'create-btn' => 'Adicionar Workflow', + 'create-success' => 'Workflow adicionado com sucesso.', + 'update-success' => 'Workflow atualizado com sucesso.', + 'delete-success' => 'Workflow excluído com sucesso.', + 'delete-failed' => 'Não é possível excluir o Workflow.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'description' => 'Descrição', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + ], + ], + 'helpers' => [ + 'update-related-leads' => 'Atualizar leads relacionados', + 'send-email-to-sales-owner' => 'Enviar e-mail para o proprietário de vendas', + 'send-email-to-participants' => 'Enviar e-mail para os participantes', + 'add-webhook' => 'Adicionar Webhook', + 'update-lead' => 'Atualizar Oportunidade', + 'update-person' => 'Atualizar Pessoa', + 'send-email-to-person' => 'Enviar e-mail para a pessoa', + 'add-tag' => 'Adicionar Tag', + 'add-note-as-activity' => 'Adicionar Nota como Atividade', + 'update-quote' => 'Update Quote', + ], + 'create' => [ + 'title' => 'Adicionar Workflow', + 'event' => 'Evento', + 'back-btn' => 'Voltar', + 'save-btn' => 'Salvar Workflow', + 'name' => 'Nome', + 'basic-details' => 'Detalhes Básicos', + 'description' => 'Descrição', + 'actions' => 'Ações', + 'basic-details-info' => 'Informe as informações básicas do workflow.', + 'event-info' => 'Um evento dispara, verifica, aplica condições e executa ações predefinidas.', + 'conditions' => 'Condições', + 'conditions-info' => 'As condições são regras que verificam cenários, acionadas em ocasiões específicas.', + 'actions-info' => 'Uma ação não apenas reduz a carga de trabalho, mas também facilita a automação do CRM.', + 'value' => 'Valor', + 'condition-type' => 'Tipo de Condição', + 'all-condition-are-true' => 'Todas as condições são verdadeiras', + 'any-condition-are-true' => 'Qualquer condição é verdadeira', + 'add-condition' => 'Adicionar Condição', + 'add-action' => 'Adicionar Ação', + 'no-webhook-found' => 'Nenhum webhook encontrado.', + 'create-webhook' => 'Criar um novo webhook.', + 'yes' => 'Sim', + 'no' => 'Não', + 'email' => 'E-mail', + 'is-equal-to' => 'É igual a', + 'is-not-equal-to' => 'Não é igual a', + 'equals-or-greater-than' => 'É igual ou maior que', + 'equals-or-less-than' => 'É igual ou menor que', + 'greater-than' => 'Maior que', + 'less-than' => 'Menor que', + 'type' => 'Tipo', + 'contain' => 'Contém', + 'contains' => 'Contém', + 'does-not-contain' => 'Não contém', + ], + 'edit' => [ + 'title' => 'Editar Workflow', + 'event' => 'Evento', + 'back-btn' => 'Voltar', + 'save-btn' => 'Salvar Workflow', + 'name' => 'Nome', + 'basic-details' => 'Detalhes Básicos', + 'description' => 'Descrição', + 'actions' => 'Ações', + 'type' => 'Tipo', + 'basic-details-info' => 'Informe as informações básicas do workflow.', + 'event-info' => 'Um evento dispara, verifica, aplica condições e executa ações predefinidas.', + 'conditions' => 'Condições', + 'conditions-info' => 'As condições são regras que verificam cenários, acionadas em ocasiões específicas.', + 'actions-info' => 'Uma ação não apenas reduz a carga de trabalho, mas também facilita a automação do CRM.', + 'value' => 'Valor', + 'condition-type' => 'Tipo de Condição', + 'all-condition-are-true' => 'Todas as condições são verdadeiras', + 'any-condition-are-true' => 'Qualquer condição é verdadeira', + 'add-condition' => 'Adicionar Condição', + 'add-action' => 'Adicionar Ação', + 'yes' => 'Sim', + 'no' => 'Não', + 'email' => 'E-mail', + 'is-equal-to' => 'É igual a', + 'is-not-equal-to' => 'Não é igual a', + 'equals-or-greater-than' => 'É igual ou maior que', + 'equals-or-less-than' => 'É igual ou menor que', + 'greater-than' => 'Maior que', + 'less-than' => 'Menor que', + 'contain' => 'Contém', + 'contains' => 'Contém', + 'does-not-contain' => 'Não contém', + ], + ], + 'google-contacts' => [ + 'index' => [ + 'title' => 'Google Contacts', + 'not-connected-info' => 'Conecte uma conta do Google para exportar contatos do CRM para o Google Contacts.', + 'connected-info' => 'Conectado como :email.', + 'connect-btn' => 'Conectar Conta do Google', + 'disconnect-btn' => 'Desconectar', + 'connect-success' => 'Conta do Google conectada com sucesso.', + 'connect-failed' => 'Não foi possível conectar sua conta do Google. Tente novamente.', + 'not-configured' => 'O Google OAuth não está configurado. Configure primeiro as variáveis de ambiente necessárias.', + 'disconnect-success' => 'Conta do Google desconectada com sucesso.', + ], + ], + 'webforms' => [ + 'index' => [ + 'title' => 'Webforms', + 'create-btn' => 'Adicionar Webform', + 'create-success' => 'Webform adicionado com sucesso.', + 'update-success' => 'Webform atualizado com sucesso.', + 'delete-success' => 'Webform excluído com sucesso.', + 'delete-failed' => 'Não é possível excluir o Webform.', + 'datagrid' => [ + 'id' => 'ID', + 'title' => 'Título', + 'edit' => 'Editar', + 'delete' => 'Excluir', + ], + ], + 'create' => [ + 'title' => 'Adicionar Webform', + 'add-attribute-btn' => 'Adicionar Botão de Atributo', + 'attribute-label-color' => 'Cor do Rótulo do Atributo', + 'attributes' => 'Atributos', + 'attributes-info' => 'Adicione atributos personalizados ao formulário.', + 'background-color' => 'Cor de Fundo', + 'create-lead' => 'Adicionar Oportunidade', + 'customize-webform' => 'Personalizar Webform', + 'customize-webform-info' => 'Personalize seu formulário com as cores dos elementos de sua escolha.', + 'description' => 'Descrição', + 'display-custom-message' => 'Exibir mensagem personalizada', + 'form-background-color' => 'Cor de Fundo do Formulário', + 'form-submit-btn-color' => 'Cor do Botão de Envio do Formulário', + 'form-submit-button-color' => 'Cor do Botão de Envio do Formulário', + 'form-title-color' => 'Cor do Título do Formulário', + 'general' => 'Geral', + 'leads' => 'Oportunidades', + 'person' => 'Pessoa', + 'pipeline' => 'Funil', + 'save-btn' => 'Salvar Webform', + 'submit-button-label' => 'Rótulo do Botão de Envio', + 'submit-success-action' => 'Ação de Sucesso ao Enviar', + 'redirect-to-url' => 'Redirecionar Para URL', + 'choose-value' => 'Escolher Valor', + 'select-file' => 'Selecionar Arquivo', + 'select-image' => 'Selecionar Imagem', + 'enter-value' => 'Inserir Valor', + ], + 'edit' => [ + 'add-attribute-btn' => 'Adicionar Botão de Atributo', + 'attribute-label-color' => 'Cor do Rótulo do Atributo', + 'attributes' => 'Atributos', + 'attributes-info' => 'Adicione atributos personalizados ao formulário.', + 'background-color' => 'Cor de Fundo', + 'choose-value' => 'Escolher Valor', + 'code-snippet' => 'Trecho de Código', + 'copied' => 'Copiado', + 'copy' => 'Copiar', + 'create-lead' => 'Adicionar Oportunidade', + 'customize-webform' => 'Personalizar Webform', + 'customize-webform-info' => 'Personalize seu formulário com as cores dos elementos de sua escolha.', + 'description' => 'Descrição', + 'display-custom-message' => 'Exibir mensagem personalizada', + 'embed' => 'Incorporar', + 'enter-value' => 'Inserir Valor', + 'form-background-color' => 'Cor de Fundo do Formulário', + 'form-submit-btn-color' => 'Cor do Botão de Envio do Formulário', + 'form-submit-button-color' => 'Cor do Botão de Envio do Formulário', + 'form-title-color' => 'Cor do Título do Formulário', + 'general' => 'Geral', + 'leads' => 'Oportunidades', + 'person' => 'Pessoa', + 'pipeline' => 'Funil', + 'preview' => 'Visualizar', + 'public-url' => 'URL Pública', + 'redirect-to-url' => 'Redirecionar Para URL', + 'save-btn' => 'Salvar Webform', + 'select-file' => 'Selecionar Arquivo', + 'select-image' => 'Selecionar Imagem', + 'submit-button-label' => 'Rótulo do Botão de Envio', + 'submit-success-action' => 'Ação de Sucesso ao Enviar', + 'title' => 'Editar Webform', + ], + ], + 'email-template' => [ + 'index' => [ + 'create-btn' => 'Adicionar Modelo de E-mail', + 'title' => 'Modelos de E-mail', + 'create-success' => 'Modelo de E-mail adicionado com sucesso.', + 'update-success' => 'Modelo de E-mail atualizado com sucesso.', + 'delete-success' => 'Modelo de E-mail excluído com sucesso.', + 'delete-failed' => 'Não é possível excluir o Modelo de E-mail.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + 'subject' => 'Assunto', + ], + ], + 'create' => [ + 'title' => 'Adicionar Modelo de E-mail', + 'save-btn' => 'Salvar Modelo de E-mail', + 'email-template' => 'Modelo de E-mail', + 'subject' => 'Assunto', + 'content' => 'Conteúdo', + 'subject-placeholders' => 'Descrição do Assunto', + 'general' => 'Geral', + 'name' => 'Nome', + ], + 'edit' => [ + 'title' => 'Editar Modelo de E-mail', + 'save-btn' => 'Salvar Modelo de E-mail', + 'email-template' => 'Modelo de E-mail', + 'subject' => 'Assunto', + 'content' => 'Conteúdo', + 'subject-placeholders' => 'Descrição do Assunto', + 'general' => 'Geral', + 'name' => 'Nome', + ], + ], + 'marketing' => [ + 'events' => [ + 'index' => [ + 'create-btn' => 'Adicionar Evento', + 'title' => 'Eventos', + 'create-success' => 'Evento adicionado com sucesso.', + 'update-success' => 'Evento atualizado com sucesso.', + 'delete-success' => 'Evento excluído com sucesso.', + 'delete-failed' => 'Não é possível excluir o evento.', + 'mass-delete-success' => 'Eventos excluídos com sucesso', + 'delete-failed-associated-campaigns' => 'Não é possível excluir o evento, pois está associado a campanhas existentes. Por favor, desvincule ou atualize essas campanhas antes da exclusão.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + 'description' => 'Descrição', + 'date' => 'Data', + ], + 'create' => [ + 'title' => 'Adicionar Evento', + 'name' => 'Nome', + 'date' => 'Data', + 'description' => 'Descrição', + 'save-btn' => 'Salvar Evento', + ], + 'edit' => [ + 'title' => 'Editar Evento', + ], + ], + ], + 'campaigns' => [ + 'index' => [ + 'create-btn' => 'Adcionar Campanha', + 'title' => 'Campanhas', + 'create-success' => 'Campanha adicionada com sucesso.', + 'update-success' => 'Campanha atualizada com sucesso.', + 'delete-success' => 'Campanha excluída com sucesso.', + 'delete-failed' => 'Não é possível excluir a campanha.', + 'mass-delete-success' => 'Campanhas excluídas com sucesso', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Nome', + 'subject' => 'Assunto', + 'status' => 'Status', + 'active' => 'Ativo', + 'inactive' => 'Inativo', + 'edit' => 'Editar', + 'delete' => 'Excluir', + ], + 'create' => [ + 'title' => 'Adicionar Campanha', + 'name' => 'Nome', + 'type' => 'Tipo', + 'subject' => 'Assunto', + 'event' => 'Evento', + 'email-template' => 'Modelo de E-mail', + 'status' => 'Status', + ], + 'edit' => [ + 'title' => 'Editar Campanha', + ], + ], + ], + ], + 'tags' => [ + 'index' => [ + 'create-btn' => 'Adicionar Tag', + 'title' => 'Tags', + 'create-success' => 'Tag adicionada com sucesso.', + 'update-success' => 'Tag atualizada com sucesso.', + 'delete-success' => 'Tag excluída com sucesso.', + 'delete-failed' => 'Não é possível excluir a Tag.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'id' => 'ID', + 'name' => 'Nome', + 'users' => 'Usuários', + 'created-at' => 'Criado Em', + ], + 'create' => [ + 'name' => 'Nome', + 'save-btn' => 'Salvar Tag', + 'title' => 'Adicionar Tag', + 'color' => 'Cor', + ], + 'edit' => [ + 'title' => 'Editar Tag', + ], + ], + ], + 'users' => [ + 'index' => [ + 'create-btn' => 'Adicionar Usuário', + 'create-success' => 'Usuário adicionado com sucesso.', + 'delete-failed' => 'Não foi possível excluir o usuário.', + 'delete-success' => 'Usuário excluído com sucesso.', + 'last-delete-error' => 'É necessário pelo menos um usuário.', + 'mass-delete-failed' => 'Não foi possível excluir os usuários.', + 'mass-delete-success' => 'Usuários excluídos com sucesso.', + 'mass-update-failed' => 'Não foi possível atualizar os usuários.', + 'mass-update-success' => 'Usuários atualizados com sucesso.', + 'title' => 'Usuários', + 'update-success' => 'Usuário atualizado com sucesso.', + 'user-define-error' => 'Não é possível excluir o usuário do sistema.', + 'active' => 'Ativo', + 'inactive' => 'Inativo', + 'datagrid' => [ + 'active' => 'Ativo', + 'created-at' => 'Criado Em', + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'email' => 'E-mail', + 'id' => 'ID', + 'inactive' => 'Inativo', + 'name' => 'Nome', + 'status' => 'Status', + 'update-status' => 'Atualizar Status', + 'group' => 'Grupo Associado', + 'no-group' => 'Sem Grupo', + 'users' => 'Usuários', + ], + 'create' => [ + 'confirm-password' => 'Confirmar Senha', + 'email' => 'E-mail', + 'general' => 'Geral', + 'global' => 'Global', + 'group' => 'Grupo', + 'individual' => 'Individual', + 'name' => 'Nome', + 'password' => 'Senha', + 'permission' => 'Permissão', + 'role' => 'Função', + 'save-btn' => 'Salvar Usuário', + 'status' => 'Status', + 'title' => 'Adicionar Usuário', + 'view-permission' => 'Visualizar Permissão', + 'select-at-lest-one-group' => 'Select at least one group', + ], + 'edit' => [ + 'title' => 'Editar Usuário', + ], + ], + ], + 'pipelines' => [ + 'index' => [ + 'title' => 'Funis', + 'create-btn' => 'Adicionar Funil', + 'create-success' => 'Funil adicionado com sucesso.', + 'update-success' => 'Funil atualizado com sucesso.', + 'default-required' => 'É necessário pelo menos um pipeline padrão.', + 'delete-success' => 'Funil excluído com sucesso.', + 'delete-failed' => 'Não foi possível excluir o funil.', + 'default-delete-error' => 'Não é possível excluir o funil padrão.', + 'datagrid' => [ + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'id' => 'ID', + 'is-default' => 'É Padrão', + 'name' => 'Nome', + 'no' => 'Não', + 'rotten-days' => 'Dias parado nesta etapa', + 'yes' => 'Sim', + ], + ], + 'create' => [ + 'title' => 'Adicionar Funil', + 'save-btn' => 'Salvar Funil', + 'name' => 'Nome', + 'rotten-days' => 'Dias parado nesta etapa', + 'mark-as-default' => 'Marcar como Padrão', + 'general' => 'Geral', + 'probability' => 'Probabilidade (%)', + 'new-stage' => 'Novo', + 'won-stage' => 'Ganho', + 'lost-stage' => 'Perdido', + 'stage-btn' => 'Adicionar Estágio', + 'stages' => 'Estágios', + 'duplicate-name' => 'O campo Nome não pode ser duplicado', + 'delete-stage' => 'Excluir Estágio', + 'add-new-stages' => 'Adicionar Novos Estágios', + 'add-stage-info' => 'Adicionar novo estágio para o seu Funil', + 'newly-added' => 'Adicionado Recentemente', + 'stage-delete-success' => 'Estágio excluído com sucesso', + ], + 'edit' => [ + 'title' => 'Editar Funil', + 'save-btn' => 'Salvar Funil', + 'name' => 'Nome', + 'rotten-days' => 'Dias parado nesta etapa', + 'mark-as-default' => 'Marcar como Padrão', + 'general' => 'Geral', + 'probability' => 'Probabilidade (%)', + 'new-stage' => 'Novo', + 'won-stage' => 'Ganho', + 'lost-stage' => 'Perdido', + 'stage-btn' => 'Adicionar Estágio', + 'stages' => 'Estágios', + 'duplicate-name' => 'O campo Nome não pode ser duplicado', + 'delete-stage' => 'Excluir Estágio', + 'add-new-stages' => 'Adicionar Novos Estágios', + 'add-stage-info' => 'Adicionar novo estágio para o seu Funil', + 'stage-delete-success' => 'Estágio excluído com sucesso', + ], + ], + 'webhooks' => [ + 'index' => [ + 'title' => 'Webhooks', + 'create-btn' => 'Adicionar Webhook', + 'create-success' => 'Webhook adicionado com sucesso.', + 'update-success' => 'Webhook atualizado com sucesso.', + 'delete-success' => 'Webhook deletado com sucesso.', + 'delete-failed' => 'Webhook não pode ser deletado.', + 'invalid-endpoint' => 'A URL do endpoint do webhook deve ser um endereço HTTP ou HTTPS público.', + 'datagrid' => [ + 'id' => 'ID', + 'delete' => 'Deletar', + 'edit' => 'Editar', + 'name' => 'Nome', + 'entity-type' => 'Tipo de Entidade', + 'end-point' => 'Ponto de Acesso', + ], + ], + 'create' => [ + 'title' => 'Adcionar Webhook', + 'save-btn' => 'Salvar Webhook', + 'info' => 'Digite os detalhes dos webhooks', + 'url-and-parameters' => 'URL e Parâmetros', + 'method' => 'Método', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'Endpoint da URL', + 'parameters' => 'Parâmetros', + 'add-new-parameter' => 'Adicionar Novo Parâmetro', + 'url-preview' => 'Pré-visualização da URL:', + 'headers' => 'Cabeçalhos', + 'add-new-header' => 'Adicionar Novo Cabeçalho', + 'body' => 'Corpo', + 'default' => 'Padrão', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Chave e Valor', + 'add-new-payload' => 'Adicionar novo payload', + 'raw' => 'Raw', + 'general' => 'Geral', + 'name' => 'Nome', + 'entity-type' => 'Tipo de Entidade', + 'insert-placeholder' => 'Inserir Placeholder', + 'description' => 'Descrição', + 'json' => 'Json', + 'text' => 'Texto', + ], + 'edit' => [ + 'title' => 'Editar Webhook', + 'edit-btn' => 'Salvar Webhook', + 'save-btn' => 'Salvar Webhook', + 'info' => 'Digite os detalhes dos webhooks', + 'url-and-parameters' => 'URL e Parâmetros', + 'method' => 'Método', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'Endpoint da URL', + 'parameters' => 'Parâmetros', + 'add-new-parameter' => 'Adicionar Novo Parâmetro', + 'url-preview' => 'Pré-visualização da URL:', + 'headers' => 'Cabeçalhos', + 'add-new-header' => 'Adicionar Novo Cabeçalho', + 'body' => 'Corpo', + 'default' => 'Padrão', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Chave e Valor', + 'add-new-payload' => 'Adicionar novo payload', + 'raw' => 'Raw', + 'general' => 'Geral', + 'name' => 'Nome', + 'entity-type' => 'Tipo de Entidade', + 'insert-placeholder' => 'Inserir Placeholder', + 'description' => 'Descrição', + 'json' => 'Json', + 'text' => 'Texto', + ], + ], + 'warehouses' => [ + 'index' => [ + 'title' => 'Depósitos', + 'create-btn' => 'Adicionar Depósito', + 'create-success' => 'Depósito adicionado com sucesso.', + 'name-exists' => 'Nome do depósito já existe.', + 'update-success' => 'Depósito atualizado com sucesso.', + 'delete-success' => 'Depósito deletado com sucesso.', + 'delete-failed' => 'Depósito não pode ser deletado.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Nome', + 'contact-name' => 'Nome de Contato', + 'delete' => 'Deletar', + 'edit' => 'Editar', + 'view' => 'Visualizar', + 'created-at' => 'Criado em', + 'products' => 'Produtos', + 'contact-emails' => 'Emails de Contato', + 'contact-numbers' => 'Números de Contato', + ], + ], + 'create' => [ + 'title' => 'Adicionar Depósito', + 'save-btn' => 'Salvar Depósito', + 'contact-info' => 'Informações de Contato', + ], + 'edit' => [ + 'title' => 'Editar Depósito', + 'save-btn' => 'Salvar Depósito', + 'contact-info' => 'Informações de Contato', + ], + 'view' => [ + 'all' => 'Todos', + 'notes' => 'Notas', + 'files' => 'Arquivos', + 'location' => 'Localização', + 'change-logs' => 'Registros de Alterações', + 'locations' => [ + 'action' => 'Ação', + 'add-location' => 'Adicionar Localização', + 'create-success' => 'Localização adicionada com sucesso.', + 'delete' => 'Deletar', + 'delete-failed' => 'Localização não pode ser deletada.', + 'delete-success' => 'Localização deletada com sucesso.', + 'name' => 'Nome', + 'save-btn' => 'Salvar', + ], + 'general-information' => [ + 'title' => 'Informações Gerais', + ], + 'contact-information' => [ + 'title' => 'Informações de Contato', + ], + ], + ], + 'attributes' => [ + 'index' => [ + 'title' => 'Atributos', + 'create-btn' => 'Adicionar Atributo', + 'create-success' => 'Atributo criados com sucesso.', + 'update-success' => 'Atributo atualizados com sucesso.', + 'delete-success' => 'Atributo deletados com sucesso.', + 'delete-failed' => 'Atributo não podem ser deletados.', + 'user-define-error' => 'Não é possível deletar atributos do sistema.', + 'mass-delete-failed' => 'Atributos do sistema não podem ser deletados.', + 'datagrid' => [ + 'yes' => 'Sim', + 'no' => 'Não', + 'id' => 'ID', + 'code' => 'Código', + 'name' => 'Nome', + 'entity-type' => 'Tipo de Entidade', + 'type' => 'Tipo', + 'is-default' => 'É Padrão', + 'edit' => 'Editar', + 'delete' => 'Deletar', + 'quick-add' => 'Adicionar Rápido', + 'entity-types' => [ + 'leads' => 'Oportunidades', + 'organizations' => 'Empresas', + 'persons' => 'Pessoas', + 'products' => 'Produtos', + 'quotes' => 'Cotações', + 'warehouses' => 'Depósitos', + ], + 'types' => [ + 'text' => 'Texto', + 'textarea' => 'Área de texto', + 'price' => 'Preço', + 'boolean' => 'Booleano', + 'select' => 'Selecionar', + 'multiselect' => 'Seleção múltipla', + 'checkbox' => 'Caixa de seleção', + 'email' => 'Email', + 'address' => 'Endereço', + 'phone' => 'Telefone', + 'lookup' => 'Busca', + 'datetime' => 'Data e hora', + 'date' => 'Data', + 'image' => 'Imagem', + 'file' => 'Arquivo', + ], + ], + ], + 'create' => [ + 'title' => 'Adicionar Atributo', + 'save-btn' => 'Salvar Atributo', + 'code' => 'Código', + 'name' => 'Nome', + 'quick_add' => 'Adicionar Rápido', + 'entity-type' => 'Tipo de Entidade', + 'type' => 'Tipo', + 'validations' => 'Validações', + 'is-required' => 'É Obrigatório', + 'input-validation' => 'Validação de Entrada', + 'is-unique' => 'É Único', + 'labels' => 'Rótulos', + 'general' => 'Geral', + 'none' => 'Nenhum', + 'numeric' => 'Numérico', + 'decimal' => 'Decimal', + 'url' => 'URL', + 'options' => 'Opções', + 'option-type' => 'Tipo de Opção', + 'lookup-type' => 'Tipo de Pesquisa', + 'add-option' => 'Adicionar Opção', + 'save-option' => 'Salvar Opção', + 'option-name' => 'Nome da Opção', + 'add-attribute-options' => 'Adicionar Opções de Atributo', + 'text' => 'Texto', + 'textarea' => 'Área de Texto', + 'price' => 'Preço', + 'boolean' => 'Verdadeiro ou falso', + 'select' => 'Seleção', + 'multiselect' => 'Multiseleção', + 'email' => 'E-mail', + 'address' => 'Endereço', + 'phone' => 'Telefone', + 'datetime' => 'Data e Hora', + 'date' => 'Data', + 'image' => 'Imagem', + 'file' => 'Arquivo', + 'lookup' => 'Pesquisa', + 'entity_type' => 'Tipo de Entidade', + 'checkbox' => 'Caixa de Seleção', + 'is_required' => 'É Obrigatório', + 'is_unique' => 'É Único', + 'actions' => 'Ações', + ], + 'edit' => [ + 'actions' => 'Ações', + 'add-attribute-options' => 'Adicionar Opções de Atributo', + 'add-option' => 'Adicionar Opção', + 'address' => 'Endereço', + 'boolean' => 'Verdadeiro ou falso', + 'checkbox' => 'Caixa de Seleção', + 'code' => 'Código', + 'date' => 'Data', + 'datetime' => 'Data e Hora', + 'decimal' => 'Decimal', + 'email' => 'E-mail', + 'entity-type' => 'Tipo de Entidade', + 'entity_type' => 'Tipo de Entidade', + 'file' => 'Arquivo', + 'general' => 'Geral', + 'image' => 'Imagem', + 'input-validation' => 'Validação de Entrada', + 'is-required' => 'É Obrigatório', + 'is-unique' => 'É Único', + 'is_required' => 'É Obrigatório', + 'is_unique' => 'É Único', + 'labels' => 'Rótulos', + 'lookup' => 'Pesquisa', + 'lookup-type' => 'Tipo de Pesquisa', + 'multiselect' => 'Multiseleção', + 'name' => 'Nome', + 'quick_add' => 'Adicionar Rápido', + 'none' => 'Nenhum', + 'numeric' => 'Numérico', + 'option-deleted' => 'Opção de Atributo eliminada com sucesso', + 'option-name' => 'Nome da Opção', + 'option-type' => 'Tipo de Opção', + 'options' => 'Opções', + 'phone' => 'Telefone', + 'price' => 'Preço', + 'save-btn' => 'Salvar Atributo', + 'save-option' => 'Salvar Opção', + 'select' => 'Seleção', + 'text' => 'Texto', + 'textarea' => 'Área de Texto', + 'title' => 'Editar Atributo', + 'type' => 'Tipo', + 'url' => 'URL', + 'validations' => 'Validações', + ], + ], + 'data-transfer' => [ + 'importers' => [ + 'persons' => [ + 'title' => 'Pessoas', + 'validation' => [ + 'errors' => [ + 'duplicate-email' => 'E-mail : \'%s\' é encontrado mais de uma vez no arquivo de importação.', + 'duplicate-phone' => 'Telefone : \'%s\' é encontrado mais de uma vez no arquivo de importação.', + 'email-not-found' => 'E-mail : \'%s\' não foi encontrado no sistema.', + 'invalid-date' => 'Data : \'%s\' não é uma data válida.', + ], + ], + ], + 'products' => [ + 'title' => 'Produtos', + 'validation' => [ + 'errors' => [ + 'sku-not-found' => 'Produto com este código não foi encontrado', + ], + ], + ], + 'leads' => [ + 'title' => 'Oportunidades', + 'validation' => [ + 'errors' => [ + 'id-not-found' => 'ID : \'%s\' não foi encontrado no sistema.', + ], + ], + ], + ], + 'validation' => [ + 'errors' => [ + 'column-empty-headers' => 'As colunas de número "%s" têm cabeçalhos vazios.', + 'column-name-invalid' => 'Nomes de colunas inválidos: "%s".', + 'column-not-found' => 'Colunas obrigatórias não encontradas: %s.', + 'column-numbers' => 'O número de colunas não corresponde ao número de linhas no cabeçalho.', + 'invalid-attribute' => 'O cabeçalho contém atributo(s) inválido(s): "%s".', + 'system' => 'Ocorreu um erro inesperado no sistema.', + 'wrong-quotes' => 'Aspas curvas usadas em vez de aspas retas.', + 'already-exists' => 'O :attribute já existe.', + ], + ], + 'imports' => [ + 'create' => [ + 'action' => 'Action', + 'allowed-errors' => 'Allowed Errors', + 'back-btn' => 'Back', + 'create-update' => 'Create/Update', + 'delete' => 'Delete', + 'download-sample' => 'Download Sample', + 'field-separator' => 'Field Separator', + 'file' => 'File', + 'general' => 'General', + 'images-directory' => 'Images Directory Path', + 'process-in-queue' => 'Process In Queue', + 'results' => 'Results', + 'save-btn' => 'Save Import', + 'settings' => 'Settings', + 'skip-errors' => 'Skip Errors', + 'stop-on-errors' => 'Stop on Errors', + 'title' => 'Create Import', + 'type' => 'Type', + 'validation-strategy' => 'Validation Strategy', + ], + 'edit' => [ + 'action' => 'Action', + 'allowed-errors' => 'Allowed Errors', + 'back-btn' => 'Back', + 'create-update' => 'Create/Update', + 'delete' => 'Delete', + 'download-sample' => 'Download Sample', + 'field-separator' => 'Field Separator', + 'file' => 'File', + 'general' => 'General', + 'images-directory' => 'Images Directory Path', + 'process-in-queue' => 'Process In Queue', + 'results' => 'Results', + 'save-btn' => 'Save Import', + 'settings' => 'Settings', + 'skip-errors' => 'Skip Errors', + 'stop-on-errors' => 'Stop on Errors', + 'title' => 'Edit Import', + 'type' => 'Type', + 'validation-strategy' => 'Validation Strategy', + ], + 'index' => [ + 'button-title' => 'Create Import', + 'title' => 'Imports', + 'datagrid' => [ + 'actions' => 'Actions', + 'completed-at' => 'Completed At', + 'created' => 'Created', + 'delete' => 'Delete', + 'deleted' => 'Deleted', + 'edit' => 'Edit', + 'error-file' => 'Error File', + 'id' => 'ID', + 'started-at' => 'Started At', + 'state' => 'State', + 'summary' => 'Summary', + 'type' => 'Type', + 'updated' => 'Updated', + 'uploaded-file' => 'Uploaded File', + ], + ], + 'import' => [ + 'back-btn' => 'Back', + 'completed-batches' => 'Total Batches Completed:', + 'download-error-report' => 'Download Full Report', + 'edit-btn' => 'Edit', + 'imported-info' => 'Congratulations! Your import was successful.', + 'importing-info' => 'Import In Process', + 'importing-queue-hint' => 'Se esta importação permanecer em 0%, o worker da fila pode não estar em execução. Peça ao administrador para executar \'php artisan queue:work\', ou desative \'Processar na fila\' e importe novamente.', + 'indexing-info' => 'Resources Indexing (Price, Inventory and Elastic Search) In Progress', + 'linking-info' => 'Resources Linking In Progress', + 'progress' => 'Progress:', + 'title' => 'Import', + 'total-batches' => 'Total Batches:', + 'total-created' => 'Total Records Created:', + 'total-deleted' => 'Total Records Deleted:', + 'total-errors' => 'Total Errors:', + 'total-invalid-rows' => 'Total Invalid Rows:', + 'total-rows-processed' => 'Total Rows Processed:', + 'total-updated' => 'Total Records Updated:', + 'validate' => 'Validate', + 'validate-info' => 'Click on Validate Data to check your import.', + 'validating-info' => 'The data started reading and Validating', + 'validation-failed-info' => 'Your import is invalid. Please fix the following errors and try again.', + 'validation-success-info' => 'Your import is valid. Click on Import to start the import process.', + ], + 'create-success' => 'Import created successfully.', + 'delete-failed' => 'Import deletion failed unexpectedly.', + 'delete-success' => 'Import deleted successfully.', + 'not-valid' => 'Import is invalid', + 'nothing-to-import' => 'There are no resources to import.', + 'queue-worker-not-running' => 'O worker da fila não está em execução. Execute php artisan queue:work para processar esta importação, ou desative Processar na fila e tente novamente.', + 'setup-queue-error' => 'Please change your queue driver to "database" or "redis" to start the import process.', + 'update-success' => 'Import updated successfully.', + ], + ], + ], + 'activities' => [ + 'index' => [ + 'title' => 'Atividades', + 'datagrid' => [ + 'comment' => 'Comentário', + 'created_at' => 'Criado Em', + 'created_by' => 'Criado Por', + 'edit' => 'Editar', + 'id' => 'ID', + 'done' => 'Concluído', + 'not-done' => 'Não Concluído', + 'lead' => 'Oportunidade', + 'mass-delete' => 'Excluir em Massa', + 'mass-update' => 'Atualizar em Massa', + 'schedule-from' => 'Agendado de', + 'schedule-to' => 'Agendado até', + 'schedule_from' => 'Agendado de', + 'schedule_to' => 'Agendado até', + 'title' => 'Título', + 'is_done' => 'Está Concluído', + 'type' => 'Tipo', + 'update' => 'Atualizar', + 'call' => 'Chamada', + 'meeting' => 'Reunião', + 'lunch' => 'Almoço', + ], + ], + 'edit' => [ + 'title' => 'Editar Atividade', + 'back-btn' => 'Voltar', + 'save-btn' => 'Salvar Atividade', + 'type' => 'Tipo de Atividade', + 'call' => 'Chamada', + 'meeting' => 'Reunião', + 'lunch' => 'Almoço', + 'schedule_to' => 'Agendado até', + 'schedule_from' => 'Agendado de', + 'location' => 'Localização', + 'comment' => 'Comentário', + 'lead' => 'Oportunidade', + 'participants' => 'Participantes', + 'general' => 'Geral', + 'persons' => 'Pessoas', + 'no-result-found' => 'Nenhum registro encontrado.', + 'users' => 'Usuários', + ], + 'updated' => 'Atualizado :attribute', + 'created' => 'Criado', + 'duration-overlapping' => 'Os participantes têm outra reunião neste horário. Deseja continuar?', + 'create-success' => 'Atividade adicionada com sucesso.', + 'update-success' => 'Atividade atualizada com sucesso.', + 'overlapping-error' => 'Os participantes têm outra reunião neste horário.', + 'destroy-success' => 'Atividade deletada com sucesso.', + 'delete-failed' => 'A atividade não pode ser deletada.', + 'mass-update-success' => 'Atividades atualizadas com sucesso.', + 'mass-destroy-success' => 'Activities deleted successfully.', + 'mass-delete-failed' => 'Activities can not be deleted.', + ], + 'mail' => [ + 'index' => [ + 'compose' => 'Escrever', + 'draft' => 'Rascunho', + 'inbox' => 'Caixa de Entrada', + 'outbox' => 'Caixa de Saída', + 'sent' => 'Enviado', + 'trash' => 'Lixeira', + 'compose-mail-btn' => 'Escrever E-mail', + 'btn' => 'E-mail', + 'mail' => [ + 'title' => 'Escrever E-mail', + 'to' => 'Para', + 'enter-emails' => 'Pressione Enter para adicionar e-mails', + 'cc' => 'Cópia', + 'bcc' => 'Cópia oculta', + 'subject' => 'Assunto', + 'send-btn' => 'Enviar', + 'message' => 'Mensagem', + 'draft' => 'Rascunho', + ], + 'datagrid' => [ + 'id' => 'ID', + 'from' => 'De', + 'to' => 'Para', + 'subject' => 'Assunto', + 'tags' => 'Tags', + 'content' => 'Content', + 'attachments' => 'Attachments', + 'date' => 'Date', + 'move-to-inbox' => 'Movido para Caixa de Entrada', + 'move-to-trash' => 'Movido para a lixeira', + 'edit' => 'Editar', + 'view' => 'Visualizar', + 'delete' => 'Excluir', + ], + ], + 'create-success' => 'E-mail enviado com sucesso.', + 'update-success' => 'E-mail atualizado com sucesso.', + 'mass-update-success' => 'E-mails atualizados com sucesso.', + 'delete-success' => 'E-mail excluído com sucesso.', + 'delete-failed' => 'E-mail não pode ser excluído.', + 'invalid-route' => 'Rota inválida para o e-mail.', + 'unauthorized' => 'Esta ação não está autorizada.', + 'view' => [ + 'title' => 'E-mails', + 'subject' => ':subject', + 'link-mail' => 'Link do E-mail', + 'to' => 'Para', + 'cc' => 'Cópia', + 'bcc' => 'Cópia oculta', + 'reply' => 'Responder', + 'reply-all' => 'Responder a Todos', + 'forward' => 'Encaminhar', + 'delete' => 'Excluir', + 'enter-mails' => 'Digite o id do e-mail', + 'rotten-days' => 'O negócio está sem movimentação há :days dias', + 'search-an-existing-lead' => 'Pesquisar um negócio existente', + 'search-an-existing-contact' => 'Pesquisar um contato existente', + 'message' => 'Mensagem', + 'add-attachments' => 'Adicionar Anexos', + 'discard' => 'Descartar', + 'send' => 'Enviar', + 'no-result-found' => 'Nenhum resultado encontrado', + 'add-new-contact' => 'Adicionar Novo Contato', + 'description' => 'Descrição', + 'search' => 'Pesquisar...', + 'add-new-lead' => 'Adicionar Nova Oportunidade', + 'create-new-contact' => 'Adicionar Novo Contato', + 'save-contact' => 'Salvar Contato', + 'create-lead' => 'Adicionar Oportunidade', + 'linked-contact' => 'Contato Vinculado', + 'link-to-contact' => 'Vincular ao Contato', + 'link-to-lead' => 'Vincular a uma Oportunidade', + 'linked-lead' => 'Oportunidade Vinculado', + 'lead-details' => 'Detalhes da Oportunidade', + 'contact-person' => 'Pessoa de Contato', + 'product' => 'Produto', + 'tags' => [ + 'create-success' => 'Tag adicionada com sucesso.', + 'destroy-success' => 'Tag excluída com sucesso.', + ], + ], + ], + 'common' => [ + 'custom-attributes' => [ + 'add-more' => 'Adicionar Mais', + 'address' => 'Endereço', + 'city' => 'Cidade', + 'contact' => 'Números de Contato', + 'country' => 'País', + 'email' => 'E-mail', + 'home' => 'Casa', + 'postcode' => 'CEP', + 'save' => 'Salvar', + 'select' => 'Selecionar', + 'select-country' => 'Selecionar País', + 'select-state' => 'Selecionar Estado', + 'state' => 'Estado', + 'update-contact-title' => 'Atualizar Números de Contato', + 'update-emails-title' => 'Atualizar E-mails de Contato', + 'work' => 'Trabalho', + ], + ], + 'leads' => [ + 'create-success' => 'Negócio adicionado com sucesso.', + 'update-success' => 'Negócio atualizado com sucesso.', + 'update-failed' => 'Leads can not be deleted.', + 'destroy-success' => 'Negócio excluído com sucesso.', + 'destroy-failed' => 'Este negócio não pode ser excluído.', + 'file' => [ + 'data-not-found' => 'Dados não encontrados.', + 'empty-content' => 'O conteúdo do PDF está vazio ou não pôde ser extraído.', + 'failed-extract' => 'Falha ao extrair texto do arquivo.', + 'insufficient-info' => 'Devido a dados insuficientes, não podemos processar sua solicitação no momento.', + 'invalid-base64' => 'Formato base64 inválido.', + 'invalid-format' => 'Formato JSON inválido.', + 'invalid-response' => 'Formato de resposta de IA inválido.', + 'missing-api-key' => 'Chave API ou configuração do modelo ausente.', + 'not-found' => 'Arquivo não encontrado.', + 'recursive-call' => 'Chamada recursiva detectada.', + 'text-generation-failed' => 'Falha na extração de texto. O arquivo pode estar vazio ou ilegível.', + ], + 'index' => [ + 'title' => 'Oportunidades', + 'create-btn' => 'Adicionar Negócio', + 'datagrid' => [ + 'id' => 'ID', + 'sales-person' => 'Vendedor', + 'subject' => 'Assunto', + 'source' => 'Origem', + 'lead-value' => 'Valor Estimado do Negócio', + 'lead-type' => 'Tipo de Negócio', + 'tag-name' => 'Nome da Tag', + 'contact-person' => 'Pessoa de Contato', + 'stage' => 'Etapa', + 'rotten-lead' => 'Negócio estagnado', + 'date-to' => 'Data esperada de fechamento', + 'created-at' => 'Criado em', + 'no' => 'Não', + 'yes' => 'Sim', + 'delete' => 'Excluir', + 'mass-delete' => 'Excluir em Massa', + 'mass-update' => 'Atualizar em Massa', + ], + 'kanban' => [ + 'rotten-days' => 'Negócio estagnado por :days dias', + 'empty-list' => 'Sua lista de Negócios está vazia', + 'empty-list-description' => 'Adicione um negócio para organizar seus objetivos.', + 'create-lead-btn' => 'Adicionar Negócio', + 'columns' => [ + 'contact-person' => 'Pessoa de Contato', + 'id' => 'ID', + 'lead-type' => 'Tipo de Negócio', + 'lead-value' => 'Valor Estimado do Negócio', + 'sales-person' => 'Vendedor', + 'source' => 'Origem', + 'title' => 'Título', + 'tags' => 'Tags', + 'expected-close-date' => 'Data esperada de fechamento', + 'date-to' => 'Data esperada de fechamento', + 'created-at' => 'Criado em', + ], + 'toolbar' => [ + 'search' => [ + 'title' => 'Buscar por título', + ], + 'filters' => [ + 'apply-filters' => 'Aplicar Filtros', + 'clear-all' => 'Limpar Tudo', + 'filter' => 'Filtrar', + 'filters' => 'Filtros', + 'from' => 'De', + 'select' => 'Selecionar', + 'to' => 'Para', + ], + 'card-settings' => [ + 'title' => 'Configurações do Cartão', + 'contact-person' => 'Pessoa de Contato', + 'rotten-indicator' => 'Indicador de Estagnação', + 'sales-person' => 'Vendedor', + 'estimated-lead-value' => 'Valor Estimado do Negócio', + 'source' => 'Origem', + 'lead-type' => 'Tipo de Negócio', + 'tags' => 'Tags', + ], + ], + 'stages' => [ + 'won' => 'Ganho', + 'lost' => 'Perdido', + 'need-more-info' => 'Precisa de Mais Informações', + 'closed-at' => 'Fechado em', + 'won-value' => 'Valor Ganhado', + 'lost-reason' => 'Motivo da Perda', + 'save-btn' => 'Salvar', + ], + ], + 'view-switcher' => [ + 'all-pipelines' => 'Todos os Funis', + 'create-new-pipeline' => 'Adicionar Novo Funil', + ], + 'upload' => [ + 'create-lead' => 'Adicionar Negócio Usando AI', + 'file' => 'Upload de arquivo', + 'file-info' => 'Apenas arquivos nos formatos pdf, bmp, jpg, jpeg, png são aceitos.', + 'file-required' => 'Por favor, selecione pelo menos um arquivo válido para prosseguir.', + 'save-btn' => 'Salvar', + 'upload-file' => 'Enviar arquivo', + ], + ], + 'create' => [ + 'title' => 'Adicionar Negócio', + 'save-btn' => 'Salvar', + 'details' => 'Detalhes', + 'details-info' => 'Coloque as informações básicas do Negócio', + 'contact-person' => 'Pessoa de Contato', + 'contact-info' => 'Informações sobre a Pessoa de Contato', + 'products' => 'Produtos', + 'products-info' => 'Informações sobre os Produtos', + ], + 'edit' => [ + 'title' => 'Editar Negócio', + 'save-btn' => 'Salvar', + 'details' => 'Detalhes', + 'details-info' => 'Coloque as informações básicas do Negócio', + 'contact-person' => 'Pessoa de Contato', + 'contact-info' => 'Informações sobre a Pessoa de Contato', + 'products' => 'Produtos', + 'products-info' => 'Informações sobre os Produtos', + ], + 'common' => [ + 'contact' => [ + 'name' => 'Nome', + 'name-search-placeholder' => 'Buscar por nombre, correo electrónico y número', + 'email' => 'E-mail', + 'contact-number' => 'Número de Contato', + 'organization' => 'Empresa', + ], + 'products' => [ + 'product-name' => 'Nome do Produto', + 'quantity' => 'Quantidade', + 'price' => 'Preço', + 'amount' => 'Valor', + 'action' => 'Ação', + 'add-more' => 'Adicionar Mais', + 'total' => 'Total', + ], + ], + 'view' => [ + 'title' => 'Negócio: :title', + 'rotten-days' => ':days Dias', + 'tabs' => [ + 'description' => 'Descrição', + 'products' => 'Produtos', + 'quotes' => 'Cotações', + ], + 'attributes' => [ + 'title' => 'Sobre o Negócio', + ], + 'quotes' => [ + 'id' => 'ID', + 'subject' => 'Assunto', + 'expired-at' => 'Expirado em', + 'sub-total' => 'Subtotal', + 'discount' => 'Desconto', + 'tax' => 'Imposto', + 'adjustment' => 'Ajuste', + 'grand-total' => 'Total Geral', + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'download' => 'Baixar', + 'destroy-success' => 'Cotação excluída com sucesso.', + 'empty-title' => 'Nenhuma Cotação Encontrada', + 'empty-info' => 'Nenhuma Cotação Encontrada para este Negócio', + 'add-btn' => 'Adicionar Cotação', + 'mail' => 'Email', + 'quote-not-found' => 'Cotação não encontrada para este lead.', + 'person-email-unavailable' => 'O e-mail da pessoa do lead não está disponível.', + 'mail-subject' => 'Cotação: :subject', + 'mail-body' => 'Please find the attached quote PDF. Mail Generated by System.', + 'mail-send-failed' => 'Não foi possível enviar o e-mail da cotação.', + ], + 'products' => [ + 'product-name' => 'Nome do Produto', + 'quantity' => 'Quantidade', + 'price' => 'Preço', + 'amount' => 'Valor', + 'action' => 'Ação', + 'add-more' => 'Adicionar Mais', + 'total' => 'Total', + 'empty-title' => 'Nenhum Produto Encontrado', + 'empty-info' => 'Nenhum Produto Encontrado para este Negócio', + 'add-product' => 'Adicionar Produto', + ], + 'persons' => [ + 'title' => 'Sobre as Pessoas', + 'job-title' => ':job_title em :organization', + 'no-person' => 'Nenhum contato foi atribuído a este lead.', + 'attach-btn' => 'Anexar Pessoa de Contato', + 'change-btn' => 'Alterar', + 'change-title' => 'Alterar Pessoa de Contato', + 'save-btn' => 'Salvar', + 'cancel-btn' => 'Cancelar', + ], + 'stages' => [ + 'won' => 'Ganho', + 'lost' => 'Perdido', + 'need-more-info' => 'Precisa de Mais Informações', + 'closed-at' => 'Fechado em', + 'won-value' => 'Valor Ganhado', + 'lost-reason' => 'Motivo da Perda', + 'save-btn' => 'Salvar', + ], + 'tags' => [ + 'create-success' => 'Tag adicionada com sucesso.', + 'destroy-success' => 'Tag excluída com sucesso.', + ], + ], + ], + 'configuration' => [ + 'index' => [ + 'back' => 'Voltar', + 'delete' => 'Excluir', + 'save-btn' => 'Salvar Configuração', + 'save-success' => 'Configuração Salva com Sucesso.', + 'search' => 'Pesquisar', + 'select-country' => 'Selecionar País', + 'select-state' => 'Selecionar Estado', + 'title' => 'Configuração', + 'general' => [ + 'title' => 'Geral', + 'info' => 'Configuração Geral', + 'general' => [ + 'title' => 'Geral', + 'info' => 'Atualize suas configurações gerais aqui.', + 'locale-settings' => [ + 'title' => 'Configurações de Idioma', + 'title-info' => 'Define o idioma usado na interface do painel administrativo. A lista suspensa exibe todos os idiomas disponíveis na sua instalação.', + ], + 'admin-logo' => [ + 'logo-image' => 'Imagem do Logo', + 'title' => 'Logo do Admin', + 'title-info' => 'Configure a imagem do logo para o seu painel de administração.', + 'favicon-image' => 'Imagem do Favicon', + 'favicon-image-info' => 'Configure a imagem do favicon para o seu painel de administração.', + ], + ], + 'settings' => [ + 'title' => 'Configurações', + 'info' => 'Atualize suas configurações aqui.', + 'footer' => [ + 'show' => 'Exibir "Desenvolvido por"', + 'show-info' => 'Exibe ou oculta a barra "Desenvolvido por" na parte inferior do painel administrativo.', + 'info' => 'Podemos configurar a seção "Desenvolvido por" aqui.', + 'powered-by' => 'Editor de texto "Desenvolvido por"', + 'title' => 'Configurações da Seção "Desenvolvido por"', + ], + 'menu' => [ + 'activities' => 'Atividades', + 'configuration' => 'Configuração', + 'contacts' => 'Contatos', + 'dashboard' => 'Painel', + 'draft' => 'Rascunho', + 'inbox' => 'Caixa de Entrada', + 'info' => 'Podemos configurar os nomes dos itens de menu aqui.', + 'leads' => 'Leads', + 'mail' => 'E-mail', + 'organizations' => 'Empresas', + 'outbox' => 'Caixa de Saída', + 'persons' => 'Pessoas', + 'products' => 'Produtos', + 'quotes' => 'Cotações', + 'sent' => 'Enviados', + 'settings' => 'Configurações', + 'title' => 'Configurações dos Itens de Menu', + 'trash' => 'Lixeira', + ], + 'dashboard' => [ + 'title' => 'Configurações do Painel', + 'info' => 'Aqui podemos configurar o intervalo de datas padrão do painel.', + 'date-range' => 'Intervalo de Datas Padrão', + 'date-range-info' => 'O período com que o painel é carregado. Os seletores de data no painel ainda podem ser ajustados para uma visualização pontual.', + 'custom-days' => 'Intervalo Personalizado (Dias)', + 'custom-days-info' => 'Número de dias a consultar quando o intervalo de datas for Personalizado.', + 'ranges' => [ + '1-month' => '1 Mês', + '3-months' => '3 Meses', + '9-months' => '9 Meses', + '1-year' => '1 Ano', + '2-years' => '2 Anos', + 'custom' => 'Personalizado', + ], + ], + 'menu-color' => [ + 'brand-color' => 'Cor da Marca', + 'info' => 'Podemos alterar as cores dos itens de menu aqui.', + 'title' => 'Configurações de Cor dos Itens de Menu', + ], + ], + ], + 'email' => [ + 'title' => 'Configurações de E-mail', + 'info' => 'Configuração de e-mail para a aplicação.', + 'imap' => [ + 'title' => 'Configurações IMAP', + 'info' => 'Configuração de e-mail IMAP para receber emails.', + 'account' => [ + 'title' => 'Conta IMAP', + 'title-info' => 'Configure as configurações da sua conta IMAP aqui.', + 'host' => 'Host', + 'port' => 'Porta', + 'encryption' => 'Tipo de Criptografia', + 'validate-cert' => 'Validar Certificado SSL', + 'username' => 'Nome de Usuário IMAP', + 'password' => 'Senha IMAP', + ], + ], + ], + 'magic-ai' => [ + 'title' => 'Magic AI', + 'info' => 'Configuração do Magic AI para a aplicação.', + 'settings' => [ + 'api-key' => 'Chave API', + 'api-key-info' => 'Lembre-se de usar uma chave API do OpenRouter para cada modelo. É um passo simples para melhorar a segurança e o desempenho.', + 'enable' => 'Habilitar', + 'info' => 'Melhore sua experiência com o Magic AI com sua chave API do OpenRouter. Integre-a agora para uma aventura de IA personalizada e perfeita, feita sob medida para você! Personalize as configurações com facilidade e assuma o controle da sua jornada de IA.', + 'other' => 'Outro Modelo', + 'other-model' => 'Para outros modelos, use o ID do Modelo do OpenRouter.', + 'doc-generation' => 'Geração de DOC', + 'doc-generation-info' => 'Ative o recurso de geração de DOC para extrair automaticamente dados de arquivos DOC e convertê-los em formato de texto. Aumente sua produtividade e eficiência ativando este recurso para simplificar seu fluxo de trabalho.', + 'title' => 'Configurações Gerais', + 'models' => [ + 'deepseek-r1' => 'Deepseek R1 Distill-llama-8b', + 'gemini-2-0-flash-001' => 'Gemini 2.0 flash-001', + 'gpt-4o' => 'GPT-4.0', + 'gpt-4o-mini' => 'GPT-4.0 mini', + 'grok-2-1212' => 'Grok 2.12', + 'llama-3-2-3b-instruct' => 'Llama 3.2 3b Instruct', + 'title' => 'Modelos', + ], + ], + ], + ], + ], + 'dashboard' => [ + 'index' => [ + 'title' => 'Início', + 'start-date' => 'Start Date', + 'end-date' => 'End Date', + 'export-pdf' => 'Exportar PDF', + 'pdf' => [ + 'title' => 'Relatório do Painel', + 'filtered-date' => 'Data Filtrada', + 'revenue' => 'Faturamento', + 'overview' => 'Visão Geral', + 'total-leads' => 'Total de negócios', + 'metric' => 'Métrica', + 'current' => 'Atual', + 'previous' => 'Anterior', + 'progress' => 'Progresso', + 'date' => 'Data', + 'all' => 'Todos', + 'total' => 'Total', + 'won' => 'Ganhos', + 'lost' => 'Perdidos', + 'source' => 'Origem', + 'type' => 'Tipo', + 'stage' => 'Estágio', + 'product' => 'Produto', + 'person' => 'Pessoa', + 'price' => 'Preço', + 'revenue-by-sources' => 'Faturamento por origens', + 'revenue-by-types' => 'Faturamento por tipos', + 'open-leads-by-stages' => 'Negócios por estágios', + 'top-selling-products' => 'Produtos mais vendidos', + 'top-persons' => 'Principais pessoas', + ], + 'revenue' => [ + 'lost-revenue' => 'Negócios Perdidos', + 'won-revenue' => 'Negócios Ganhos', + ], + 'over-all' => [ + 'average-lead-value' => 'Ticket médio', + 'total-leads' => 'Total de negócios', + 'average-leads-per-day' => 'Média de negócios por dia', + 'total-quotations' => 'Total de cotações', + 'total-persons' => 'Total de pessoas', + 'total-organizations' => 'Total de empresas', + ], + 'total-leads' => [ + 'title' => 'Negócios', + 'total' => 'Total de negócios', + 'won' => 'Negócios ganhos', + 'lost' => 'Negócios perdidos', + ], + 'revenue-by-sources' => [ + 'title' => 'Faturamento por origens', + 'empty-title' => 'Ainda não há dados', + 'empty-info' => 'Nenhum dado disponível para o intervalo selecionado', + ], + 'revenue-by-types' => [ + 'title' => 'Faturamento por tipos', + 'empty-title' => 'Ainda não há dados', + 'empty-info' => 'Nenhum dado disponível para o intervalo selecionado', + ], + 'top-selling-products' => [ + 'title' => 'Produtos mais vendidos', + 'empty-title' => 'Ainda não há dados', + 'empty-info' => 'Nenhum produto disponível para o intervalo selecionado', + ], + 'top-persons' => [ + 'title' => 'Principais pessoas', + 'empty-title' => 'Ainda não há dados', + 'empty-info' => 'Nenhuma pessoa disponível para o intervalo selecionado', + ], + 'open-leads-by-states' => [ + 'title' => 'Negócios por estágios', + 'empty-title' => 'Ainda não há dados', + 'empty-info' => 'Nenhum dado disponível para o intervalo selecionado', + ], + ], + ], + 'help' => [ + 'index' => [ + 'title' => 'Ajuda e recursos', + 'description' => 'Tudo o que você precisa para aproveitar ao máximo o Krayin Admin — hospedagem, suporte e serviços profissionais, além de extensões e documentação para desenvolvedores.', + 'contact-us' => 'Fale conosco', + 'view-all' => 'Ver tudo', + 'still-need-help-title' => 'Ainda precisa de ajuda?', + 'still-need-help-description' => 'Fale com a equipe Krayin sobre hospedagem, desenvolvimento personalizado ou qualquer outra coisa.', + + 'services' => [ + 'title' => 'Serviços e recursos', + 'cloud-hosting' => [ + 'title' => 'Hospedagem na nuvem', + 'description' => 'Hospedagem na nuvem gerenciada e econômica — experimente e coloque o Krayin na nuvem em minutos, totalmente otimizado e escalável.', + ], + 'support' => [ + 'title' => 'Suporte e manutenção', + 'description' => 'Suporte técnico dedicado e planos de manutenção contínua para manter seu CRM seguro, atualizado e funcionando sem problemas.', + ], + 'extensions' => [ + 'title' => 'Extensões', + 'description' => 'Navegue por complementos oficiais e da comunidade para ampliar o Krayin com novos conectores, canais e recursos.', + ], + ], + + 'resources' => [ + 'title' => 'Documentação', + 'user-docs' => [ + 'title' => 'Guia do usuário', + 'description' => 'Ajuda especializada para integração de módulos, personalização, migração de dados, atualizações de versão e desenvolvimento sob medida.', + ], + 'dev-docs' => [ + 'title' => 'Documentação para desenvolvedores e blogs', + 'description' => 'Guias para desenvolvedores, tutoriais e os artigos mais recentes para ajudar você a criar, configurar e se manter atualizado.', + ], + 'api-docs' => [ + 'title' => 'Documentação da API', + 'description' => 'Referência completa da API REST com endpoints, autenticação e exemplos para integrar o Krayin ao seu stack.', + ], + ], + + 'community' => [ + 'forums' => [ + 'title' => 'Fóruns da comunidade', + 'description' => 'Conecte-se com milhares de outros usuários e desenvolvedores do Krayin para compartilhar dicas e resolver problemas.', + 'link' => 'Participe da comunidade', + ], + 'tutorials' => [ + 'title' => 'Tutoriais em vídeo', + 'description' => 'Assista a guias em vídeo passo a passo sobre como configurar seu CRM, configurar funis e gerenciar usuários.', + 'link' => 'Acessar o canal do YouTube', + ], + ], + ], + ], + 'layouts' => [ + 'app-version' => 'Versão: :version', + 'dashboard' => 'Início', + 'help' => 'Ajuda e recursos', + 'leads' => 'Oportunidades', + 'quotes' => 'Cotações', + 'quote' => 'Cotação', + 'mail' => [ + 'title' => 'E-mail', + 'compose' => 'Escrever', + 'inbox' => 'Caixa de Entrada', + 'draft' => 'Rascunho', + 'outbox' => 'Caixa de Saída', + 'sent' => 'Enviado', + 'trash' => 'Lixeira', + 'setting' => 'Configurações', + ], + 'activities' => 'Atividades', + 'contacts' => 'Contatos', + 'persons' => 'Pessoas', + 'person' => 'Pessoa', + 'organizations' => 'Empresas', + 'organization' => 'Empresa', + 'products' => 'Produtos', + 'product' => 'Produto', + 'settings' => 'Configurações', + 'user' => 'Usuário', + 'user-info' => 'Gerencie todos os seus usuários e suas permissões no CRM, o que eles estão autorizados a fazer.', + 'groups' => 'Grupos', + 'groups-info' => 'Adicionar, editar ou excluir grupos do CRM', + 'roles' => 'Funções', + 'role' => 'Função', + 'roles-info' => 'Adicionar, editar ou excluir funções do CRM', + 'users' => 'Usuários', + 'users-info' => 'Adicionar, editar ou excluir usuários do CRM', + 'lead' => 'Negócio', + 'lead-info' => 'Gerencie todas as configurações relacionadas aos Negócios no CRM', + 'sidebar' => [ + 'collapse' => 'Recolher', + 'expand' => 'Expandir', + ], + 'quick-add' => [ + 'title' => 'Adição rápida', + 'save' => 'Salvar', + 'to' => 'Para', + 'subject' => 'Assunto', + 'message' => 'Mensagem', + ], + 'pipelines' => 'Funis', + 'pipelines-info' => 'Adicionar, editar ou excluir funis do CRM', + 'sources' => 'Origens', + 'sources-info' => 'Adicionar, editar ou excluir origems do CRM', + 'types' => 'Tipos', + 'types-info' => 'Adicionar, editar ou excluir tipos do CRM', + 'automation' => 'Automação', + 'automation-info' => 'Gerencie todas as configurações de automação no CRM', + 'attributes' => 'Atributos', + 'attribute' => 'Atributo', + 'attributes-info' => 'Adicionar, editar ou excluir atributos do CRM', + 'email-templates' => 'Modelos de E-mail', + 'email' => 'E-mail', + 'email-templates-info' => 'Adicionar, editar ou excluir modelos de e-mail do CRM', + 'events' => 'Events', + 'events-info' => 'Add, edit or delete events from CRM', + 'campaigns' => 'Campaigns', + 'campaigns-info' => 'Add, edit or delete campaigns from CRM', + 'workflows' => 'Fluxos de Trabalho', + 'workflows-info' => 'Adicionar, editar ou excluir fluxos de trabalho do CRM', + 'webhooks' => 'Webhooks', + 'webhooks-info' => 'Add, edit or delete webhooks from CRM', + 'other-settings' => 'Outras Configurações', + 'other-settings-info' => 'Gerencie todas as configurações extras no CRM', + 'tags' => 'Tags', + 'tags-info' => 'Adicionar, editar ou excluir tags do CRM', + 'my-account' => 'Minha conta', + 'sign-out' => 'Sair', + 'back' => 'Voltar', + 'name' => 'Nome', + 'configuration' => 'Configuração', + 'howdy' => 'Olá!', + 'warehouses' => 'Depósitos', + 'warehouse' => 'Depósito', + 'warehouses-info' => 'Adicionar, editar ou excluir depósitos do CRM', + 'inventory' => 'Estoque', + 'inventory-info' => 'Gerenciar todas as configurações relacionadas ao estoque no CRM', + 'data_transfer' => 'Data Transfer', + 'data_transfer_info' => 'Manage persons, products and leads data transfer related settings in the CRM', + ], + 'user' => [ + 'account' => [ + 'name' => 'Nome', + 'email' => 'E-mail', + 'password' => 'Senha', + 'my_account' => 'Minha conta', + 'update_details' => 'Atualizar Detalhes', + 'current_password' => 'Senha atual', + 'confirm_password' => 'Confirmar senha', + 'password-match' => 'A senha atual não corresponde.', + 'account-save' => 'Alterações na conta salvas com sucesso.', + 'permission-denied' => 'Permissão Negada', + 'remove-image' => 'Remover Imagem', + 'upload_image_pix' => 'Carregar uma Imagem de Perfil (100px x 100px)', + 'upload_image_format' => 'em formato PNG ou JPG', + 'image_upload_message' => 'Somente imagens (.jpeg, .jpg, .png, ..) são permitidas.', + ], + ], + 'emails' => [ + 'common' => [ + 'dear' => 'Prezado(a) :name', + 'cheers' => 'Atenciosamente,
    Equipe :app_name', + 'user' => [ + 'dear' => 'Prezado(a) :username', + 'create-subject' => 'Você foi adicionado como membro.', + 'create-body' => 'Parabéns! Agora você é um membro da nossa equipe.', + 'forget-password' => [ + 'subject' => 'Redefinir Senha do Cliente', + 'dear' => 'Prezado(a) :username', + 'reset-password' => 'Redefinir Senha', + 'info' => 'Você está recebendo este e-mail porque recebemos uma solicitação de redefinição de senha para sua conta.', + 'final-summary' => 'Se você não solicitou a redefinição de senha, nenhuma outra ação é necessária.', + 'thanks' => 'Obrigado!', + ], + ], + ], + ], + 'validations' => [ + 'message' => [ + 'code' => 'O campo deve ser um código válido.', + 'decimal' => 'The :attribute must be a decimal.', + ], + ], + 'errors' => [ + 'dashboard' => 'Dashboard', + 'go-back' => 'Go Back', + 'support' => 'If the problem persists, reach out to us at :email for assistance.', + 'unauthorized' => 'Esta ação não está autorizada.', + 'role-exceeds-own' => 'You can only assign a role whose permissions are the same as or fewer than your own.', + 'user-exceeds-own' => 'You can only manage users whose permissions are the same as or fewer than your own.', + 'own-privileges' => 'You cannot change your own role or data scope.', + 'scope-exceeds-own' => 'You cannot grant a data scope wider than your own.', + 'primary-admin-protected' => 'The primary administrator account is protected and cannot be modified here.', + 'role-permissions-exceed-own' => 'You can only create or edit roles whose permissions are the same as or fewer than your own.', + + '404' => [ + 'description' => 'Oops! The page you\'re looking for is on vacation. It seems we couldn\'t find what you were searching for.', + 'title' => '404 Page Not Found', + ], + '401' => [ + 'description' => 'Ops! Parece que você não tem permissão para acessar esta página. Parece que estão faltando as credenciais necessárias.', + 'title' => '401 Não autorizado.', + ], + '403' => [ + 'description' => 'Oops! This page is off-limits. It appears you don\'t have the required permissions to view this content.', + 'title' => '403 Forbidden', + ], + '500' => [ + 'description' => 'Oops! Something went wrong. It seems we\'re having trouble loading the page you\'re looking for.', + 'title' => '500 Internal Server Error', + ], + '503' => [ + 'description' => 'Oops! Looks like we\'re temporarily down for maintenance. Please check back in a bit.', + 'title' => '503 Service Unavailable', + ], + ], + 'export' => [ + 'csv' => 'CSV', + 'download' => 'Download', + 'export' => 'Exportar', + 'google-contacts' => 'Google Contacts', + 'google-contacts-in-progress' => 'Exportando seus contatos para o Google… isso pode levar um momento.', + 'google-contacts-total' => 'Total de Contatos:', + 'google-contacts-exported' => 'Exportados:', + 'google-contacts-duplicate' => 'Já Existiam:', + 'google-contacts-failed' => 'Falharam:', + 'google-contacts-summary' => 'Exportação concluída: :exported exportados, :duplicate já existiam, :failed falharam.', + 'no-records' => 'Nenhum registro encontrado.', + 'xls' => 'XLS', + 'xlsx' => 'XLSX', + ], +]; diff --git a/packages/Webkul/Admin/src/Resources/lang/tr/app.php b/packages/Webkul/Admin/src/Resources/lang/tr/app.php new file mode 100644 index 0000000..d68c154 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/lang/tr/app.php @@ -0,0 +1,2481 @@ + [ + 'help' => 'Yardım', + 'leads' => 'Potansiyeller', + 'lead' => 'Potansiyel', + 'quotes' => 'Teklifler', + 'mail' => 'Posta', + 'inbox' => 'Gelen Kutusu', + 'draft' => 'Taslak', + 'outbox' => 'Gönderilenler', + 'sent' => 'Gönderildi', + 'trash' => 'Çöp Kutusu', + 'activities' => 'Etkinlikler', + 'webhook' => 'Web Kancası', + 'contacts' => 'Kişiler', + 'persons' => 'Kişiler', + 'organizations' => 'Organizasyonlar', + 'products' => 'Ürünler', + 'settings' => 'Ayarlar', + 'groups' => 'Gruplar', + 'roles' => 'Roller', + 'users' => 'Kullanıcılar', + 'user' => 'Kullanıcı', + 'automation' => 'Otomasyon', + 'attributes' => 'Öznitelikler', + 'pipelines' => 'Pipelines', + 'sources' => 'Kaynaklar', + 'types' => 'Türler', + 'email-templates' => 'E-posta Şablonları', + 'workflows' => 'İş Akışları', + 'other-settings' => 'Diğer Ayarlar', + 'tags' => 'Etiketler', + 'configuration' => 'Yapılandırma', + 'create' => 'Oluştur', + 'quick_add' => 'Hızlı Ekle', + 'edit' => 'Düzenle', + 'view' => 'Görüntüle', + 'print' => 'Yazdır', + 'delete' => 'Sil', + 'export' => 'Dışa Aktar', + 'mass-delete' => 'Toplu Sil', + 'data-transfer' => 'Veri Transferi', + 'imports' => 'İthalatlar', + 'import' => 'İthalat', + 'event' => 'Etkinlik', + 'campaigns' => 'Kampanyalar', + 'warehouses' => 'Depolar', + 'inventory' => 'Envanter', + ], + 'users' => [ + 'activate-warning' => 'Hesabınız henüz etkinleştirilmedi. Lütfen yönetici ile iletişime geçin.', + 'login-error' => 'Kimlik bilgileri kayıtlarımızla eşleşmiyor.', + 'not-permission' => 'Yönetici paneline erişim izniniz yok.', + 'login' => [ + 'email' => 'E-posta Adresi', + 'forget-password-link' => 'Şifremi Unuttum?', + 'password' => 'Şifre', + 'submit-btn' => 'Giriş Yap', + 'title' => 'Giriş Yap', + ], + 'forget-password' => [ + 'create' => [ + 'email' => 'Kayıtlı E-posta', + 'email-not-exist' => 'E-posta Mevcut Değil', + 'page-title' => 'Şifremi Unuttum', + 'reset-link-sent' => 'Şifre sıfırlama bağlantısı gönderildi', + 'sign-in-link' => 'Giriş Yapmaya Dön?', + 'submit-btn' => 'Sıfırla', + 'title' => 'Şifre Kurtarma', + ], + ], + 'reset-password' => [ + 'back-link-title' => 'Giriş Yapmaya Dön?', + 'confirm-password' => 'Şifreyi Onayla', + 'email' => 'Kayıtlı E-posta', + 'password' => 'Şifre', + 'submit-btn' => 'Şifreyi Sıfırla', + 'title' => 'Şifre Sıfırlama', + ], + ], + 'account' => [ + 'edit' => [ + 'back-btn' => 'Geri', + 'change-password' => 'Şifreyi Değiştir', + 'confirm-password' => 'Şifreyi Onayla', + 'current-password' => 'Mevcut Şifre', + 'email' => 'E-posta', + 'general' => 'Genel', + 'invalid-password' => 'Girdiğiniz mevcut şifre yanlış.', + 'name' => 'Ad', + 'password' => 'Şifre', + 'profile-image' => 'Profil Resmi', + 'save-btn' => 'Hesabı Kaydet', + 'title' => 'Hesabım', + 'update-success' => 'Hesap başarıyla güncellendi', + 'upload-image-info' => 'Profil Resmi Yükleyin (110px X 110px) PNG veya JPG Formatında', + ], + ], + 'components' => [ + 'activities' => [ + 'actions' => [ + 'mail' => [ + 'btn' => 'Mail', + 'title' => 'Compose Mail', + 'to' => 'To', + 'enter-emails' => 'Press enter to add emails', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Subject', + 'send-btn' => 'Send', + 'message' => 'Message', + ], + 'file' => [ + 'btn' => 'File', + 'title' => 'Add File', + 'title-control' => 'Title', + 'name' => 'Name', + 'description' => 'Description', + 'file' => 'File', + 'save-btn' => 'Save File', + ], + 'note' => [ + 'btn' => 'Note', + 'title' => 'Add Note', + 'comment' => 'Comment', + 'save-btn' => 'Save Note', + ], + 'activity' => [ + 'btn' => 'Activity', + 'title' => 'Add Activity', + 'title-control' => 'Title', + 'description' => 'Description', + 'schedule-from' => 'Schedule From', + 'schedule-to' => 'Schedule To', + 'location' => 'Location', + 'call' => 'Call', + 'meeting' => 'Meeting', + 'lunch' => 'Lunch', + 'save-btn' => 'Save Activity', + 'participants' => [ + 'title' => 'Participants', + 'placeholder' => 'Type to search participants', + 'users' => 'Users', + 'persons' => 'Persons', + 'no-results' => 'No result found...', + ], + ], + ], + 'index' => [ + 'all' => 'All', + 'bcc' => 'Bcc', + 'by-user' => 'By :user', + 'calls' => 'Calls', + 'cc' => 'Cc', + 'change-log' => 'Changelogs', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'emails' => 'Emails', + 'empty' => 'Empty', + 'files' => 'Files', + 'from' => 'From', + 'location' => 'Location', + 'lunches' => 'Lunches', + 'mark-as-done' => 'Mark as Done', + 'meetings' => 'Meetings', + 'notes' => 'Notes', + 'participants' => 'Participants', + 'planned' => 'Planned', + 'quotes' => 'Quotes', + 'scheduled-on' => 'Scheduled on', + 'system' => 'System', + 'to' => 'To', + 'unlink' => 'Unlink', + 'view' => 'View', + 'empty-placeholders' => [ + 'all' => [ + 'title' => 'No Activities Found', + 'description' => 'No activities found for this. You can add activities by clicking on the Activity button on the left panel.', + ], + 'planned' => [ + 'title' => 'No Planned Activities Found', + 'description' => 'No planned activities found for this. You can add planned activities by clicking on the Activity button on the left panel.', + ], + 'notes' => [ + 'title' => 'No Notes Found', + 'description' => 'No notes found for this. You can add notes by clicking on the Note button on the left panel.', + ], + 'calls' => [ + 'title' => 'No Calls Found', + 'description' => 'No calls found for this. You can add calls by clicking on the Activity button on the left panel and selecting the Call type.', + ], + 'meetings' => [ + 'title' => 'No Meetings Found', + 'description' => 'No meetings found for this. You can add meetings by clicking on the Activity button on the left panel and selecting the Meeting type.', + ], + 'lunches' => [ + 'title' => 'No Lunches Found', + 'description' => 'No lunches found for this. You can add lunches by clicking on the Activity button on the left panel and selecting the Lunch type.', + ], + 'files' => [ + 'title' => 'No Files Found', + 'description' => 'No files found for this. You can add files by clicking on the File button on the left panel.', + ], + 'emails' => [ + 'title' => 'No Emails Found', + 'description' => 'No emails found for this. You can add emails by clicking on the Mail button on the left panel.', + ], + 'system' => [ + 'title' => 'No Changelogs Found', + 'description' => 'No changelogs found for this.', + ], + ], + ], + ], + 'media' => [ + 'images' => [ + 'add-image-btn' => 'Resim Ekle', + 'ai-add-image-btn' => 'Sihirli AI', + 'allowed-types' => 'png, jpeg, jpg', + 'not-allowed-error' => 'Sadece resim dosyalarına (.jpeg, .jpg, .png, vb.) izin verilmektedir.', + 'placeholders' => [ + 'front' => 'Ön', + 'next' => 'Sonraki', + 'size' => 'Boyut', + 'use-cases' => 'Kullanım Alanları', + 'zoom' => 'Yakınlaştır', + ], + ], + 'videos' => [ + 'add-video-btn' => 'Video Ekle', + 'allowed-types' => 'mp4, webm, mkv', + 'not-allowed-error' => 'Sadece video dosyalarına (.mp4, .mov, .ogg vb.) izin verilmektedir.', + ], + ], + 'datagrid' => [ + 'index' => [ + 'no-records-selected' => 'Hiçbir kayıt seçilmedi.', + 'must-select-a-mass-action-option' => 'Bir toplu işlem seçeneği seçmelisiniz.', + 'must-select-a-mass-action' => 'Bir toplu işlem seçmelisiniz.', + ], + 'toolbar' => [ + 'length-of' => ':length kadar', + 'of' => 'üzerinden', + 'per-page' => 'Sayfa Başına', + 'results' => ':total Sonuç', + 'delete' => 'Sil', + 'selected' => ':total Seçilen Öğeler', + 'mass-actions' => [ + 'submit' => 'Gönder', + 'select-option' => 'Seçim Yap', + 'select-action' => 'Eylem Seç', + ], + 'filter' => [ + 'apply-filters-btn' => 'Filtreleri Uygula', + 'back-btn' => 'Geri', + 'create-new-filter' => 'Yeni Filtre Oluştur', + 'custom-filters' => 'Özel Filtreler', + 'delete-error' => 'Filtre silinirken bir hata oluştu, lütfen tekrar deneyin.', + 'delete-success' => 'Filtre başarıyla silindi.', + 'empty-description' => 'Kaydedilecek seçili filtre bulunmamaktadır. Lütfen kaydetmek için filtreler seçin.', + 'empty-title' => 'Kaydetmek İçin Filtreler Ekleyin', + 'name' => 'Ad', + 'quick-filters' => 'Hızlı Filtreler', + 'save-btn' => 'Kaydet', + 'save-filter' => 'Filtreyi Kaydet', + 'saved-success' => 'Filtre başarıyla kaydedildi.', + 'selected-filters' => 'Seçilen Filtreler', + 'title' => 'Filtre', + 'update' => 'Güncelle', + 'update-filter' => 'Filtreyi Güncelle', + 'updated-success' => 'Filtre başarıyla güncellendi.', + ], + 'search' => [ + 'title' => 'Ara', + ], + 'column-settings' => [ + 'title' => 'Sütun Ayarları', + ], + ], + 'filters' => [ + 'select' => 'Seç', + 'title' => 'Filtreler', + 'dropdown' => [ + 'searchable' => [ + 'at-least-two-chars' => 'En az 2 karakter yazın...', + 'no-results' => 'Sonuç bulunamadı...', + ], + ], + 'custom-filters' => [ + 'clear-all' => 'Hepsini Temizle', + 'title' => 'Özel Filtreler', + ], + 'boolean-options' => [ + 'false' => 'Yanlış', + 'true' => 'Doğru', + ], + 'date-options' => [ + 'last-month' => 'Geçen Ay', + 'last-six-months' => 'Son 6 Ay', + 'last-three-months' => 'Son 3 Ay', + 'this-month' => 'Bu Ay', + 'this-week' => 'Bu Hafta', + 'this-year' => 'Bu Yıl', + 'today' => 'Bugün', + 'yesterday' => 'Dün', + ], + ], + 'table' => [ + 'actions' => 'Eylemler', + 'no-records-available' => 'Kayıt Bulunmuyor.', + ], + ], + 'modal' => [ + 'confirm' => [ + 'agree-btn' => 'Kabul Et', + 'disagree-btn' => 'Reddet', + 'message' => 'Bu işlemi gerçekleştirmek istediğinizden emin misiniz?', + 'title' => 'Emin Misiniz?', + ], + ], + 'tags' => [ + 'index' => [ + 'title' => 'Etiketler', + 'added-tags' => 'Eklenen Etiketler', + 'save-btn' => 'Etiketi Kaydet', + 'placeholder' => 'Etiketleri aramak için yazın', + 'add-tag' => '\\" :term \\" Ekle...', + 'aquarelle-red' => 'Aquarelle Kırmızı', + 'crushed-cashew' => 'Ezilmiş Antep Fıstığı', + 'beeswax' => 'Abeş Mum', + 'lemon-chiffon' => 'Limon Şifon', + 'snow-flurry' => 'Kar Fırtınası', + 'honeydew' => 'Honeydew', + ], + ], + 'layouts' => [ + 'powered-by' => [ + 'description' => ':webkul tarafından geliştirilen açık kaynaklı bir proje olan :krayin tarafından desteklenmektedir.', + ], + 'header' => [ + 'mega-search' => [ + 'title' => 'Mega Arama', + 'tabs' => [ + 'leads' => 'Müşteriler', + 'quotes' => 'Teklifler', + 'persons' => 'Kişiler', + 'products' => 'Ürünler', + 'settings' => 'Ayarlar', + 'configurations' => 'Yaplandırmalar', + ], + 'explore-all-products' => 'Tüm Ürünleri Keşfet', + 'explore-all-leads' => 'Tüm Müşterileri Keşfet', + 'explore-all-contacts' => 'Tüm İletişimleri Keşfet', + 'explore-all-quotes' => 'Tüm Teklifleri Keşfet', + 'explore-all-settings' => 'Tüm Ayarları Keşfet', + 'explore-all-configurations' => 'Tüm Yapılandırmaları Keşfet', + 'explore-all-matching-products' => '":query" (:count) ile eşleşen tüm ürünleri keşfet', + 'explore-all-matching-leads' => '":query" (:count) ile eşleşen tüm müşterileri keşfet', + 'explore-all-matching-contacts' => '":query" (:count) ile eşleşen tüm iletişimleri keşfet', + 'explore-all-matching-quotes' => '":query" (:count) ile eşleşen tüm teklifleri keşfet', + ], + ], + ], + 'attributes' => [ + 'edit' => [ + 'delete' => 'Sil', + ], + 'lookup' => [ + 'click-to-add' => 'Eklemek için tıklayın', + 'search' => 'Arama...', + 'no-result-found' => 'Sonuç bulunamadı', + ], + ], + 'lookup' => [ + 'click-to-add' => 'Eklemek için Tıklayın', + 'no-results' => 'Sonuç Bulunamadı', + 'add-as-new' => 'Yeni Olarak Ekle', + 'search' => 'Arama...', + ], + 'flash-group' => [ + 'success' => 'Başarı', + 'error' => 'Hata', + 'warning' => 'Uyarı', + 'info' => 'Bilgi', + ], + 'tiny-mce' => [ + 'http-error' => 'HTTP Hatası', + 'invalid-json' => 'Sunucudan geçersiz JSON yanıtı.', + 'upload-failed' => 'Dosya yüklemesi başarısız oldu. Lütfen tekrar deneyin.', + ], + ], + 'quotes' => [ + 'index' => [ + 'title' => 'Teklifler', + 'create-btn' => 'Teklif Oluştur', + 'create-success' => 'Teklif başarıyla oluşturuldu.', + 'update-success' => 'Teklif başarıyla güncellendi.', + 'delete-success' => 'Teklif başarıyla silindi.', + 'delete-failed' => 'Teklif silinemedi.', + 'datagrid' => [ + 'subject' => 'Konu', + 'sales-person' => 'Satış Temsilcisi', + 'expired-at' => 'Son Kullanma Tarihi', + 'created-at' => 'Oluşturulma Tarihi', + 'person' => 'Kişi', + 'subtotal' => 'Ara Toplam', + 'discount' => 'İndirim', + 'tax' => 'Vergi', + 'adjustment' => 'Düzenleme', + 'grand-total' => 'Genel Toplam', + 'edit' => 'Düzenle', + 'delete' => 'Sil', + 'print' => 'Yazdır', + ], + 'pdf' => [ + 'adjustment' => 'Düzenleme', + 'amount' => 'Tutar', + 'billing-address' => 'Fatura Adresi', + 'date' => 'Tarih', + 'discount' => 'İndirim', + 'description' => 'Açıklama', + 'expired-at' => 'Son Kullanma Tarihi', + 'grand-total' => 'Genel Toplam', + 'person' => 'Kişi', + 'price' => 'Fiyat', + 'product-name' => 'Ürün Adı', + 'quantity' => 'Miktar', + 'quote-id' => 'Teklif ID', + 'sales-person' => 'Satış Temsilcisi', + 'shipping-address' => 'Teslimat Adresi', + 'sku' => 'SKU', + 'sub-total' => 'Ara Toplam', + 'subject' => 'Konu', + 'tax' => 'Vergi', + 'title' => 'Teklif', + ], + ], + 'create' => [ + 'title' => 'Teklif Oluştur', + 'save-btn' => 'Teklifi Kaydet', + 'quote-info' => 'Teklif Bilgileri', + 'quote-info-info' => 'Teklifin temel bilgilerini girin.', + 'address-info' => 'Adres Bilgileri', + 'address-info-info' => 'Teklif ile ilgili adres bilgileri.', + 'same-as-billing' => 'Teslimat adresi fatura adresi ile aynı', + 'quote-items' => 'Teklif Kalemleri', + 'search-products' => 'Ürünleri Ara', + 'link-to-lead' => 'Potansiyele Bağla', + 'quote-item-info' => 'Bu teklif için ürün talebi ekleyin.', + 'quote-name' => 'Teklif Adı', + 'quantity' => 'Miktar', + 'price' => 'Fiyat', + 'discount' => 'İndirim', + 'tax' => 'Vergi', + 'total' => 'Toplam', + 'amount' => 'Tutar', + 'add-item' => '+ Kalem Ekle', + 'sub-total' => 'Ara Toplam (:symbol)', + 'total-discount' => 'İndirim (:symbol)', + 'total-tax' => 'Vergi (:symbol)', + 'total-adjustment' => 'Düzenleme (:symbol)', + 'grand-total' => 'Genel Toplam (:symbol)', + 'discount-amount' => 'İndirim Tutarı', + 'tax-amount' => 'Vergi Tutarı', + 'adjustment-amount' => 'Düzenleme Tutarı', + 'product-name' => 'Ürün Adı', + 'action' => 'Eylem', + ], + 'edit' => [ + 'title' => 'Teklifi Düzenle', + 'save-btn' => 'Teklifi Kaydet', + 'quote-info' => 'Teklif Bilgileri', + 'quote-info-info' => 'Teklifin temel bilgilerini girin.', + 'address-info' => 'Adres Bilgileri', + 'address-info-info' => 'Teklif ile ilgili adres bilgileri.', + 'quote-items' => 'Teklif Kalemleri', + 'link-to-lead' => 'Potansiyele Bağla', + 'quote-item-info' => 'Bu teklif için ürün talebi ekleyin.', + 'quote-name' => 'Teklif Adı', + 'quantity' => 'Miktar', + 'price' => 'Fiyat', + 'search-products' => 'Ürünleri Ara', + 'discount' => 'İndirim', + 'tax' => 'Vergi', + 'total' => 'Toplam', + 'amount' => 'Tutar', + 'add-item' => '+ Kalem Ekle', + 'sub-total' => 'Ara Toplam (:symbol)', + 'total-discount' => 'İndirim (:symbol)', + 'total-tax' => 'Vergi (:symbol)', + 'total-adjustment' => 'Düzenleme (:symbol)', + 'grand-total' => 'Genel Toplam (:symbol)', + 'discount-amount' => 'İndirim Tutarı', + 'tax-amount' => 'Vergi Tutarı', + 'adjustment-amount' => 'Düzenleme Tutarı', + 'product-name' => 'Ürün Adı', + 'action' => 'Eylem', + ], + ], + 'contacts' => [ + 'persons' => [ + 'index' => [ + 'title' => 'Kişiler', + 'create-btn' => 'Kişi Oluştur', + 'create-success' => 'Kişi başarıyla oluşturuldu.', + 'update-success' => 'Kişi başarıyla güncellendi.', + 'all-delete-success' => 'Seçilen tüm kişiler başarıyla silindi.', + 'partial-delete-warning' => 'Bazı kişiler başarıyla silindi. Diğerleri potansiyel müşterilerle bağlantılı olduğu için silinemedi.', + 'none-delete-warning' => 'Seçilen kişilerin hiçbiri potansiyel müşterilerle bağlantılı olduğu için silinemedi.', + 'no-selection' => 'Silinecek kişi seçilmedi.', + 'delete-failed' => 'Seçilen kişiler silinemedi.', + 'datagrid' => [ + 'contact-numbers' => 'İletişim Numaraları', + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'emails' => 'E-postalar', + 'id' => 'ID', + 'view' => 'Görüntüle', + 'name' => 'Ad', + 'organization-name' => 'Kuruluş Adı', + 'tag-name' => 'Etiket Adı', + 'google-export-not-connected' => 'Lütfen önce Ayarlar > Diğer Ayarlar > Google Kişileri bölümünden Google hesabınızı bağlayın.', + 'google-export-started' => 'Google Kişilerine aktarım başlatıldı.', + ], + ], + 'view' => [ + 'title' => ':name', + 'about-person' => 'Kişi Hakkında', + 'about-organization' => 'Kuruluş Hakkında', + 'activities' => [ + 'index' => [ + 'all' => 'Hepsi', + 'calls' => 'Aramalar', + 'meetings' => 'Toplantılar', + 'lunches' => 'Öğle Yemekleri', + 'files' => 'Dosyalar', + 'quotes' => 'Teklifler', + 'notes' => 'Notlar', + 'emails' => 'E-postalar', + 'by-user' => ':user tarafından', + 'scheduled-on' => 'Planlandığı Tarih', + 'location' => 'Konum', + 'participants' => 'Katılımcılar', + 'mark-as-done' => 'Tamamlandı olarak işaretle', + 'delete' => 'Sil', + 'edit' => 'Düzenle', + ], + 'actions' => [ + 'mail' => [ + 'btn' => 'E-posta', + 'title' => 'E-posta Oluştur', + 'to' => 'Kime', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Konu', + 'send-btn' => 'Gönder', + 'message' => 'Mesaj', + ], + 'file' => [ + 'btn' => 'Dosya', + 'title' => 'Dosya Ekle', + 'title-control' => 'Başlık', + 'name' => 'Dosya Adı', + 'description' => 'Açıklama', + 'file' => 'Dosya', + 'save-btn' => 'Dosyayı Kaydet', + ], + 'note' => [ + 'btn' => 'Not', + 'title' => 'Not Ekle', + 'comment' => 'Yorum', + 'save-btn' => 'Notu Kaydet', + ], + 'activity' => [ + 'btn' => 'Etkinlik', + 'title' => 'Etkinlik Ekle', + 'title-control' => 'Başlık', + 'description' => 'Açıklama', + 'schedule-from' => 'Başlangıç Tarihi', + 'schedule-to' => 'Bitiş Tarihi', + 'location' => 'Konum', + 'call' => 'Çağrı', + 'meeting' => 'Toplantı', + 'lunch' => 'Öğle Yemeği', + 'save-btn' => 'Etkinliği Kaydet', + ], + ], + ], + 'tags' => [ + 'create-success' => 'Etiket başarıyla oluşturuldu.', + 'destroy-success' => 'Etiket başarıyla silindi.', + ], + ], + 'create' => [ + 'title' => 'Kişi Oluştur', + 'save-btn' => 'Kişiyi Kaydet', + ], + 'edit' => [ + 'title' => 'Kişiyi Düzenle', + 'save-btn' => 'Kişiyi Kaydet', + ], + ], + 'organizations' => [ + 'index' => [ + 'title' => 'Kuruluşlar', + 'create-btn' => 'Kuruluş Oluştur', + 'create-success' => 'Kuruluş başarıyla oluşturuldu.', + 'update-success' => 'Kuruluş başarıyla güncellendi.', + 'delete-success' => 'Kuruluş başarıyla silindi.', + 'delete-failed' => 'Kuruluş silinemedi.', + 'datagrid' => [ + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + 'persons-count' => 'Kişi Sayısı', + ], + ], + 'create' => [ + 'title' => 'Kuruluş Oluştur', + 'save-btn' => 'Kuruluşu Kaydet', + ], + 'edit' => [ + 'title' => 'Kuruluşu Düzenle', + 'save-btn' => 'Kuruluşu Kaydet', + ], + ], + ], + 'products' => [ + 'index' => [ + 'title' => 'Ürünler', + 'create-btn' => 'Ürün Oluştur', + 'create-success' => 'Ürün başarıyla oluşturuldu.', + 'update-success' => 'Ürün başarıyla güncellendi.', + 'delete-success' => 'Ürün başarıyla silindi.', + 'delete-failed' => 'Ürün silinemedi.', + 'datagrid' => [ + 'allocated' => 'Tahsis Edilen', + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'in-stock' => 'Stokta', + 'name' => 'Ad', + 'on-hand' => 'Elinde', + 'tag-name' => 'Etiket Adı', + 'price' => 'Fiyat', + 'sku' => 'SKU', + 'view' => 'Görüntüle', + ], + ], + 'create' => [ + 'save-btn' => 'Ürünleri Kaydet', + 'title' => 'Ürün Oluştur', + 'general' => 'Genel', + 'price' => 'Fiyat', + ], + 'edit' => [ + 'title' => 'Ürünleri Düzenle', + 'save-btn' => 'Ürünleri Kaydet', + 'general' => 'Genel', + 'price' => 'Fiyat', + ], + 'view' => [ + 'sku' => 'SKU', + 'all' => 'Hepsi', + 'notes' => 'Notlar', + 'files' => 'Dosyalar', + 'inventories' => 'Envanter', + 'change-logs' => 'Değişiklik Günlükleri', + 'attributes' => [ + 'about-product' => 'Ürün Hakkında', + ], + 'inventory' => [ + 'warehouse' => 'Depo', + 'in-stock' => 'Stokta', + 'allocated' => 'Tahsis Edilen', + 'on-hand' => 'Elinde', + 'actions' => 'İşlemler', + 'assign' => 'Ata', + 'add-warehouse' => 'Depo Ekle', + 'location' => 'Konum', + 'add-more' => 'Daha Fazla Ekle', + 'save' => 'Kaydet', + ], + ], + ], + 'settings' => [ + 'title' => 'Ayarlar', + 'groups' => [ + 'index' => [ + 'create-btn' => 'Grup Oluştur', + 'title' => 'Gruplar', + 'create-success' => 'Grup başarıyla oluşturuldu.', + 'update-success' => 'Grup başarıyla güncellendi.', + 'destroy-success' => 'Grup başarıyla silindi.', + 'delete-failed' => 'Grup silinemedi.', + 'delete-failed-associated-users' => 'Grup silinemiyor, çünkü kullanıcılar tarafından kullanılıyor.', + 'datagrid' => [ + 'delete' => 'Sil', + 'description' => 'Açıklama', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + ], + 'edit' => [ + 'title' => 'Grubu Düzenle', + ], + 'create' => [ + 'name' => 'Ad', + 'title' => 'Grup Oluştur', + 'description' => 'Açıklama', + 'save-btn' => 'Grubu Kaydet', + ], + ], + ], + 'roles' => [ + 'index' => [ + 'being-used' => 'Rol silinemedi, çünkü bu admin kullanıcısında kullanılıyor.', + 'create-btn' => 'Rol Oluştur', + 'create-success' => 'Rol başarıyla oluşturuldu.', + 'current-role-delete-error' => 'Mevcut kullanıcıya atanmış rol silinemedi.', + 'current-role-edit-error' => 'Kendi hesabınıza atanmış rolü düzenleyemezsiniz.', + 'delete-failed' => 'Rol silinemedi.', + 'delete-success' => 'Rol başarıyla silindi.', + 'last-delete-error' => 'En az bir rol gereklidir.', + 'settings' => 'Ayarlar', + 'title' => 'Roller', + 'update-success' => 'Rol başarıyla güncellendi.', + 'user-define-error' => 'Sistem rolü silinemedi.', + 'datagrid' => [ + 'all' => 'Hepsi', + 'custom' => 'Özel', + 'delete' => 'Sil', + 'description' => 'Açıklama', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + 'permission-type' => 'İzin Türü', + ], + ], + 'create' => [ + 'access-control' => 'Erişim Kontrolü', + 'all' => 'Hepsi', + 'back-btn' => 'Geri', + 'custom' => 'Özel', + 'description' => 'Açıklama', + 'general' => 'Genel', + 'name' => 'Ad', + 'permissions' => 'İzinler', + 'save-btn' => 'Rolü Kaydet', + 'title' => 'Rol Oluştur', + ], + 'edit' => [ + 'access-control' => 'Erişim Kontrolü', + 'all' => 'Hepsi', + 'back-btn' => 'Geri', + 'custom' => 'Özel', + 'description' => 'Açıklama', + 'general' => 'Genel', + 'name' => 'Ad', + 'permissions' => 'İzinler', + 'save-btn' => 'Rolü Kaydet', + 'title' => 'Rol Düzenle', + ], + ], + 'types' => [ + 'index' => [ + 'create-btn' => 'Tür Oluştur', + 'create-success' => 'Tür başarıyla oluşturuldu.', + 'delete-failed' => 'Tür silinemedi.', + 'delete-success' => 'Tür başarıyla silindi.', + 'title' => 'Türler', + 'update-success' => 'Tür başarıyla güncellendi.', + 'datagrid' => [ + 'delete' => 'Sil', + 'description' => 'Açıklama', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + ], + 'create' => [ + 'name' => 'Ad', + 'save-btn' => 'Türü Kaydet', + 'title' => 'Tür Oluştur', + ], + 'edit' => [ + 'title' => 'Tür Düzenle', + ], + ], + ], + 'sources' => [ + 'index' => [ + 'title' => 'Kaynaklar', + 'create-btn' => 'Kaynak Oluştur', + 'create-success' => 'Kaynak başarıyla oluşturuldu.', + 'delete-failed' => 'Kaynak silinemedi.', + 'delete-success' => 'Kaynak başarıyla silindi.', + 'update-success' => 'Kaynak başarıyla güncellendi.', + 'delete-failed-associated-leads' => 'Kaynak silinemiyor çünkü mevcut adaylarla ilişkili. Lütfen bu adayları silmeden önce bağlantılarını kaldırın veya güncelleyin.', + 'datagrid' => [ + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + ], + 'create' => [ + 'name' => 'Ad', + 'save-btn' => 'Kaynağı Kaydet', + 'title' => 'Kaynak Oluştur', + ], + 'edit' => [ + 'title' => 'Kaynağı Düzenle', + ], + ], + ], + 'workflows' => [ + 'index' => [ + 'title' => 'İş Akışları', + 'create-btn' => 'İş Akışı Oluştur', + 'create-success' => 'İş akışı başarıyla oluşturuldu.', + 'update-success' => 'İş akışı başarıyla güncellendi.', + 'delete-success' => 'İş akışı başarıyla silindi.', + 'delete-failed' => 'İş akışı silinemedi.', + 'datagrid' => [ + 'delete' => 'Sil', + 'description' => 'Açıklama', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + ], + ], + 'helpers' => [ + 'update-related-leads' => 'İlgili fırsatları güncelle', + 'send-email-to-sales-owner' => 'Satış sahibiyle e-posta gönder', + 'send-email-to-participants' => 'Katılımcılara e-posta gönder', + 'add-webhook' => 'Webhook Ekle', + 'update-lead' => 'Fırsatı Güncelle', + 'update-person' => 'Kişiyi Güncelle', + 'send-email-to-person' => 'Kişiye e-posta gönder', + 'add-tag' => 'Etiket Ekle', + 'add-note-as-activity' => 'Notu Aktivite Olarak Ekle', + 'update-quote' => 'Teklifi Güncelle', + ], + 'create' => [ + 'title' => 'İş Akışı Oluştur', + 'event' => 'Olay', + 'back-btn' => 'Geri', + 'save-btn' => 'İş Akışını Kaydet', + 'name' => 'Ad', + 'basic-details' => 'Temel Bilgiler', + 'description' => 'Açıklama', + 'actions' => 'Eylemler', + 'basic-details-info' => 'İş akışının temel bilgilerini girin.', + 'event-info' => 'Bir olay tetikler, kontrolleri yapar, koşulları değerlendirir ve önceden tanımlanmış eylemleri gerçekleştirir.', + 'conditions' => 'Koşullar', + 'conditions-info' => 'Koşullar, belirli durumlarda tetiklenen kurallardır.', + 'actions-info' => 'Bir eylem sadece iş yükünü azaltmakla kalmaz, aynı zamanda CRM otomasyonu için oldukça kolaylaştırır.', + 'value' => 'Değer', + 'condition-type' => 'Koşul Türü', + 'all-condition-are-true' => 'Tüm koşullar doğru', + 'any-condition-are-true' => 'Herhangi bir koşul doğru', + 'add-condition' => 'Koşul Ekle', + 'add-action' => 'Eylem Ekle', + 'no-webhook-found' => 'Webhook bulunamadı.', + 'create-webhook' => 'Yeni bir webhook oluşturun.', + 'yes' => 'Evet', + 'no' => 'Hayır', + 'email' => 'E-posta', + 'is-equal-to' => 'Eşittir', + 'is-not-equal-to' => 'Eşit değildir', + 'equals-or-greater-than' => 'Eşittir veya büyük', + 'equals-or-less-than' => 'Eşittir veya küçük', + 'greater-than' => 'Büyük', + 'less-than' => 'Küçük', + 'type' => 'Tür', + 'contain' => 'İçerir', + 'contains' => 'İçerir', + 'does-not-contain' => 'İçermez', + ], + 'edit' => [ + 'title' => 'İş Akışını Düzenle', + 'event' => 'Olay', + 'back-btn' => 'Geri', + 'save-btn' => 'İş Akışını Kaydet', + 'name' => 'Ad', + 'basic-details' => 'Temel Bilgiler', + 'description' => 'Açıklama', + 'actions' => 'Eylemler', + 'type' => 'Tür', + 'basic-details-info' => 'İş akışının temel bilgilerini girin.', + 'event-info' => 'Bir olay tetikler, kontrolleri yapar, koşulları değerlendirir ve önceden tanımlanmış eylemleri gerçekleştirir.', + 'conditions' => 'Koşullar', + 'conditions-info' => 'Koşullar, belirli durumlarda tetiklenen kurallardır.', + 'actions-info' => 'Bir eylem sadece iş yükünü azaltmakla kalmaz, aynı zamanda CRM otomasyonu için oldukça kolaylaştırır.', + 'value' => 'Değer', + 'condition-type' => 'Koşul Türü', + 'all-condition-are-true' => 'Tüm koşullar doğru', + 'any-condition-are-true' => 'Herhangi bir koşul doğru', + 'add-condition' => 'Koşul Ekle', + 'add-action' => 'Eylem Ekle', + 'yes' => 'Evet', + 'no' => 'Hayır', + 'email' => 'E-posta', + 'is-equal-to' => 'Eşittir', + 'is-not-equal-to' => 'Eşit değildir', + 'equals-or-greater-than' => 'Eşittir veya büyük', + 'equals-or-less-than' => 'Eşittir veya küçük', + 'greater-than' => 'Büyük', + 'less-than' => 'Küçük', + 'contain' => 'İçerir', + 'contains' => 'İçerir', + 'does-not-contain' => 'İçermez', + ], + ], + 'google-contacts' => [ + 'index' => [ + 'title' => 'Google Kişileri', + 'not-connected-info' => 'CRM kişilerini Google Kişilerine aktarmak için bir Google hesabı bağlayın.', + 'connected-info' => ':email olarak bağlandı.', + 'connect-btn' => 'Google Hesabı Bağla', + 'disconnect-btn' => 'Bağlantıyı Kes', + 'connect-success' => 'Google hesabı başarıyla bağlandı.', + 'connect-failed' => 'Google hesabınız bağlanamadı. Lütfen tekrar deneyin.', + 'not-configured' => 'Google OAuth yapılandırılmamış. Lütfen önce gerekli ortam değişkenlerini ayarlayın.', + 'disconnect-success' => 'Google hesabı bağlantısı başarıyla kesildi.', + ], + ], + 'webforms' => [ + 'index' => [ + 'title' => 'Web Formları', + 'create-btn' => 'Web Formu Oluştur', + 'create-success' => 'Web formu başarıyla oluşturuldu.', + 'update-success' => 'Web formu başarıyla güncellendi.', + 'delete-success' => 'Web formu başarıyla silindi.', + 'delete-failed' => 'Web formu silinemedi.', + 'datagrid' => [ + 'id' => 'ID', + 'title' => 'Başlık', + 'edit' => 'Düzenle', + 'delete' => 'Sil', + ], + ], + 'create' => [ + 'title' => 'Web Formu Oluştur', + 'add-attribute-btn' => 'Öznitelik Düğmesi Ekle', + 'attribute-label-color' => 'Öznitelik Etiketi Rengi', + 'attributes' => 'Öznitelikler', + 'attributes-info' => 'Forma özel öznitelikler ekleyin.', + 'background-color' => 'Arka Plan Rengi', + 'create-lead' => 'Fırsat Oluştur', + 'customize-webform' => 'Web Formunu Özelleştir', + 'customize-webform-info' => 'Web formunuzu seçtiğiniz eleman renkleri ile özelleştirin.', + 'description' => 'Açıklama', + 'display-custom-message' => 'Özel mesaj göster', + 'form-background-color' => 'Form Arka Plan Rengi', + 'form-submit-btn-color' => 'Form Gönderim Düğmesi Rengi', + 'form-submit-button-color' => 'Form Gönderim Düğmesi Rengi', + 'form-title-color' => 'Form Başlık Rengi', + 'general' => 'Genel', + 'leads' => 'Fırsatlar', + 'person' => 'Kişi', + 'pipeline' => 'Pipeline', + 'save-btn' => 'Web Formunu Kaydet', + 'submit-button-label' => 'Gönderim Düğmesi Etiketi', + 'submit-success-action' => 'Gönderim Başarı Eylemi', + 'redirect-to-url' => 'URL\'ye Yönlendir', + 'choose-value' => 'Değer Seç', + 'select-file' => 'Dosya Seç', + 'select-image' => 'Görüntü Seç', + 'enter-value' => 'Değer Gir', + ], + 'edit' => [ + 'add-attribute-btn' => 'Öznitelik Düğmesi Ekle', + 'attribute-label-color' => 'Öznitelik Etiketi Rengi', + 'attributes' => 'Öznitelikler', + 'attributes-info' => 'Forma özel öznitelikler ekleyin.', + 'background-color' => 'Arka Plan Rengi', + 'choose-value' => 'Değer Seç', + 'code-snippet' => 'Kod Parçası', + 'copied' => 'Kopyalandı', + 'copy' => 'Kopyala', + 'create-lead' => 'Fırsat Oluştur', + 'customize-webform' => 'Web Formunu Özelleştir', + 'customize-webform-info' => 'Web formunuzu seçtiğiniz eleman renkleri ile özelleştirin.', + 'description' => 'Açıklama', + 'display-custom-message' => 'Özel mesaj göster', + 'embed' => 'Göm', + 'enter-value' => 'Değer Gir', + 'form-background-color' => 'Form Arka Plan Rengi', + 'form-submit-btn-color' => 'Form Gönderim Düğmesi Rengi', + 'form-submit-button-color' => 'Form Gönderim Düğmesi Rengi', + 'form-title-color' => 'Form Başlık Rengi', + 'general' => 'Genel', + 'leads' => 'Fırsatlar', + 'person' => 'Kişi', + 'pipeline' => 'Pipeline', + 'preview' => 'Önizleme', + 'public-url' => 'Genel URL', + 'redirect-to-url' => 'URL\'ye Yönlendir', + 'save-btn' => 'Web Formunu Kaydet', + 'select-file' => 'Dosya Seç', + 'select-image' => 'Görüntü Seç', + 'submit-button-label' => 'Gönderim Düğmesi Etiketi', + 'submit-success-action' => 'Gönderim Başarı Eylemi', + 'title' => 'Web Formunu Düzenle', + ], + ], + 'email-template' => [ + 'index' => [ + 'create-btn' => 'E-posta Şablonu Oluştur', + 'title' => 'E-posta Şablonları', + 'create-success' => 'E-posta şablonu başarıyla oluşturuldu.', + 'update-success' => 'E-posta şablonu başarıyla güncellendi.', + 'delete-success' => 'E-posta şablonu başarıyla silindi.', + 'delete-failed' => 'E-posta şablonu silinemedi.', + 'datagrid' => [ + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + 'subject' => 'Konu', + ], + ], + 'create' => [ + 'title' => 'E-posta Şablonu Oluştur', + 'save-btn' => 'E-posta Şablonunu Kaydet', + 'email-template' => 'E-posta Şablonu', + 'subject' => 'Konu', + 'content' => 'İçerik', + 'subject-placeholders' => 'Konu Yer Tutucuları', + 'general' => 'Genel', + 'name' => 'Ad', + ], + 'edit' => [ + 'title' => 'E-posta Şablonunu Düzenle', + 'save-btn' => 'E-posta Şablonunu Kaydet', + 'email-template' => 'E-posta Şablonu', + 'subject' => 'Konu', + 'content' => 'İçerik', + 'subject-placeholders' => 'Konu Yer Tutucuları', + 'general' => 'Genel', + 'name' => 'Ad', + ], + ], + 'marketing' => [ + 'events' => [ + 'index' => [ + 'create-btn' => 'Etkinlik Oluştur', + 'title' => 'Etkinlikler', + 'create-success' => 'Etkinlik başarıyla oluşturuldu.', + 'update-success' => 'Etkinlik başarıyla güncellendi.', + 'delete-success' => 'Etkinlik başarıyla silindi.', + 'delete-failed' => 'Etkinlik silinemedi.', + 'mass-delete-success' => 'Etkinlikler başarıyla silindi', + 'delete-failed-associated-campaigns' => 'Kampanya silinemiyor çünkü mevcut adaylarla ilişkili. Lütfen bu adayları silmeden önce bağlantılarını kaldırın veya güncelleyin.', + 'datagrid' => [ + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + 'description' => 'Açıklama', + 'date' => 'Tarih', + ], + 'create' => [ + 'title' => 'Etkinlik Oluştur', + 'name' => 'Ad', + 'date' => 'Tarih', + 'description' => 'Açıklama', + 'save-btn' => 'Etkinliği Kaydet', + ], + 'edit' => [ + 'title' => 'Etkinliği Düzenle', + ], + ], + ], + 'campaigns' => [ + 'index' => [ + 'create-btn' => 'Kampanya Oluştur', + 'title' => 'Kampanyalar', + 'create-success' => 'Kampanya başarıyla oluşturuldu.', + 'update-success' => 'Kampanya başarıyla güncellendi.', + 'delete-success' => 'Kampanya başarıyla silindi.', + 'delete-failed' => 'Kampanya silinemedi.', + 'mass-delete-success' => 'Kampanyalar başarıyla silindi', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Ad', + 'subject' => 'Konu', + 'status' => 'Durum', + 'active' => 'Aktif', + 'inactive' => 'Pasif', + 'edit' => 'Düzenle', + 'delete' => 'Sil', + ], + 'create' => [ + 'title' => 'Kampanya Oluştur', + 'name' => 'Ad', + 'type' => 'Tür', + 'subject' => 'Konu', + 'event' => 'Etkinlik', + 'email-template' => 'E-posta Şablonu', + 'status' => 'Durum', + ], + 'edit' => [ + 'title' => 'Kampanyayı Düzenle', + ], + ], + ], + ], + 'tags' => [ + 'index' => [ + 'create-btn' => 'Etiket Oluştur', + 'title' => 'Etiketler', + 'create-success' => 'Etiket başarıyla oluşturuldu.', + 'update-success' => 'Etiket başarıyla güncellendi.', + 'delete-success' => 'Etiket başarıyla silindi.', + 'delete-failed' => 'Etiket silinemedi.', + 'datagrid' => [ + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'name' => 'Ad', + 'users' => 'Kullanıcılar', + 'created-at' => 'Oluşturulma Tarihi', + ], + 'create' => [ + 'name' => 'Ad', + 'save-btn' => 'Etiketi Kaydet', + 'title' => 'Etiket Oluştur', + 'color' => 'Renk', + ], + 'edit' => [ + 'title' => 'Etiketi Düzenle', + ], + ], + ], + 'users' => [ + 'index' => [ + 'create-btn' => 'Kullanıcı Oluştur', + 'create-success' => 'Kullanıcı başarıyla oluşturuldu.', + 'delete-failed' => 'Kullanıcı silinemedi.', + 'delete-success' => 'Kullanıcı başarıyla silindi.', + 'last-delete-error' => 'En az bir kullanıcı gereklidir.', + 'mass-delete-failed' => 'Kullanıcılar silinemedi.', + 'mass-delete-success' => 'Kullanıcılar başarıyla silindi.', + 'mass-update-failed' => 'Kullanıcılar güncellenemedi.', + 'mass-update-success' => 'Kullanıcılar başarıyla güncellendi.', + 'title' => 'Kullanıcılar', + 'update-success' => 'Kullanıcı başarıyla güncellendi.', + 'user-define-error' => 'Sistem kullanıcısı silinemedi.', + 'active' => 'Aktif', + 'inactive' => 'Pasif', + 'datagrid' => [ + 'active' => 'Aktif', + 'created-at' => 'Oluşturulma Tarihi', + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'email' => 'E-posta', + 'id' => 'ID', + 'inactive' => 'Pasif', + 'name' => 'Ad', + 'status' => 'Durum', + 'update-status' => 'Durumu Güncelle', + 'group' => 'İlişkili Grup', + 'no-group' => 'Grup Yok', + 'users' => 'Kullanıcılar', + ], + 'create' => [ + 'confirm-password' => 'Şifreyi Onayla', + 'email' => 'E-posta', + 'general' => 'Genel', + 'global' => 'Küresel', + 'group' => 'Grup', + 'individual' => 'Bireysel', + 'name' => 'Ad', + 'password' => 'Şifre', + 'permission' => 'İzin', + 'role' => 'Rol', + 'save-btn' => 'Kullanıcıyı Kaydet', + 'status' => 'Durum', + 'title' => 'Kullanıcı Oluştur', + 'view-permission' => 'Görüntüleme İzni', + 'select-at-lest-one-group' => 'En az bir grup seçin', + ], + 'edit' => [ + 'title' => 'Kullanıcıyı Düzenle', + ], + ], + ], + 'pipelines' => [ + 'index' => [ + 'title' => 'Pipelines', + 'create-btn' => 'Pipeline Oluştur', + 'create-success' => 'Pipeline başarıyla oluşturuldu.', + 'update-success' => 'Pipeline başarıyla güncellendi.', + 'default-required' => 'En az bir varsayılan boru hattı gereklidir.', + 'delete-success' => 'Pipeline başarıyla silindi.', + 'delete-failed' => 'Pipeline silinemedi.', + 'default-delete-error' => 'Varsayılan pipeline silinemez.', + 'datagrid' => [ + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'is-default' => 'Varsayılan mı', + 'name' => 'Ad', + 'no' => 'Hayır', + 'rotten-days' => 'Çürük Günler', + 'yes' => 'Evet', + ], + ], + 'create' => [ + 'title' => 'Pipeline Oluştur', + 'save-btn' => 'Pipeline Kaydet', + 'name' => 'Ad', + 'rotten-days' => 'Çürük Günler', + 'mark-as-default' => 'Varsayılan Olarak İşaretle', + 'general' => 'Genel', + 'probability' => 'Olasılık (%)', + 'new-stage' => 'Yeni', + 'won-stage' => 'Kazandı', + 'lost-stage' => 'Kaybetti', + 'stage-btn' => 'Aşama Ekle', + 'stages' => 'Aşamalar', + 'duplicate-name' => 'Ad alanı tekrar edemez', + 'delete-stage' => 'Aşama Sil', + 'add-new-stages' => 'Yeni Aşamalar Ekle', + 'add-stage-info' => 'Pipeline için yeni aşama ekleyin', + 'newly-added' => 'Yeni Eklenen', + 'stage-delete-success' => 'Aşama Başarıyla Silindi', + ], + 'edit' => [ + 'title' => 'Pipeline\'ı Düzenle', + 'save-btn' => 'Pipeline Kaydet', + 'name' => 'Ad', + 'rotten-days' => 'Çürük Günler', + 'mark-as-default' => 'Varsayılan Olarak İşaretle', + 'general' => 'Genel', + 'probability' => 'Olasılık (%)', + 'new-stage' => 'Yeni', + 'won-stage' => 'Kazandı', + 'lost-stage' => 'Kaybetti', + 'stage-btn' => 'Aşama Ekle', + 'stages' => 'Aşamalar', + 'duplicate-name' => 'Ad alanı tekrar edemez', + 'delete-stage' => 'Aşama Sil', + 'add-new-stages' => 'Yeni Aşamalar Ekle', + 'add-stage-info' => 'Pipeline için yeni aşama ekleyin', + 'stage-delete-success' => 'Aşama Başarıyla Silindi', + ], + ], + 'webhooks' => [ + 'index' => [ + 'title' => 'Webhooks', + 'create-btn' => 'Webhook Oluştur', + 'create-success' => 'Webhook başarıyla oluşturuldu.', + 'update-success' => 'Webhook başarıyla güncellendi.', + 'delete-success' => 'Webhook başarıyla silindi.', + 'delete-failed' => 'Webhook silinemedi.', + 'invalid-endpoint' => 'Web kancası uç nokta URL\'si genel bir HTTP veya HTTPS adresi olmalıdır.', + 'datagrid' => [ + 'id' => 'ID', + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'name' => 'Ad', + 'entity-type' => 'Varlık Türü', + 'end-point' => 'Son Nokta', + ], + ], + 'create' => [ + 'title' => 'Webhook Oluştur', + 'save-btn' => 'Webhook Kaydet', + 'info' => 'Webhook detaylarını girin', + 'url-and-parameters' => 'URL ve Parametreler', + 'method' => 'Yöntem', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'URL Son Noktası', + 'parameters' => 'Parametreler', + 'add-new-parameter' => 'Yeni Parametre Ekle', + 'url-preview' => 'URL Önizleme:', + 'headers' => 'Başlıklar', + 'add-new-header' => 'Yeni Başlık Ekle', + 'body' => 'Gövde', + 'default' => 'Varsayılan', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Anahtar ve Değer', + 'add-new-payload' => 'Yeni yük ekle', + 'raw' => 'Ham', + 'general' => 'Genel', + 'name' => 'Ad', + 'entity-type' => 'Varlık Türü', + 'insert-placeholder' => 'Yer Tutucu Ekle', + 'description' => 'Açıklama', + 'json' => 'Json', + 'text' => 'Metin', + ], + 'edit' => [ + 'title' => 'Webhook\'u Düzenle', + 'edit-btn' => 'Webhook Kaydet', + 'save-btn' => 'Webhook Kaydet', + 'info' => 'Webhook detaylarını girin', + 'url-and-parameters' => 'URL ve Parametreler', + 'method' => 'Yöntem', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'URL Son Noktası', + 'parameters' => 'Parametreler', + 'add-new-parameter' => 'Yeni Parametre Ekle', + 'url-preview' => 'URL Önizleme:', + 'headers' => 'Başlıklar', + 'add-new-header' => 'Yeni Başlık Ekle', + 'body' => 'Gövde', + 'default' => 'Varsayılan', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Anahtar ve Değer', + 'add-new-payload' => 'Yeni yük ekle', + 'raw' => 'Ham', + 'general' => 'Genel', + 'name' => 'Ad', + 'entity-type' => 'Varlık Türü', + 'insert-placeholder' => 'Yer Tutucu Ekle', + 'description' => 'Açıklama', + 'json' => 'Json', + 'text' => 'Metin', + ], + ], + 'warehouses' => [ + 'index' => [ + 'title' => 'Depolar', + 'create-btn' => 'Depo Oluştur', + 'create-success' => 'Depo başarıyla oluşturuldu.', + 'name-exists' => 'Depo adı zaten mevcut.', + 'update-success' => 'Depo başarıyla güncellendi.', + 'delete-success' => 'Depo başarıyla silindi.', + 'delete-failed' => 'Depo silinemedi.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Ad', + 'contact-name' => 'İletişim Adı', + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'view' => 'Görüntüle', + 'created-at' => 'Oluşturulma Tarihi', + 'products' => 'Ürünler', + 'contact-emails' => 'İletişim E-postaları', + 'contact-numbers' => 'İletişim Numaraları', + ], + ], + 'create' => [ + 'title' => 'Depo Oluştur', + 'save-btn' => 'Depoyu Kaydet', + 'contact-info' => 'İletişim Bilgileri', + ], + 'edit' => [ + 'title' => 'Depoyu Düzenle', + 'save-btn' => 'Depoyu Kaydet', + 'contact-info' => 'İletişim Bilgileri', + ], + 'view' => [ + 'all' => 'Tümü', + 'notes' => 'Notlar', + 'files' => 'Dosyalar', + 'location' => 'Konum', + 'change-logs' => 'Değişiklik Kayıtları', + 'locations' => [ + 'action' => 'Eylem', + 'add-location' => 'Konum Ekle', + 'create-success' => 'Konum başarıyla oluşturuldu.', + 'delete' => 'Sil', + 'delete-failed' => 'Konum silinemedi.', + 'delete-success' => 'Konum başarıyla silindi.', + 'name' => 'Ad', + 'save-btn' => 'Kaydet', + ], + 'general-information' => [ + 'title' => 'Genel Bilgiler', + ], + 'contact-information' => [ + 'title' => 'İletişim Bilgileri', + ], + ], + ], + 'attributes' => [ + 'index' => [ + 'title' => 'Öznitelikler', + 'create-btn' => 'Öznitelik Oluştur', + 'create-success' => 'Öznitelik başarıyla oluşturuldu.', + 'update-success' => 'Öznitelik başarıyla güncellendi.', + 'delete-success' => 'Öznitelik başarıyla silindi.', + 'delete-failed' => 'Öznitelik silinemedi.', + 'user-define-error' => 'Sistem özniteliği silinemez.', + 'mass-delete-failed' => 'Sistem öznitelikleri silinemez.', + 'datagrid' => [ + 'yes' => 'Evet', + 'no' => 'Hayır', + 'id' => 'ID', + 'code' => 'Kod', + 'name' => 'Ad', + 'entity-type' => 'Varlık Türü', + 'type' => 'Tür', + 'is-default' => 'Varsayılan mı', + 'edit' => 'Düzenle', + 'delete' => 'Sil', + 'quick-add' => 'Hızlı Ekle', + 'entity-types' => [ + 'leads' => 'Potansiyeller', + 'organizations' => 'Organizasyonlar', + 'persons' => 'Kişiler', + 'products' => 'Ürünler', + 'quotes' => 'Teklifler', + 'warehouses' => 'Depolar', + ], + 'types' => [ + 'text' => 'Metin', + 'textarea' => 'Metin alanı', + 'price' => 'Fiyat', + 'boolean' => 'Mantıksal', + 'select' => 'Seçim', + 'multiselect' => 'Çoklu seçim', + 'checkbox' => 'Onay kutusu', + 'email' => 'E-posta', + 'address' => 'Adres', + 'phone' => 'Telefon', + 'lookup' => 'Arama', + 'datetime' => 'Tarih ve saat', + 'date' => 'Tarih', + 'image' => 'Görsel', + 'file' => 'Dosya', + ], + ], + ], + 'create' => [ + 'title' => 'Öznitelik Oluştur', + 'save-btn' => 'Özniteliği Kaydet', + 'code' => 'Kod', + 'name' => 'Ad', + 'quick_add' => 'Hızlı Ekle', + 'entity-type' => 'Varlık Türü', + 'type' => 'Tür', + 'validations' => 'Doğrulamalar', + 'is-required' => 'Gerekli mi', + 'input-validation' => 'Girdi Doğrulaması', + 'is-unique' => 'Benzersiz mi', + 'labels' => 'Etiketler', + 'general' => 'Genel', + 'none' => 'Yok', + 'numeric' => 'Sayısal', + 'decimal' => 'Ondalık', + 'url' => 'URL', + 'options' => 'Seçenekler', + 'option-type' => 'Seçenek Türü', + 'lookup-type' => 'Arama Türü', + 'add-option' => 'Seçenek Ekle', + 'save-option' => 'Seçeneği Kaydet', + 'option-name' => 'Seçenek Adı', + 'add-attribute-options' => 'Öznitelik Seçenekleri Ekle', + 'text' => 'Metin', + 'textarea' => 'Metin Alanı', + 'price' => 'Fiyat', + 'boolean' => 'Boolean', + 'select' => 'Seç', + 'multiselect' => 'Çoklu Seçim', + 'email' => 'E-posta', + 'address' => 'Adres', + 'phone' => 'Telefon', + 'datetime' => 'Tarih Saat', + 'date' => 'Tarih', + 'image' => 'Resim', + 'file' => 'Dosya', + 'lookup' => 'Arama', + 'entity_type' => 'Varlık türü', + 'checkbox' => 'Onay Kutusu', + 'is_required' => 'Gerekli mi', + 'is_unique' => 'Benzersiz mi', + 'actions' => 'İşlemler', + ], + 'edit' => [ + 'actions' => 'İşlemler', + 'add-attribute-options' => 'Öznitelik Seçenekleri Ekle', + 'add-option' => 'Seçenek Ekle', + 'address' => 'Adres', + 'boolean' => 'Boolean', + 'checkbox' => 'Onay Kutusu', + 'code' => 'Kod', + 'date' => 'Tarih', + 'datetime' => 'Tarih Saat', + 'decimal' => 'Ondalık', + 'email' => 'E-posta', + 'entity-type' => 'Varlık Türü', + 'entity_type' => 'Varlık türü', + 'file' => 'Dosya', + 'general' => 'Genel', + 'image' => 'Resim', + 'input-validation' => 'Girdi Doğrulaması', + 'is-required' => 'Gerekli mi', + 'is-unique' => 'Benzersiz mi', + 'is_required' => 'Gerekli mi', + 'is_unique' => 'Benzersiz mi', + 'labels' => 'Etiketler', + 'lookup' => 'Arama', + 'lookup-type' => 'Arama Türü', + 'multiselect' => 'Çoklu Seçim', + 'name' => 'Ad', + 'quick_add' => 'Hızlı Ekle', + 'none' => 'Yok', + 'numeric' => 'Sayısal', + 'option-deleted' => 'Öznitelik Seçeneği başarıyla silindi', + 'option-name' => 'Seçenek Adı', + 'option-type' => 'Seçenek Türü', + 'options' => 'Seçenekler', + 'phone' => 'Telefon', + 'price' => 'Fiyat', + 'save-btn' => 'Özniteliği Kaydet', + 'save-option' => 'Seçeneği Kaydet', + 'select' => 'Seç', + 'text' => 'Metin', + 'textarea' => 'Metin Alanı', + 'title' => 'Özniteliği Düzenle', + 'type' => 'Tür', + 'url' => 'URL', + 'validations' => 'Doğrulamalar', + ], + ], + 'data-transfer' => [ + 'importers' => [ + 'persons' => [ + 'title' => 'Kişiler', + 'validation' => [ + 'errors' => [ + 'duplicate-email' => 'E-posta: \'%s\' içe aktarma dosyasında birden fazla kez bulundu.', + 'duplicate-phone' => 'Telefon: \'%s\' içe aktarma dosyasında birden fazla kez bulundu.', + 'email-not-found' => 'E-posta: \'%s\' sistemde bulunamadı.', + 'invalid-date' => 'Tarih: \'%s\' geçerli bir tarih değil.', + ], + ], + ], + 'products' => [ + 'title' => 'Ürünler', + 'validation' => [ + 'errors' => [ + 'sku-not-found' => 'Belirtilen SKU\'ya sahip ürün bulunamadı.', + ], + ], + ], + 'leads' => [ + 'title' => 'Müşteri Adayları', + 'validation' => [ + 'errors' => [ + 'id-not-found' => 'ID: \'%s\' sistemde bulunamadı.', + ], + ], + ], + ], + 'validation' => [ + 'errors' => [ + 'column-empty-headers' => '"%s" numaralı sütunların başlıkları boş.', + 'column-name-invalid' => 'Geçersiz sütun adları: "%s".', + 'column-not-found' => 'Gerekli sütunlar bulunamadı: %s.', + 'column-numbers' => 'Sütun sayısı başlıktaki satır sayısına karşılık gelmiyor.', + 'invalid-attribute' => 'Başlık geçersiz öznitelikler içeriyor: "%s".', + 'system' => 'Beklenmeyen bir sistem hatası oluştu.', + 'wrong-quotes' => 'Doğru olmayan tırnak işaretleri kullanıldı.', + 'already-exists' => 'The :attribute already exists.', + ], + ], + 'imports' => [ + 'create' => [ + 'action' => 'Eylem', + 'allowed-errors' => 'İzin Verilen Hatalar', + 'back-btn' => 'Geri', + 'create-update' => 'Oluştur/Güncelle', + 'delete' => 'Sil', + 'download-sample' => 'Örneği İndir', + 'field-separator' => 'Alan Ayırıcı', + 'file' => 'Dosya', + 'general' => 'Genel', + 'images-directory' => 'Resim Dizini Yolu', + 'process-in-queue' => 'Kuyrukta İşle', + 'results' => 'Sonuçlar', + 'save-btn' => 'İthalatı Kaydet', + 'settings' => 'Ayarlar', + 'skip-errors' => 'Hataları Atla', + 'stop-on-errors' => 'Hatalarda Durdur', + 'title' => 'İthalat Oluştur', + 'type' => 'Tür', + 'validation-strategy' => 'Doğrulama Stratejisi', + ], + 'edit' => [ + 'action' => 'Eylem', + 'allowed-errors' => 'İzin Verilen Hatalar', + 'back-btn' => 'Geri', + 'create-update' => 'Oluştur/Güncelle', + 'delete' => 'Sil', + 'download-sample' => 'Örneği İndir', + 'field-separator' => 'Alan Ayırıcı', + 'file' => 'Dosya', + 'general' => 'Genel', + 'images-directory' => 'Resim Dizini Yolu', + 'process-in-queue' => 'Kuyrukta İşle', + 'results' => 'Sonuçlar', + 'save-btn' => 'İthalatı Kaydet', + 'settings' => 'Ayarlar', + 'skip-errors' => 'Hataları Atla', + 'stop-on-errors' => 'Hatalarda Durdur', + 'title' => 'İthalatı Düzenle', + 'type' => 'Tür', + 'validation-strategy' => 'Doğrulama Stratejisi', + ], + 'index' => [ + 'button-title' => 'İthalat Oluştur', + 'title' => 'İthalatlar', + 'datagrid' => [ + 'actions' => 'Eylemler', + 'completed-at' => 'Tamamlandığı Zaman', + 'created' => 'Oluşturuldu', + 'delete' => 'Sil', + 'deleted' => 'Silindi', + 'edit' => 'Düzenle', + 'error-file' => 'Hata Dosyası', + 'id' => 'Kimlik', + 'started-at' => 'Başlama Zamanı', + 'state' => 'Durum', + 'summary' => 'Özet', + 'type' => 'Tür', + 'updated' => 'Güncellendi', + 'uploaded-file' => 'Yüklenen Dosya', + ], + ], + 'import' => [ + 'back-btn' => 'Geri', + 'completed-batches' => 'Tamamlanan Toplam Gruplar:', + 'download-error-report' => 'Tam Raporu İndir', + 'edit-btn' => 'Düzenle', + 'imported-info' => 'Tebrikler! İthalatınız başarılı oldu.', + 'importing-info' => 'İthalat İşlemde', + 'importing-queue-hint' => 'Bu içe aktarma %0 değerinde kalırsa, kuyruk işçisi çalışmıyor olabilir. Yöneticinizden \'php artisan queue:work\' komutunu çalıştırmasını isteyin veya \'Kuyrukta İşle\' seçeneğini kapatıp yeniden içe aktarın.', + 'indexing-info' => 'Kaynaklar İndeksleniyor (Fiyat, Stok ve Elastic Search) İlerliyor', + 'linking-info' => 'Kaynaklar Bağlanıyor', + 'progress' => 'İlerleme:', + 'title' => 'İthalat', + 'total-batches' => 'Toplam Gruplar:', + 'total-created' => 'Oluşturulan Toplam Kayıtlar:', + 'total-deleted' => 'Silinen Toplam Kayıtlar:', + 'total-errors' => 'Toplam Hatalar:', + 'total-invalid-rows' => 'Geçersiz Satırların Toplamı:', + 'total-rows-processed' => 'İşlenen Toplam Satırlar:', + 'total-updated' => 'Güncellenen Toplam Kayıtlar:', + 'validate' => 'Doğrula', + 'validate-info' => 'İthalatınızı kontrol etmek için Verileri Doğrula\'ya tıklayın.', + 'validating-info' => 'Veriler okunmaya ve doğrulanmaya başlandı', + 'validation-failed-info' => 'İthalatınız geçersiz. Lütfen aşağıdaki hataları düzeltin ve tekrar deneyin.', + 'validation-success-info' => 'İthalatınız geçerli. İthalat işlemini başlatmak için İthalat\'a tıklayın.', + ], + 'create-success' => 'İthalat başarıyla oluşturuldu.', + 'delete-failed' => 'İthalatı silme beklenmedik bir şekilde başarısız oldu.', + 'delete-success' => 'İthalat başarıyla silindi.', + 'not-valid' => 'İthalat geçersiz', + 'nothing-to-import' => 'İthal edilecek kaynak yok.', + 'queue-worker-not-running' => 'Kuyruk işçisi çalışmıyor. Bu içe aktarmayı işlemek için php artisan queue:work komutunu çalıştırın veya Kuyrukta İşle seçeneğini kapatıp tekrar deneyin.', + 'setup-queue-error' => 'İthalat işlemini başlatmak için kuyruk sürücünüzü "veritabanı" veya "redis" olarak değiştirin.', + 'update-success' => 'İthalat başarıyla güncellendi.', + ], + ], + ], + 'activities' => [ + 'index' => [ + 'title' => 'Etkinlikler', + 'datagrid' => [ + 'comment' => 'Yorum', + 'created_at' => 'Oluşturulma Tarihi', + 'created_by' => 'Oluşturan', + 'edit' => 'Düzenle', + 'id' => 'ID', + 'done' => 'Tamamlandı mı', + 'not-done' => 'Tamamlanmadı', + 'lead' => 'Müşteri', + 'mass-delete' => 'Toplu Sil', + 'mass-update' => 'Toplu Güncelle', + 'schedule-from' => 'Başlangıç Tarihi', + 'schedule-to' => 'Bitiş Tarihi', + 'schedule_from' => 'Başlangıç Tarihi', + 'schedule_to' => 'Bitiş Tarihi', + 'title' => 'Başlık', + 'is_done' => 'Tamamlandı mı', + 'type' => 'Tür', + 'update' => 'Güncelle', + 'call' => 'Arama', + 'meeting' => 'Toplantı', + 'lunch' => 'Öğle Yemeği', + ], + ], + 'edit' => [ + 'title' => 'Etkinliği Düzenle', + 'back-btn' => 'Geri', + 'save-btn' => 'Etkinliği Kaydet', + 'type' => 'Etkinlik Türü', + 'call' => 'Arama', + 'meeting' => 'Toplantı', + 'lunch' => 'Öğle Yemeği', + 'schedule_to' => 'Bitiş Tarihi', + 'schedule_from' => 'Başlangıç Tarihi', + 'location' => 'Konum', + 'comment' => 'Yorum', + 'lead' => 'Müşteri', + 'participants' => 'Katılımcılar', + 'general' => 'Genel', + 'persons' => 'Kişiler', + 'no-result-found' => 'Kayıt bulunamadı.', + 'users' => 'Kullanıcılar', + ], + 'updated' => 'Güncellendi :attribute', + 'created' => 'Oluşturuldu', + 'duration-overlapping' => 'Katılımcıların bu saatte başka bir toplantısı var. Devam etmek istiyor musunuz?', + 'create-success' => 'Etkinlik başarıyla oluşturuldu.', + 'update-success' => 'Etkinlik başarıyla güncellendi.', + 'overlapping-error' => 'Katılımcıların bu saatte başka bir toplantısı var.', + 'destroy-success' => 'Etkinlik başarıyla silindi.', + 'delete-failed' => 'Etkinlik silinemiyor.', + 'mass-update-success' => 'Etkinlikler başarıyla güncellendi.', + 'mass-destroy-success' => 'Etkinlikler başarıyla silindi.', + 'mass-delete-failed' => 'Etkinlikler silinemiyor.', + ], + 'mail' => [ + 'index' => [ + 'compose' => 'Oluştur', + 'draft' => 'Taslak', + 'inbox' => 'Gelen Kutusu', + 'outbox' => 'Giden Kutusu', + 'sent' => 'Gönderildi', + 'trash' => 'Çöp', + 'compose-mail-btn' => 'Mail Oluştur', + 'btn' => 'Mail', + 'mail' => [ + 'title' => 'Mail Oluştur', + 'to' => 'Kime', + 'enter-emails' => 'E-posta eklemek için enter tuşuna basın', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Konu', + 'send-btn' => 'Gönder', + 'message' => 'Mesaj', + 'draft' => 'Taslak', + ], + 'datagrid' => [ + 'id' => 'ID', + 'from' => 'Gönderen', + 'to' => 'Alıcı', + 'subject' => 'Konu', + 'tags' => 'Etiketler', + 'content' => 'Eklentiler', + 'attachments' => 'Attachments', + 'date' => 'Tarih', + 'move-to-inbox' => 'Gelen Kutusuna Taşı', + 'move-to-trash' => 'Çöp kutusuna taşındı', + 'edit' => 'Düzenle', + 'view' => 'Görüntüle', + 'delete' => 'Sil', + ], + ], + 'create-success' => 'E-posta başarıyla gönderildi.', + 'update-success' => 'E-posta başarıyla güncellendi.', + 'mass-update-success' => 'E-postalar başarıyla güncellendi.', + 'delete-success' => 'E-posta başarıyla silindi.', + 'delete-failed' => 'E-posta silinemedi.', + 'invalid-route' => 'Geçersiz rota için mail.', + 'unauthorized' => 'Bu işlem yetkilendirilmemiştir.', + 'view' => [ + 'title' => 'Mails', + 'subject' => ':subject', + 'link-mail' => 'Maili Bağla', + 'to' => 'Kime', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'reply' => 'Yanıtla', + 'reply-all' => 'Hepsine Yanıtla', + 'forward' => 'Yönlendir', + 'delete' => 'Sil', + 'enter-mails' => 'E-posta ID girin', + 'rotten-days' => ':days gün boyunca geçersiz', + 'search-an-existing-lead' => 'Mevcut bir müşteri arayın', + 'search-an-existing-contact' => 'Mevcut bir kişi arayın', + 'message' => 'Mesaj', + 'add-attachments' => 'Ek Ekle', + 'discard' => 'İptal Et', + 'send' => 'Gönder', + 'no-result-found' => 'Sonuç bulunamadı', + 'add-new-contact' => 'Yeni İletişim Ekle', + 'description' => 'Açıklama', + 'search' => 'Ara...', + 'add-new-lead' => 'Yeni Müşteri Ekle', + 'create-new-contact' => 'Yeni İletişim Oluştur', + 'save-contact' => 'İletişimi Kaydet', + 'create-lead' => 'Müşteri Oluştur', + 'linked-contact' => 'Bağlı Kişi', + 'link-to-contact' => 'Kişiye Bağla', + 'link-to-lead' => 'Müşteriye Bağla', + 'linked-lead' => 'Bağlı Müşteri', + 'lead-details' => 'Müşteri Detayları', + 'contact-person' => 'İletişim Kişisi', + 'product' => 'Ürün', + 'tags' => [ + 'create-success' => 'Etiket başarıyla oluşturuldu.', + 'destroy-success' => 'Etiket başarıyla silindi.', + ], + ], + ], + 'common' => [ + 'custom-attributes' => [ + 'add-more' => 'Daha Fazla Ekle', + 'address' => 'Adres', + 'city' => 'Şehir', + 'contact' => 'İletişim Numaraları', + 'country' => 'Ülke', + 'email' => 'E-posta', + 'home' => 'Ev', + 'postcode' => 'Posta Kodu', + 'save' => 'Kaydet', + 'select' => 'Seç', + 'select-country' => 'Ülke Seç', + 'select-state' => 'Eyalet Seç', + 'state' => 'Eyalet', + 'update-contact-title' => 'İletişim Numaralarını Güncelle', + 'update-emails-title' => 'İletişim E-postalarını Güncelle', + 'work' => 'İş', + ], + ], + 'leads' => [ + 'create-success' => 'Lead başarıyla oluşturuldu.', + 'update-success' => 'Lead başarıyla güncellendi.', + 'update-failed' => 'Potansiyel müşteriler silinemez.', + 'destroy-success' => 'Lead başarıyla silindi.', + 'destroy-failed' => 'Lead silinemedi.', + 'file' => [ + 'data-not-found' => 'Veri bulunamadı.', + 'empty-content' => 'PDF içeriği boş veya çıkarılamadı.', + 'failed-extract' => 'Dosyadan metin çıkarılamadı.', + 'insufficient-info' => 'Yetersiz veri nedeniyle, şu anda isteğinizi işleyemiyoruz.', + 'invalid-base64' => 'Geçersiz base64 formatı.', + 'invalid-format' => 'Geçersiz JSON formatı.', + 'invalid-response' => 'Geçersiz AI yanıt formatı.', + 'missing-api-key' => 'API anahtarı veya model yapılandırması eksik.', + 'not-found' => 'Dosya bulunamadı.', + 'recursive-call' => 'Özyinelemeli çağrı tespit edildi.', + 'text-generation-failed' => 'Metin çıkarma başarısız oldu. Dosya boş veya okunamaz olabilir.', + ], + 'index' => [ + 'title' => 'Leads', + 'create-btn' => 'Lead Oluştur', + 'datagrid' => [ + 'id' => 'ID', + 'sales-person' => 'Satış Temsilcisi', + 'subject' => 'Konu', + 'source' => 'Kaynak', + 'lead-value' => 'Tahmini Lead Değeri', + 'lead-type' => 'Potansiyel Müşteri Türü', + 'tag-name' => 'Etiket Adı', + 'contact-person' => 'İletişim Kişisi', + 'stage' => 'Aşama', + 'rotten-lead' => 'Çürümüş Lead', + 'date-to' => 'Bitiş Tarihi', + 'created-at' => 'Oluşturulma Tarihi', + 'no' => 'Hayır', + 'yes' => 'Evet', + 'delete' => 'Sil', + 'mass-delete' => 'Toplu Sil', + 'mass-update' => 'Toplu Güncelle', + ], + 'kanban' => [ + 'rotten-days' => 'Bu müşteri adayı :days gündür çürük', + 'empty-list' => 'Müşteri Adayı Listeniz Boş', + 'empty-list-description' => 'Hedeflerinizi düzenlemek için bir müşteri adayı oluşturun.', + 'create-lead-btn' => 'Müşteri Adayı Oluştur', + 'columns' => [ + 'contact-person' => 'İletişim Kişisi', + 'id' => 'ID', + 'lead-type' => 'Lead Türü', + 'lead-value' => 'Tahmini Lead Değeri', + 'sales-person' => 'Satış Temsilcisi', + 'source' => 'Kaynak', + 'title' => 'Başlık', + 'tags' => 'Etiketler', + 'expected-close-date' => 'Beklenen Kapanış Tarihi', + 'date-to' => 'Bitiş Tarihi', + 'created-at' => 'Oluşturulma Tarihi', + ], + 'toolbar' => [ + 'search' => [ + 'title' => 'Başlığa göre ara', + ], + 'filters' => [ + 'apply-filters' => 'Filtreleri Uygula', + 'clear-all' => 'Tümünü Temizle', + 'filter' => 'Filtre', + 'filters' => 'Filtreler', + 'from' => 'Kimden', + 'select' => 'Seç', + 'to' => 'Kime', + ], + 'card-settings' => [ + 'title' => 'Kart Ayarları', + 'contact-person' => 'İletişim Kişisi', + 'rotten-indicator' => 'Çürük Göstergesi', + 'sales-person' => 'Satış Temsilcisi', + 'estimated-lead-value' => 'Tahmini Lead Değeri', + 'source' => 'Kaynak', + 'lead-type' => 'Lead Türü', + 'tags' => 'Etiketler', + ], + ], + 'stages' => [ + 'won' => 'Kazandı', + 'lost' => 'Kayıp', + 'need-more-info' => 'Daha Fazla Bilgi Gerekiyor', + 'closed-at' => 'Kapanış Tarihi', + 'won-value' => 'Kazanan Değer', + 'lost-reason' => 'Kayıp Nedeni', + 'save-btn' => 'Kaydet', + ], + ], + 'view-switcher' => [ + 'all-pipelines' => 'Tüm Boru Hatları', + 'create-new-pipeline' => 'Yeni Boru Hattı Oluştur', + ], + 'upload' => [ + 'create-lead' => 'AI Kullanarak Lead Oluştur', + 'file' => 'Dosya yükleme', + 'file-info' => 'Yalnızca pdf, bmp, jpg, jpeg, png formatındaki dosyalar kabul edilir.', + 'file-required' => 'Devam etmek için lütfen en az bir geçerli dosya seçin.', + 'save-btn' => 'Kaydet', + 'upload-file' => 'Dosya yükle', + ], + ], + 'create' => [ + 'title' => 'Lead Oluştur', + 'save-btn' => 'Kaydet', + 'details' => 'Detaylar', + 'details-info' => 'Lead\'in Temel Bilgilerini Girin', + 'contact-person' => 'İletişim Kişisi', + 'contact-info' => 'İletişim Kişisi Hakkında Bilgiler', + 'products' => 'Ürünler', + 'products-info' => 'Ürünler Hakkında Bilgiler', + ], + 'edit' => [ + 'title' => 'Lead\'i Düzenle', + 'save-btn' => 'Kaydet', + 'details' => 'Detaylar', + 'details-info' => 'Lead\'in Temel Bilgilerini Girin', + 'contact-person' => 'İletişim Kişisi', + 'contact-info' => 'İletişim Kişisi Hakkında Bilgiler', + 'products' => 'Ürünler', + 'products-info' => 'Ürünler Hakkında Bilgiler', + ], + 'common' => [ + 'contact' => [ + 'name' => 'Ad', + 'name-search-placeholder' => 'Ad, e-posta ve numaraya göre ara', + 'email' => 'E-posta', + 'contact-number' => 'İletişim Numarası', + 'organization' => 'Kuruluş', + ], + 'products' => [ + 'product-name' => 'Ürün Adı', + 'quantity' => 'Miktar', + 'price' => 'Fiyat', + 'amount' => 'Tutar', + 'action' => 'Eylem', + 'add-more' => 'Daha Fazla Ekle', + 'total' => 'Toplam', + ], + ], + 'view' => [ + 'title' => 'Lead: :title', + 'rotten-days' => ':days Gün', + 'tabs' => [ + 'description' => 'Açıklama', + 'products' => 'Ürünler', + 'quotes' => 'Teklifler', + ], + 'attributes' => [ + 'title' => 'Lead Hakkında', + ], + 'quotes' => [ + 'id' => 'ID', + 'subject' => 'Konu', + 'expired-at' => 'Son Tarih', + 'sub-total' => 'Ara Toplam', + 'discount' => 'İndirim', + 'tax' => 'Vergi', + 'adjustment' => 'Düzeltme', + 'grand-total' => 'Genel Toplam', + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'download' => 'İndir', + 'destroy-success' => 'Teklif başarıyla silindi.', + 'empty-title' => 'Teklif Bulunamadı', + 'empty-info' => 'Bu Lead için Teklif Bulunamadı', + 'add-btn' => 'Teklif Ekle', + 'mail' => 'Mail', + 'quote-not-found' => 'Bu lead için teklif bulunamadı.', + 'person-email-unavailable' => 'Lead kişisinin e-posta adresi mevcut değil.', + 'mail-subject' => 'Teklif: :subject', + 'mail-body' => 'Please find the attached quote PDF. Mail Generated by System.', + 'mail-send-failed' => 'Teklif e-postası gönderilemedi.', + ], + 'products' => [ + 'product-name' => 'Ürün Adı', + 'quantity' => 'Miktar', + 'price' => 'Fiyat', + 'amount' => 'Tutar', + 'action' => 'Eylem', + 'add-more' => 'Daha Fazla Ekle', + 'total' => 'Toplam', + 'empty-title' => 'Ürün Bulunamadı', + 'empty-info' => 'Bu Lead için Ürün Bulunamadı', + 'add-product' => 'Ürün Ekle', + ], + 'persons' => [ + 'title' => 'Kişiler Hakkında', + 'job-title' => ':job_title @ :organization', + 'no-person' => 'Bu potansiyel müşteriye atanmış bir iletişim kişisi yok.', + 'attach-btn' => 'İletişim Kişisi Ekle', + 'change-btn' => 'Değiştir', + 'change-title' => 'İletişim Kişisini Değiştir', + 'save-btn' => 'Kaydet', + 'cancel-btn' => 'İptal', + ], + 'stages' => [ + 'won' => 'Kazandı', + 'lost' => 'Kayıp', + 'need-more-info' => 'Daha Fazla Bilgi Gerekiyor', + 'closed-at' => 'Kapanış Tarihi', + 'won-value' => 'Kazanan Değer', + 'lost-reason' => 'Kayıp Nedeni', + 'save-btn' => 'Kaydet', + ], + 'tags' => [ + 'create-success' => 'Etiket başarıyla oluşturuldu.', + 'destroy-success' => 'Etiket başarıyla silindi.', + ], + ], + ], + 'configuration' => [ + 'index' => [ + 'back' => 'Geri', + 'delete' => 'Sil', + 'save-btn' => 'Yapılandırmayı Kaydet', + 'save-success' => 'Yapılandırma Başarıyla Kaydedildi.', + 'search' => 'Ara', + 'select-country' => 'Ülke Seç', + 'select-state' => 'Eyalet Seç', + 'title' => 'Yapılandırma', + 'general' => [ + 'title' => 'Genel', + 'info' => 'Genel Yapılandırma', + 'general' => [ + 'title' => 'Genel', + 'info' => 'Genel ayarlarınızı burada güncelleyin.', + 'locale-settings' => [ + 'title' => 'Yerel Ayarlar', + 'title-info' => 'Yönetim paneli arayüzünde kullanılan dili tanımlar. Açılır liste, kurulumunuzda mevcut olan tüm dilleri gösterir.', + ], + 'admin-logo' => [ + 'logo-image' => 'Logo Resmi', + 'title' => 'Yönetici Logosu', + 'title-info' => 'Yönetici paneliniz için logo resmini yapılandırın.', + 'favicon-image' => 'Favicon Resmi', + 'favicon-image-info' => 'Yönetici paneliniz için favicon resmini yapılandırın.', + ], + ], + 'settings' => [ + 'title' => 'Ayarlar', + 'info' => 'Ayarlarınızı burada güncelleyin.', + 'footer' => [ + 'show' => '"Powered By" Göster', + 'show-info' => 'Yönetim panelinin altındaki "Powered by" çubuğunu gösterir veya gizler.', + 'info' => 'Powered by bölümünü burada yapılandırabiliriz.', + 'powered-by' => 'Powered by metin düzenleyici', + 'title' => 'Powered by Bölüm Yapılandırmaları', + ], + 'menu' => [ + 'activities' => 'Aktiviteler', + 'configuration' => 'Yapılandırma', + 'contacts' => 'İletişim', + 'dashboard' => 'Gösterge Paneli', + 'draft' => 'Taslak', + 'inbox' => 'Gelen Kutusu', + 'info' => 'Menü öğelerinin adlarını burada yapılandırabiliriz.', + 'leads' => 'Leadler', + 'mail' => 'Mail', + 'organizations' => 'Organizasyonlar', + 'outbox' => 'Gönderilenler', + 'persons' => 'Kişiler', + 'products' => 'Ürünler', + 'quotes' => 'Teklifler', + 'sent' => 'Gönderildi', + 'settings' => 'Ayarlar', + 'title' => 'Menü Öğesi Yapılandırmaları', + 'trash' => 'Çöp Kutusu', + ], + 'dashboard' => [ + 'title' => 'Kontrol Paneli Yapılandırmaları', + 'info' => 'Kontrol panelinin varsayılan tarih aralığını burada yapılandırabiliriz.', + 'date-range' => 'Varsayılan Tarih Aralığı', + 'date-range-info' => 'Kontrol panelinin yüklendiği dönem. Kontrol panelindeki tarih seçicileri tek seferlik görünüm için yine de değiştirilebilir.', + 'custom-days' => 'Özel Aralık (Gün)', + 'custom-days-info' => 'Tarih aralığı Özel olarak ayarlandığında geriye dönük bakılacak gün sayısı.', + 'ranges' => [ + '1-month' => '1 Ay', + '3-months' => '3 Ay', + '9-months' => '9 Ay', + '1-year' => '1 Yıl', + '2-years' => '2 Yıl', + 'custom' => 'Özel', + ], + ], + 'menu-color' => [ + 'brand-color' => 'Marka Rengi', + 'info' => 'Menü öğelerinin renklerini burada değiştirebiliriz.', + 'title' => 'Menü Öğesi Renk Yapılandırmaları', + ], + ], + ], + 'email' => [ + 'title' => 'Email Ayarları', + 'info' => 'Uygulama için email yapılandırması.', + 'imap' => [ + 'title' => 'IMAP Ayarları', + 'info' => 'IMAP email yapılandırması için.', + 'account' => [ + 'title' => 'IMAP Hesabı', + 'title-info' => 'IMAP hesap ayarlarınızı burada yapılandırın.', + 'host' => 'Host', + 'port' => 'Port', + 'encryption' => 'Şifreleme Türü', + 'validate-cert' => 'SSL Sertifikasını Doğrula', + 'username' => 'IMAP Kullanıcı Adı', + 'password' => 'IMAP Şifresi', + ], + ], + ], + 'magic-ai' => [ + 'title' => 'Sihirli AI', + 'info' => 'Uygulama için Sihirli AI yapılandırması.', + 'settings' => [ + 'api-key' => 'API Anahtarı', + 'api-key-info' => 'Her model için bir OpenRouter API anahtarı kullanmayı unutmayın. Bu, güvenliği ve performansı artırmak için basit bir adımdır.', + 'enable' => 'Etkinleştir', + 'info' => 'OpenRouter API Anahtarınız ile Magic AI deneyiminizi geliştirin. Şimdi entegre edin ve size özel, sorunsuz bir AI macerası yaşayın! Ayarları kolayca özelleştirin ve AI yolculuğunuzun kontrolünü elinize alın.', + 'other' => 'Diğer Model', + 'other-model' => 'Diğer modeller için OpenRouter\'dan Model ID kullanın.', + 'doc-generation' => 'DOC Oluşturma', + 'doc-generation-info' => 'DOC dosyalarından verileri otomatik olarak çıkartıp metin formatına dönüştürmek için DOC Oluşturma özelliğini etkinleştirin. Bu özelliği etkinleştirerek iş akışınızı kolaylaştırın ve verimliliğinizi artırın.', + 'title' => 'Genel Ayarlar', + 'models' => [ + 'deepseek-r1' => 'Deepseek R1 Distill-llama-8b', + 'gemini-2-0-flash-001' => 'Gemini 2.0 flash-001', + 'gpt-4o' => 'GPT-4.0', + 'gpt-4o-mini' => 'GPT-4.0 mini', + 'grok-2-1212' => 'Grok 2.12', + 'llama-3-2-3b-instruct' => 'Llama 3.2 3b Instruct', + 'title' => 'Modeller', + ], + ], + ], + ], + ], + 'dashboard' => [ + 'index' => [ + 'title' => 'Gösterge Paneli', + 'start-date' => 'Başlangıç Tarihi', + 'end-date' => 'Bitiş Tarihi', + 'export-pdf' => 'PDF Dışa Aktar', + 'pdf' => [ + 'title' => 'Gösterge Paneli Raporu', + 'filtered-date' => 'Filtrelenmiş Tarih', + 'revenue' => 'Gelir', + 'overview' => 'Genel Bakış', + 'total-leads' => 'Toplam Lead', + 'metric' => 'Metrik', + 'current' => 'Mevcut', + 'previous' => 'Önceki', + 'progress' => 'İlerleme', + 'date' => 'Tarih', + 'all' => 'Tümü', + 'total' => 'Toplam', + 'won' => 'Kazanılan', + 'lost' => 'Kaybedilen', + 'source' => 'Kaynak', + 'type' => 'Tür', + 'stage' => 'Aşama', + 'product' => 'Ürün', + 'person' => 'Kişi', + 'price' => 'Fiyat', + 'revenue-by-sources' => 'Kaynaklara Göre Gelir', + 'revenue-by-types' => 'Türlere Göre Gelir', + 'open-leads-by-stages' => 'Aşamalara Göre Açık Leadler', + 'top-selling-products' => 'En Çok Satılan Ürünler', + 'top-persons' => 'En İyi Kişiler', + ], + 'revenue' => [ + 'lost-revenue' => 'Kayıp Gelir', + 'won-revenue' => 'Kazançlı Gelir', + ], + 'over-all' => [ + 'average-lead-value' => 'Ortalama Lead Değeri', + 'total-leads' => 'Toplam Lead', + 'average-leads-per-day' => 'Günlük Ortalama Lead', + 'total-quotations' => 'Toplam Teklif', + 'total-persons' => 'Toplam Kişi', + 'total-organizations' => 'Toplam Organizasyon', + ], + 'total-leads' => [ + 'title' => 'Leadler', + 'total' => 'Toplam Lead', + 'won' => 'Kazanan Leadler', + 'lost' => 'Kayıp Leadler', + ], + 'revenue-by-sources' => [ + 'title' => 'Kaynaklara Göre Gelir', + 'empty-title' => 'Veri Bulunamadı', + 'empty-info' => 'Seçilen aralık için veri bulunamadı', + ], + 'revenue-by-types' => [ + 'title' => 'Türlere Göre Gelir', + 'empty-title' => 'Veri Bulunamadı', + 'empty-info' => 'Seçilen aralık için veri bulunamadı', + ], + 'top-selling-products' => [ + 'title' => 'En Çok Satılan Ürünler', + 'empty-title' => 'Ürün Bulunamadı', + 'empty-info' => 'Seçilen aralık için ürün bulunamadı', + ], + 'top-persons' => [ + 'title' => 'En İyi Kişiler', + 'empty-title' => 'Kişi Bulunamadı', + 'empty-info' => 'Seçilen aralık için kişi bulunamadı', + ], + 'open-leads-by-states' => [ + 'title' => 'Aşamalara Göre Açık Leadler', + 'empty-title' => 'Veri Bulunamadı', + 'empty-info' => 'Seçilen aralık için veri bulunamadı', + ], + ], + ], + 'help' => [ + 'index' => [ + 'title' => 'Yardım ve Kaynaklar', + 'description' => 'Krayin Admin panelinden en iyi şekilde yararlanmanız için gereken her şey — barındırma, destek ve profesyonel hizmetlerin yanı sıra eklentiler ve geliştirici dokümantasyonu.', + 'contact-us' => 'Bize ulaşın', + 'view-all' => 'Tümünü gör', + 'still-need-help-title' => 'Hâlâ yardıma mı ihtiyacınız var?', + 'still-need-help-description' => 'Barındırma, özel geliştirme veya başka her konuda Krayin ekibiyle görüşün.', + + 'services' => [ + 'title' => 'Hizmetler ve Kaynaklar', + 'cloud-hosting' => [ + 'title' => 'Bulut Barındırma', + 'description' => 'Uygun maliyetli, yönetilen bulut barındırma — Krayin uygulamasını dakikalar içinde bulutta deneyip yayına alın; tamamen optimize edilmiş ve ölçeklenebilir.', + ], + 'support' => [ + 'title' => 'Destek ve Bakım', + 'description' => 'CRM sisteminizi güvenli, güncel ve sorunsuz tutmak için özel teknik destek ve sürekli bakım planları.', + ], + 'extensions' => [ + 'title' => 'Eklentiler', + 'description' => 'Krayin uygulamasını yeni bağlayıcılar, kanallar ve özelliklerle genişletmek için resmi ve topluluk eklentilerine göz atın.', + ], + ], + + 'resources' => [ + 'title' => 'Dokümantasyon', + 'user-docs' => [ + 'title' => 'Kullanıcı Kılavuzu', + 'description' => 'Modül entegrasyonu, özelleştirme, veri taşıma, sürüm yükseltmeleri ve özel geliştirme için uzman yardımı.', + ], + 'dev-docs' => [ + 'title' => 'Geliştirici Dokümanları ve Bloglar', + 'description' => 'Oluşturmanıza, yapılandırmanıza ve güncel kalmanıza yardımcı olacak geliştirici kılavuzları, eğitimler ve en yeni makaleler.', + ], + 'api-docs' => [ + 'title' => 'API Dokümanları', + 'description' => 'Krayin uygulamasını sisteminize entegre etmek için uç noktalar, kimlik doğrulama ve örnekler içeren eksiksiz REST API referansı.', + ], + ], + + 'community' => [ + 'forums' => [ + 'title' => 'Topluluk Forumları', + 'description' => 'İpuçlarını paylaşmak ve sorunları çözmek için binlerce Krayin kullanıcısı ve geliştiricisiyle bağlantı kurun.', + 'link' => 'Topluluğa katılın', + ], + 'tutorials' => [ + 'title' => 'Video Eğitimleri', + 'description' => 'CRM kurulumu, pipeline yapılandırması ve kullanıcı yönetimi hakkında adım adım video kılavuzlarını izleyin.', + 'link' => 'YouTube kanalına göz atın', + ], + ], + ], + ], + 'layouts' => [ + 'app-version' => 'Sürüm: :version', + 'dashboard' => 'Gösterge Paneli', + 'help' => 'Yardım ve Kaynaklar', + 'leads' => 'Leadler', + 'quotes' => 'Teklifler', + 'quote' => 'Teklif', + 'mail' => [ + 'title' => 'Mail', + 'compose' => 'Yeni Mesaj', + 'inbox' => 'Gelen Kutusu', + 'draft' => 'Taslak', + 'outbox' => 'Gönderilenler', + 'sent' => 'Gönderildi', + 'trash' => 'Çöp Kutusu', + 'setting' => 'Ayar', + ], + 'activities' => 'Aktiviteler', + 'contacts' => 'İletişim', + 'persons' => 'Kişiler', + 'person' => 'Kişi', + 'organizations' => 'Organizasyonlar', + 'organization' => 'Organizasyon', + 'products' => 'Ürünler', + 'product' => 'Ürün', + 'settings' => 'Ayarlar', + 'user' => 'Kullanıcı', + 'user-info' => 'CRM’de tüm kullanıcılarınızı ve yetkilerini yönetin, ne yapmalarına izin verildiğini belirleyin.', + 'groups' => 'Gruplar', + 'groups-info' => 'CRM’den grupları ekleyin, düzenleyin veya silin', + 'roles' => 'Roller', + 'role' => 'Rol', + 'roles-info' => 'CRM’den rolleri ekleyin, düzenleyin veya silin', + 'users' => 'Kullanıcılar', + 'users-info' => 'CRM’den kullanıcıları ekleyin, düzenleyin veya silin', + 'lead' => 'Lead', + 'lead-info' => 'CRM’de tüm lead ayarlarınızı yönetin', + 'sidebar' => [ + 'collapse' => 'Daralt', + 'expand' => 'Genişlet', + ], + 'quick-add' => [ + 'title' => 'Hızlı Ekle', + 'save' => 'Kaydet', + 'to' => 'Kime', + 'subject' => 'Konu', + 'message' => 'Mesaj', + ], + 'pipelines' => 'Pipeline’lar', + 'pipelines-info' => 'CRM’den pipeline’ları ekleyin, düzenleyin veya silin', + 'sources' => 'Kaynaklar', + 'sources-info' => 'CRM’den kaynakları ekleyin, düzenleyin veya silin', + 'types' => 'Türler', + 'types-info' => 'CRM’den türleri ekleyin, düzenleyin veya silin', + 'automation' => 'Otomasyon', + 'automation-info' => 'CRM’de tüm otomasyon ayarlarınızı yönetin', + 'attributes' => 'Nitelikler', + 'attribute' => 'Nitelik', + 'attributes-info' => 'CRM’den nitelikleri ekleyin, düzenleyin veya silin', + 'email-templates' => 'E-posta Şablonları', + 'email' => 'E-posta', + 'email-templates-info' => 'CRM’den e-posta şablonları ekleyin, düzenleyin veya silin', + 'events' => 'Etkinlikler', + 'events-info' => 'CRM üzerinden etkinlikleri ekleyin, düzenleyin veya silin', + 'campaigns' => 'Kampanyalar', + 'campaigns-info' => 'CRM üzerinden kampanyaları ekleyin, düzenleyin veya silin', + 'workflows' => 'İş Akışları', + 'workflows-info' => 'CRM’den iş akışlarını ekleyin, düzenleyin veya silin', + 'webhooks' => 'Webhooklar', + 'webhooks-info' => 'CRM’den webhookları ekleyin, düzenleyin veya silin', + 'other-settings' => 'Diğer Ayarlar', + 'other-settings-info' => 'CRM’de tüm ekstra ayarlarınızı yönetin', + 'tags' => 'Etiketler', + 'tags-info' => 'CRM’den etiketleri ekleyin, düzenleyin veya silin', + 'my-account' => 'Hesabım', + 'sign-out' => 'Çıkış Yap', + 'back' => 'Geri', + 'name' => 'Ad', + 'configuration' => 'Yapılandırma', + 'howdy' => 'Merhaba!', + 'warehouses' => 'Depolar', + 'warehouse' => 'Depo', + 'warehouses-info' => 'CRM’den depoları ekleyin, düzenleyin veya silin', + 'data_transfer' => 'Veri Transferi', + 'data_transfer_info' => 'CRM’de kişiler, ürünler ve potansiyel müşterilere ilişkin veri transferi ayarlarını yönetin', + 'inventory' => 'Envanter', + 'inventory-info' => 'CRM’deki tüm envanter ayarlarını yönetin', + ], + 'user' => [ + 'account' => [ + 'name' => 'İsim', + 'email' => 'E-posta', + 'password' => 'Şifre', + 'my_account' => 'Hesabım', + 'update_details' => 'Bilgileri Güncelle', + 'current_password' => 'Mevcut şifre', + 'confirm_password' => 'Şifreyi onayla', + 'password-match' => 'Mevcut şifre eşleşmiyor.', + 'account-save' => 'Hesap değişiklikleri başarıyla kaydedildi.', + 'permission-denied' => 'İzin Reddedildi', + 'remove-image' => 'Görseli Kaldır', + 'upload_image_pix' => 'Profil Görseli Yükle (100px x 100px)', + 'upload_image_format' => 'PNG veya JPG Formatında', + 'image_upload_message' => 'Sadece görseller (.jpeg, .jpg, .png, ..) izinlidir.', + ], + ], + 'emails' => [ + 'common' => [ + 'dear' => 'Sevgili :name', + 'cheers' => 'Saygılar,
    :app_name Ekibi', + 'user' => [ + 'dear' => 'Sevgili :username', + 'create-subject' => 'Bir üye olarak eklendiniz.', + 'create-body' => 'Tebrikler! Artık ekibimizin bir üyesisiniz.', + 'forget-password' => [ + 'subject' => 'Müşteri Şifre Sıfırlama', + 'dear' => 'Sevgili :username', + 'reset-password' => 'Şifreyi Sıfırla', + 'info' => 'Bu e-postayı almanız, hesabınız için bir şifre sıfırlama talebi aldığımız anlamına gelir', + 'final-summary' => 'Eğer şifre sıfırlama talebinde bulunmadıysanız, herhangi bir ek işlem yapmanıza gerek yoktur', + 'thanks' => 'Teşekkürler!', + ], + ], + ], + ], + 'validations' => [ + 'message' => [ + 'code' => 'Alan geçerli bir kod olmalıdır.', + 'decimal' => ':attribute ondalıklı bir sayı olmalıdır.', + ], + ], + 'errors' => [ + 'dashboard' => 'Kontrol Paneli', + 'go-back' => 'Geri Dön', + 'support' => 'Sorun devam ederse, yardım için bize :email adresinden ulaşın.', + 'unauthorized' => 'Bu işlem için yetkiniz yok.', + 'role-exceeds-own' => 'You can only assign a role whose permissions are the same as or fewer than your own.', + 'user-exceeds-own' => 'You can only manage users whose permissions are the same as or fewer than your own.', + 'own-privileges' => 'You cannot change your own role or data scope.', + 'scope-exceeds-own' => 'You cannot grant a data scope wider than your own.', + 'primary-admin-protected' => 'The primary administrator account is protected and cannot be modified here.', + 'role-permissions-exceed-own' => 'You can only create or edit roles whose permissions are the same as or fewer than your own.', + + '404' => [ + 'description' => 'Oops! Aradığınız sayfa tatilde. Aradığınız şeyi bulamadık gibi görünüyor.', + 'title' => '404 Sayfa Bulunamadı', + ], + '401' => [ + 'description' => 'Oops! Bu sayfaya erişim izniniz yok gibi görünüyor. Gerekli yetkilere sahip değilsiniz.', + 'title' => '401 Yetkisiz', + ], + '403' => [ + 'description' => 'Oops! Bu sayfa erişime kapalı. Bu içeriği görüntülemek için gerekli izinlere sahip değilsiniz gibi görünüyor.', + 'title' => '403 Yasak', + ], + '500' => [ + 'description' => 'Oops! Bir şeyler ters gitti. Aradığınız sayfa yüklenirken sorun yaşıyoruz gibi görünüyor.', + 'title' => '500 Sunucu Hatası', + ], + '503' => [ + 'description' => 'Oops! Görünüşe göre geçici bir bakım nedeniyle kapalıyız. Lütfen kısa süre sonra tekrar kontrol edin.', + 'title' => '503 Hizmet Kullanılamıyor', + ], + ], + 'export' => [ + 'csv' => 'CSV', + 'download' => 'İndir', + 'export' => 'Dışa Aktar', + 'google-contacts' => 'Google Kişileri', + 'google-contacts-in-progress' => 'Kişileriniz Google\'a aktarılıyor... bu biraz zaman alabilir.', + 'google-contacts-total' => 'Toplam Kişi:', + 'google-contacts-exported' => 'Aktarıldı:', + 'google-contacts-duplicate' => 'Zaten Vardı:', + 'google-contacts-failed' => 'Başarısız:', + 'google-contacts-summary' => 'Aktarım tamamlandı: :exported aktarıldı, :duplicate zaten vardı, :failed başarısız.', + 'no-records' => 'Dışa aktarılacak kayıt bulunamadı.', + 'xls' => 'XLS', + 'xlsx' => 'XLSX', + ], +]; diff --git a/packages/Webkul/Admin/src/Resources/lang/vi/app.php b/packages/Webkul/Admin/src/Resources/lang/vi/app.php new file mode 100644 index 0000000..c24487f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/lang/vi/app.php @@ -0,0 +1,2481 @@ + [ + 'help' => 'Trợ giúp', + 'leads' => 'Những Khách hàng tiềm năng', + 'lead' => 'Khách hàng tiềm năng', + 'quotes' => 'Báo giá', + 'mail' => 'Thư', + 'inbox' => 'Hộp thư đến', + 'draft' => 'Thư nháp', + 'outbox' => 'Hộp thư đi', + 'sent' => 'Đã gửi', + 'trash' => 'Thùng rác', + 'activities' => 'Hoạt động', + 'webhook' => 'Webhook', + 'contacts' => 'Danh bạ', + 'persons' => 'Cá nhân', + 'organizations' => 'Tổ chức', + 'products' => 'Sản phẩm', + 'settings' => 'Cài đặt', + 'groups' => 'Nhóm', + 'roles' => 'Vai trò', + 'users' => 'Người dùng', + 'user' => 'Người dùng', + 'automation' => 'Tự động hóa', + 'attributes' => 'Thuộc tính', + 'pipelines' => 'Quy trình', + 'sources' => 'Nguồn', + 'types' => 'Loại', + 'email-templates' => 'Mẫu email', + 'workflows' => 'Quy trình làm việc', + 'other-settings' => 'Cài đặt khác', + 'tags' => 'Thẻ', + 'configuration' => 'Cấu hình', + 'create' => 'Tạo mới', + 'quick_add' => 'Thêm Nhanh', + 'edit' => 'Chỉnh sửa', + 'view' => 'Xem', + 'print' => 'In', + 'delete' => 'Xóa', + 'export' => 'Xuất khẩu', + 'mass-delete' => 'Xóa hàng loạt', + 'data-transfer' => 'Data Transfer', + 'imports' => 'Imports', + 'import' => 'Import', + 'event' => 'Sự kiện', + 'campaigns' => 'Chiến dịch', + 'warehouses' => 'Kho', + 'inventory' => 'Hàng tồn kho', + ], + 'users' => [ + 'activate-warning' => 'Tài khoản của bạn chưa được kích hoạt. Vui lòng liên hệ quản trị viên.', + 'login-error' => 'Thông tin đăng nhập không khớp với hồ sơ của chúng tôi.', + 'not-permission' => 'Bạn không có quyền truy cập vào bảng quản trị.', + 'login' => [ + 'email' => 'Địa chỉ Email', + 'forget-password-link' => 'Quên Mật khẩu?', + 'password' => 'Mật khẩu', + 'submit-btn' => 'Đăng Nhập', + 'title' => 'Đăng Nhập', + ], + 'forget-password' => [ + 'create' => [ + 'email' => 'Email Đã Đăng Ký', + 'email-not-exist' => 'Email Không Tồn Tại', + 'page-title' => 'Quên Mật khẩu', + 'reset-link-sent' => 'Liên kết đặt lại mật khẩu đã được gửi', + 'sign-in-link' => 'Quay lại Đăng Nhập?', + 'submit-btn' => 'Đặt Lại', + 'title' => 'Khôi Phục Mật khẩu', + ], + ], + 'reset-password' => [ + 'back-link-title' => 'Quay lại Đăng Nhập?', + 'confirm-password' => 'Xác Nhận Mật Khẩu', + 'email' => 'Email Đã Đăng Ký', + 'password' => 'Mật Khẩu', + 'submit-btn' => 'Đặt Lại Mật Khẩu', + 'title' => 'Đặt Lại Mật Khẩu', + ], + ], + 'account' => [ + 'edit' => [ + 'back-btn' => 'Quay Lại', + 'change-password' => 'Đổi Mật Khẩu', + 'confirm-password' => 'Xác Nhận Mật Khẩu', + 'current-password' => 'Mật Khẩu Hiện Tại', + 'email' => 'Email', + 'general' => 'Chung', + 'invalid-password' => 'Mật khẩu hiện tại bạn nhập không đúng.', + 'name' => 'Tên', + 'password' => 'Mật Khẩu', + 'profile-image' => 'Ảnh Hồ Sơ', + 'save-btn' => 'Lưu Tài Khoản', + 'title' => 'Tài Khoản Của Tôi', + 'update-success' => 'Tài khoản đã được cập nhật thành công', + 'upload-image-info' => 'Tải lên Ảnh Hồ Sơ (110px X 110px) ở định dạng PNG hoặc JPG', + ], + ], + 'components' => [ + 'activities' => [ + 'actions' => [ + 'mail' => [ + 'btn' => 'Thư', + 'title' => 'Soạn thư', + 'to' => 'Tới', + 'enter-emails' => 'Nhấn enter để thêm email', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Chủ đề', + 'send-btn' => 'Gửi', + 'message' => 'Tin nhắn', + ], + 'file' => [ + 'btn' => 'Tệp', + 'title' => 'Thêm tệp', + 'title-control' => 'Tiêu đề', + 'name' => 'Tên', + 'description' => 'Mô tả', + 'file' => 'Tệp', + 'save-btn' => 'Lưu tệp', + ], + 'note' => [ + 'btn' => 'Ghi chú', + 'title' => 'Thêm ghi chú', + 'comment' => 'Bình luận', + 'save-btn' => 'Lưu ghi chú', + ], + 'activity' => [ + 'btn' => 'Hoạt động', + 'title' => 'Thêm hoạt động', + 'title-control' => 'Tiêu đề', + 'description' => 'Mô tả', + 'schedule-from' => 'Lịch từ', + 'schedule-to' => 'Lịch đến', + 'location' => 'Địa điểm', + 'call' => 'Cuộc gọi', + 'meeting' => 'Cuộc họp', + 'lunch' => 'Bữa trưa', + 'save-btn' => 'Lưu hoạt động', + 'participants' => [ + 'title' => 'Người tham gia', + 'placeholder' => 'Nhập để tìm kiếm người tham gia', + 'users' => 'Người dùng', + 'persons' => 'Người', + 'no-results' => 'Không có kết quả...', + ], + ], + ], + 'index' => [ + 'all' => 'Tất cả', + 'bcc' => 'Bcc', + 'by-user' => 'Bởi :user', + 'calls' => 'Cuộc gọi', + 'cc' => 'Cc', + 'change-log' => 'Nhật ký thay đổi', + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'emails' => 'Email', + 'empty' => 'Trống', + 'files' => 'Tệp tin', + 'from' => 'Từ', + 'location' => 'Địa điểm', + 'lunches' => 'Bữa trưa', + 'mark-as-done' => 'Đánh dấu hoàn thành', + 'meetings' => 'Cuộc họp', + 'notes' => 'Ghi chú', + 'participants' => 'Người tham gia', + 'planned' => 'Đã lên kế hoạch', + 'quotes' => 'Báo giá', + 'scheduled-on' => 'Lên lịch vào', + 'system' => 'Hệ thống', + 'to' => 'Đến', + 'unlink' => 'Gỡ liên kết', + 'view' => 'Xem', + 'empty-placeholders' => [ + 'all' => [ + 'title' => 'Không tìm thấy hoạt động nào', + 'description' => 'Không có hoạt động nào được tìm thấy. Bạn có thể thêm hoạt động bằng cách nhấn nút Hoạt động ở bảng bên trái.', + ], + 'planned' => [ + 'title' => 'Không tìm thấy hoạt động đã lên kế hoạch', + 'description' => 'Không có hoạt động đã lên kế hoạch nào được tìm thấy. Thêm hoạt động bằng cách nhấn nút Hoạt động ở bảng bên trái.', + ], + 'notes' => [ + 'title' => 'Không tìm thấy ghi chú', + 'description' => 'Không có ghi chú nào được tìm thấy. Thêm ghi chú bằng cách nhấn nút Ghi chú ở bảng bên trái.', + ], + 'calls' => [ + 'title' => 'Không tìm thấy cuộc gọi', + 'description' => 'Không có cuộc gọi nào được tìm thấy. Thêm cuộc gọi bằng cách nhấn nút Hoạt động và chọn loại Cuộc gọi.', + ], + 'meetings' => [ + 'title' => 'Không tìm thấy cuộc họp', + 'description' => 'Không có cuộc họp nào được tìm thấy. Thêm cuộc họp bằng cách nhấn nút Hoạt động và chọn loại Cuộc họp.', + ], + 'lunches' => [ + 'title' => 'Không tìm thấy buổi ăn trưa', + 'description' => 'Không có buổi ăn trưa nào được tìm thấy. Thêm ăn trưa bằng cách nhấn nút Hoạt động và chọn loại Ăn trưa.', + ], + 'files' => [ + 'title' => 'Không tìm thấy tệp', + 'description' => 'Không có tệp nào được tìm thấy. Thêm tệp bằng cách nhấn nút Tệp ở bảng bên trái.', + ], + 'emails' => [ + 'title' => 'Không tìm thấy email', + 'description' => 'Không có email nào được tìm thấy. Thêm email bằng cách nhấn nút Thư ở bảng bên trái.', + ], + 'system' => [ + 'title' => 'Không tìm thấy nhật ký thay đổi', + 'description' => 'Không có nhật ký thay đổi nào được tìm thấy.', + ], + ], + ], + ], + 'media' => [ + 'images' => [ + 'add-image-btn' => 'Thêm hình ảnh', + 'ai-add-image-btn' => 'Magic AI', + 'allowed-types' => 'png, jpeg, jpg', + 'not-allowed-error' => 'Chỉ chấp nhận tệp hình ảnh (.jpeg, .jpg, .png, ..).', + 'placeholders' => [ + 'front' => 'Mặt trước', + 'next' => 'Kế tiếp', + 'size' => 'Kích thước', + 'use-cases' => 'Trường hợp sử dụng', + 'zoom' => 'Thu phóng', + ], + ], + 'videos' => [ + 'add-video-btn' => 'Thêm video', + 'allowed-types' => 'mp4, webm, mkv', + 'not-allowed-error' => 'Chỉ chấp nhận tệp video (.mp4, .mov, .ogg ..).', + ], + ], + 'datagrid' => [ + 'index' => [ + 'no-records-selected' => 'Chưa có bản ghi nào được chọn.', + 'must-select-a-mass-action-option' => 'Bạn phải chọn một tùy chọn hành động hàng loạt.', + 'must-select-a-mass-action' => 'Bạn phải chọn một hành động hàng loạt.', + ], + 'toolbar' => [ + 'length-of' => ':length của', + 'of' => 'của', + 'per-page' => 'Mỗi Trang', + 'results' => ':total Kết quả', + 'delete' => 'Xóa', + 'selected' => ':total Mục đã chọn', + 'mass-actions' => [ + 'submit' => 'Gửi', + 'select-option' => 'Chọn Tùy chọn', + 'select-action' => 'Chọn Hành động', + ], + 'filter' => [ + 'apply-filters-btn' => 'Áp dụng Bộ lọc', + 'back-btn' => 'Quay lại', + 'create-new-filter' => 'Tạo Bộ lọc Mới', + 'custom-filters' => 'Bộ lọc Tùy chỉnh', + 'delete-error' => 'Đã xảy ra lỗi khi xóa bộ lọc, vui lòng thử lại.', + 'delete-success' => 'Bộ lọc đã được xóa thành công.', + 'empty-description' => 'Không có bộ lọc nào được chọn để lưu. Vui lòng chọn bộ lọc để lưu.', + 'empty-title' => 'Thêm Bộ lọc để Lưu', + 'name' => 'Tên', + 'quick-filters' => 'Bộ lọc Nhanh', + 'save-btn' => 'Lưu', + 'save-filter' => 'Lưu Bộ lọc', + 'saved-success' => 'Bộ lọc đã được lưu thành công.', + 'selected-filters' => 'Bộ lọc đã chọn', + 'title' => 'Bộ lọc', + 'update' => 'Cập nhật', + 'update-filter' => 'Cập nhật Bộ lọc', + 'updated-success' => 'Bộ lọc đã được cập nhật thành công.', + ], + 'search' => [ + 'title' => 'Tìm kiếm', + ], + 'column-settings' => [ + 'title' => 'Cài Đặt Cột', + ], + ], + 'filters' => [ + 'select' => 'Chọn', + 'title' => 'Bộ lọc', + 'dropdown' => [ + 'searchable' => [ + 'at-least-two-chars' => 'Nhập ít nhất 2 ký tự...', + 'no-results' => 'Không tìm thấy kết quả...', + ], + ], + 'custom-filters' => [ + 'clear-all' => 'Xóa tất cả', + 'title' => 'Bộ lọc Tùy chỉnh', + ], + 'boolean-options' => [ + 'false' => 'Sai', + 'true' => 'Đúng', + ], + 'date-options' => [ + 'last-month' => 'Tháng trước', + 'last-six-months' => '6 Tháng trước', + 'last-three-months' => '3 Tháng trước', + 'this-month' => 'Tháng này', + 'this-week' => 'Tuần này', + 'this-year' => 'Năm nay', + 'today' => 'Hôm nay', + 'yesterday' => 'Hôm qua', + ], + ], + 'table' => [ + 'actions' => 'Hành động', + 'no-records-available' => 'Không có Bản ghi nào.', + ], + ], + 'modal' => [ + 'confirm' => [ + 'agree-btn' => 'Đồng ý', + 'disagree-btn' => 'Không đồng ý', + 'message' => 'Bạn có chắc chắn muốn thực hiện hành động này không?', + 'title' => 'Bạn có chắc chắn?', + ], + ], + 'tags' => [ + 'index' => [ + 'title' => 'Thẻ', + 'added-tags' => 'Thẻ đã thêm', + 'save-btn' => 'Lưu Thẻ', + 'placeholder' => 'Nhập để tìm thẻ', + 'add-tag' => 'Thêm ":term"...', + 'aquarelle-red' => 'Đỏ Aquarelle', + 'crushed-cashew' => 'Hạt điều nghiền', + 'beeswax' => 'Sáp ong', + 'lemon-chiffon' => 'Vàng Chanh', + 'snow-flurry' => 'Tuyết Bay', + 'honeydew' => 'Mật Ong', + ], + ], + 'layouts' => [ + 'powered-by' => [ + 'description' => 'Được hỗ trợ bởi :krayin, một dự án mã nguồn mở được phát triển bởi :webkul.', + ], + 'header' => [ + 'mega-search' => [ + 'title' => 'Tìm kiếm Mega', + 'tabs' => [ + 'leads' => 'Khách hàng tiềm năng', + 'quotes' => 'Báo giá', + 'persons' => 'Người', + 'products' => 'Sản phẩm', + 'settings' => 'Cài đặt', + 'configurations' => 'Cấu hình', + ], + 'explore-all-products' => 'Khám phá tất cả Sản phẩm', + 'explore-all-leads' => 'Khám phá tất cả Khách hàng tiềm năng', + 'explore-all-contacts' => 'Khám phá tất cả Liên hệ', + 'explore-all-quotes' => 'Khám phá tất cả Báo giá', + 'explore-all-settings' => 'Khám phá tất cả Cài đặt', + 'explore-all-configurations' => 'Khám phá tất cả Cấu hình', + 'explore-all-matching-products' => 'Khám phá tất cả sản phẩm khớp với ":query" (:count)', + 'explore-all-matching-leads' => 'Khám phá tất cả khách hàng tiềm năng khớp với ":query" (:count)', + 'explore-all-matching-contacts' => 'Khám phá tất cả liên hệ khớp với ":query" (:count)', + 'explore-all-matching-quotes' => 'Khám phá tất cả báo giá khớp với ":query" (:count)', + ], + ], + ], + 'attributes' => [ + 'edit' => [ + 'delete' => 'Xóa', + ], + 'lookup' => [ + 'click-to-add' => 'Nhấn để thêm', + 'search' => 'Tìm kiếm...', + 'no-result-found' => 'Không tìm thấy kết quả', + ], + ], + 'lookup' => [ + 'click-to-add' => 'Nhấn để Thêm', + 'no-results' => 'Không tìm thấy kết quả', + 'add-as-new' => 'Thêm như mới', + 'search' => 'Tìm kiếm...', + ], + 'flash-group' => [ + 'success' => 'Thành công', + 'error' => 'Lỗi', + 'warning' => 'Cảnh báo', + 'info' => 'Thông tin', + ], + 'tiny-mce' => [ + 'http-error' => 'Lỗi HTTP', + 'invalid-json' => 'Phản hồi JSON không hợp lệ từ máy chủ.', + 'upload-failed' => 'Tải tệp lên không thành công. Vui lòng thử lại.', + ], + ], + 'quotes' => [ + 'index' => [ + 'title' => 'Báo giá', + 'create-btn' => 'Tạo Báo giá', + 'create-success' => 'Báo giá đã được tạo thành công.', + 'update-success' => 'Báo giá đã được cập nhật thành công.', + 'delete-success' => 'Báo giá đã được xóa thành công.', + 'delete-failed' => 'Không thể xóa báo giá.', + 'datagrid' => [ + 'subject' => 'Chủ đề', + 'sales-person' => 'Nhân viên bán hàng', + 'expired-at' => 'Hết hạn vào', + 'created-at' => 'Tạo vào', + 'person' => 'Người', + 'subtotal' => 'Tổng phụ', + 'discount' => 'Giảm giá', + 'tax' => 'Thuế', + 'adjustment' => 'Điều chỉnh', + 'grand-total' => 'Tổng cộng', + 'edit' => 'Chỉnh sửa', + 'delete' => 'Xóa', + 'print' => 'In', + ], + 'pdf' => [ + 'adjustment' => 'Điều chỉnh', + 'amount' => 'Số tiền', + 'billing-address' => 'Địa chỉ thanh toán', + 'date' => 'Ngày', + 'discount' => 'Giảm giá', + 'description' => 'Mô tả', + 'expired-at' => 'Hết hạn vào', + 'grand-total' => 'Tổng cộng', + 'person' => 'Người', + 'price' => 'Giá', + 'product-name' => 'Tên sản phẩm', + 'quantity' => 'Số lượng', + 'quote-id' => 'ID Báo giá', + 'sales-person' => 'Nhân viên bán hàng', + 'shipping-address' => 'Địa chỉ giao hàng', + 'sku' => 'Mã sản phẩm (SKU)', + 'sub-total' => 'Tổng phụ', + 'subject' => 'Chủ đề', + 'tax' => 'Thuế', + 'title' => 'Báo giá', + ], + ], + 'create' => [ + 'title' => 'Tạo Báo giá', + 'save-btn' => 'Lưu Báo giá', + 'quote-info' => 'Thông tin Báo giá', + 'quote-info-info' => 'Nhập thông tin cơ bản của báo giá.', + 'address-info' => 'Thông tin Địa chỉ', + 'address-info-info' => 'Thông tin về địa chỉ liên quan đến báo giá.', + 'same-as-billing' => 'Địa chỉ giao hàng giống địa chỉ thanh toán', + 'quote-items' => 'Mục Báo giá', + 'search-products' => 'Tìm kiếm Sản phẩm', + 'link-to-lead' => 'Liên kết tới lead', + 'quote-item-info' => 'Thêm Yêu cầu Sản phẩm cho báo giá này.', + 'quote-name' => 'Tên Báo giá', + 'quantity' => 'Số lượng', + 'price' => 'Giá', + 'discount' => 'Giảm giá', + 'tax' => 'Thuế', + 'total' => 'Tổng cộng', + 'amount' => 'Số tiền', + 'add-item' => '+ Thêm Mục', + 'sub-total' => 'Tổng phụ (:symbol)', + 'total-discount' => 'Giảm giá (:symbol)', + 'total-tax' => 'Thuế (:symbol)', + 'total-adjustment' => 'Điều chỉnh (:symbol)', + 'grand-total' => 'Tổng cộng (:symbol)', + 'discount-amount' => 'Số tiền giảm giá', + 'tax-amount' => 'Số tiền thuế', + 'adjustment-amount' => 'Số tiền điều chỉnh', + 'product-name' => 'Tên Sản phẩm', + 'action' => 'Hành động', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Báo giá', + 'save-btn' => 'Lưu Báo giá', + 'quote-info' => 'Thông tin Báo giá', + 'quote-info-info' => 'Nhập thông tin cơ bản của báo giá.', + 'address-info' => 'Thông tin Địa chỉ', + 'address-info-info' => 'Thông tin về địa chỉ liên quan đến báo giá.', + 'quote-items' => 'Mục Báo giá', + 'link-to-lead' => 'Liên kết tới lead', + 'quote-item-info' => 'Thêm Yêu cầu Sản phẩm cho báo giá này.', + 'quote-name' => 'Tên Báo giá', + 'quantity' => 'Số lượng', + 'price' => 'Giá', + 'search-products' => 'Tìm kiếm Sản phẩm', + 'discount' => 'Giảm giá', + 'tax' => 'Thuế', + 'total' => 'Tổng cộng', + 'amount' => 'Số tiền', + 'add-item' => '+ Thêm Mục', + 'sub-total' => 'Tổng phụ (:symbol)', + 'total-discount' => 'Giảm giá (:symbol)', + 'total-tax' => 'Thuế (:symbol)', + 'total-adjustment' => 'Điều chỉnh (:symbol)', + 'grand-total' => 'Tổng cộng (:symbol)', + 'discount-amount' => 'Số tiền giảm giá', + 'tax-amount' => 'Số tiền thuế', + 'adjustment-amount' => 'Số tiền điều chỉnh', + 'product-name' => 'Tên Sản phẩm', + 'action' => 'Hành động', + ], + ], + 'contacts' => [ + 'persons' => [ + 'index' => [ + 'title' => 'Người', + 'create-btn' => 'Tạo Người', + 'create-success' => 'Người đã được tạo thành công.', + 'update-success' => 'Người đã được cập nhật thành công.', + 'all-delete-success' => 'Tất cả người được chọn đã được xóa thành công.', + 'partial-delete-warning' => 'Một số người đã được xóa thành công. Những người khác không thể xóa vì có liên kết với khách hàng tiềm năng.', + 'none-delete-warning' => 'Không thể xóa bất kỳ người nào được chọn vì họ có liên kết với khách hàng tiềm năng.', + 'no-selection' => 'Chưa chọn người nào để xóa.', + 'delete-failed' => 'Xóa người được chọn không thành công.', + 'datagrid' => [ + 'contact-numbers' => 'Số Liên hệ', + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'emails' => 'Email', + 'id' => 'ID', + 'view' => 'Xem', + 'name' => 'Tên', + 'organization-name' => 'Tên Tổ chức', + 'tag-name' => 'Tên thẻ', + 'google-export-not-connected' => 'Vui lòng kết nối tài khoản Google trước trong Cài đặt > Cài đặt khác > Google Contacts.', + 'google-export-started' => 'Đã bắt đầu xuất sang Google Contacts.', + ], + ], + 'view' => [ + 'title' => ':name', + 'about-person' => 'Thông tin về Người', + 'about-organization' => 'Thông tin về Tổ chức', + 'activities' => [ + 'index' => [ + 'all' => 'Tất cả', + 'calls' => 'Cuộc gọi', + 'meetings' => 'Cuộc họp', + 'lunches' => 'Bữa trưa', + 'files' => 'Tệp', + 'quotes' => 'Báo giá', + 'notes' => 'Ghi chú', + 'emails' => 'Email', + 'by-user' => 'Bởi :user', + 'scheduled-on' => 'Đã lên lịch vào', + 'location' => 'Vị trí', + 'participants' => 'Người tham gia', + 'mark-as-done' => 'Đánh dấu là Đã hoàn thành', + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + ], + 'actions' => [ + 'mail' => [ + 'btn' => 'Mail', + 'title' => 'Soạn Mail', + 'to' => 'Đến', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Chủ đề', + 'send-btn' => 'Gửi', + 'message' => 'Tin nhắn', + ], + 'file' => [ + 'btn' => 'Tệp', + 'title' => 'Thêm Tệp', + 'title-control' => 'Tiêu đề', + 'name' => 'Tên Tệp', + 'description' => 'Mô tả', + 'file' => 'Tệp', + 'save-btn' => 'Lưu Tệp', + ], + 'note' => [ + 'btn' => 'Ghi chú', + 'title' => 'Thêm Ghi chú', + 'comment' => 'Bình luận', + 'save-btn' => 'Lưu Ghi chú', + ], + 'activity' => [ + 'btn' => 'Hoạt động', + 'title' => 'Thêm Hoạt động', + 'title-control' => 'Tiêu đề', + 'description' => 'Mô tả', + 'schedule-from' => 'Lên lịch từ', + 'schedule-to' => 'Lên lịch đến', + 'location' => 'Vị trí', + 'call' => 'Cuộc gọi', + 'meeting' => 'Cuộc họp', + 'lunch' => 'Bữa trưa', + 'save-btn' => 'Lưu Hoạt động', + ], + ], + ], + 'tags' => [ + 'create-success' => 'Thẻ được tạo thành công.', + 'destroy-success' => 'Thẻ đã được xóa thành công.', + ], + ], + 'create' => [ + 'title' => 'Tạo Người', + 'save-btn' => 'Lưu Người', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Người', + 'save-btn' => 'Lưu Người', + ], + ], + 'organizations' => [ + 'index' => [ + 'title' => 'Tổ chức', + 'create-btn' => 'Tạo Tổ chức', + 'create-success' => 'Tổ chức đã được tạo thành công.', + 'update-success' => 'Tổ chức đã được cập nhật thành công.', + 'delete-success' => 'Tổ chức đã được xóa thành công.', + 'delete-failed' => 'Không thể xóa tổ chức.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + 'persons-count' => 'Số người', + ], + ], + 'create' => [ + 'title' => 'Tạo Tổ chức', + 'save-btn' => 'Lưu Tổ chức', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Tổ chức', + 'save-btn' => 'Lưu Tổ chức', + ], + ], + ], + 'products' => [ + 'index' => [ + 'title' => 'Sản phẩm', + 'create-btn' => 'Tạo Sản phẩm', + 'create-success' => 'Sản phẩm đã được tạo thành công.', + 'update-success' => 'Sản phẩm đã được cập nhật thành công.', + 'delete-success' => 'Sản phẩm đã được xóa thành công.', + 'delete-failed' => 'Không thể xóa sản phẩm.', + 'datagrid' => [ + 'allocated' => 'Đã phân bổ', + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'in-stock' => 'Có sẵn', + 'name' => 'Tên', + 'on-hand' => 'Sẵn có', + 'tag-name' => 'Tên thẻ', + 'price' => 'Giá', + 'sku' => 'SKU', + 'view' => 'Xem', + ], + ], + 'create' => [ + 'save-btn' => 'Lưu Sản phẩm', + 'title' => 'Tạo Sản phẩm', + 'general' => 'Thông tin chung', + 'price' => 'Giá', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Sản phẩm', + 'save-btn' => 'Lưu Sản phẩm', + 'general' => 'Thông tin chung', + 'price' => 'Giá', + ], + 'view' => [ + 'sku' => 'SKU', + 'all' => 'Tất cả', + 'notes' => 'Ghi chú', + 'files' => 'Tệp', + 'inventories' => 'Tồn kho', + 'change-logs' => 'Nhật ký thay đổi', + 'attributes' => [ + 'about-product' => 'Thông tin về sản phẩm', + ], + 'inventory' => [ + 'warehouse' => 'Kho', + 'in-stock' => 'Có sẵn', + 'allocated' => 'Đã phân bổ', + 'on-hand' => 'Sẵn có', + 'actions' => 'Hành động', + 'assign' => 'Phân bổ', + 'add-warehouse' => 'Thêm kho', + 'location' => 'Vị trí', + 'add-more' => 'Thêm nữa', + 'save' => 'Lưu', + ], + ], + ], + 'settings' => [ + 'title' => 'Cài đặt', + 'groups' => [ + 'index' => [ + 'create-btn' => 'Tạo Nhóm', + 'title' => 'Nhóm', + 'create-success' => 'Tạo nhóm thành công.', + 'update-success' => 'Cập nhật nhóm thành công.', + 'destroy-success' => 'Xóa nhóm thành công.', + 'delete-failed' => 'Không thể xóa nhóm.', + 'delete-failed-associated-users' => 'Không thể xóa nhóm vì đang được sử dụng bởi người dùng.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'description' => 'Mô tả', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Nhóm', + ], + 'create' => [ + 'name' => 'Tên', + 'title' => 'Tạo Nhóm', + 'description' => 'Mô tả', + 'save-btn' => 'Lưu Nhóm', + ], + ], + ], + 'roles' => [ + 'index' => [ + 'being-used' => 'Vai trò không thể xóa, vì đang được sử dụng trong người dùng quản trị.', + 'create-btn' => 'Tạo Vai trò', + 'create-success' => 'Vai trò đã được tạo thành công.', + 'current-role-delete-error' => 'Không thể xóa vai trò đã gán cho người dùng hiện tại.', + 'current-role-edit-error' => 'Không thể chỉnh sửa vai trò đã gán cho tài khoản của bạn.', + 'delete-failed' => 'Không thể xóa vai trò.', + 'delete-success' => 'Vai trò đã được xóa thành công.', + 'last-delete-error' => 'Cần ít nhất một vai trò.', + 'settings' => 'Cài đặt', + 'title' => 'Vai trò', + 'update-success' => 'Vai trò đã được cập nhật thành công.', + 'user-define-error' => 'Không thể xóa vai trò hệ thống.', + 'datagrid' => [ + 'all' => 'Tất cả', + 'custom' => 'Tùy chỉnh', + 'delete' => 'Xóa', + 'description' => 'Mô tả', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + 'permission-type' => 'Loại quyền', + ], + ], + 'create' => [ + 'access-control' => 'Kiểm soát truy cập', + 'all' => 'Tất cả', + 'back-btn' => 'Quay lại', + 'custom' => 'Tùy chỉnh', + 'description' => 'Mô tả', + 'general' => 'Thông tin chung', + 'name' => 'Tên', + 'permissions' => 'Quyền', + 'save-btn' => 'Lưu Vai trò', + 'title' => 'Tạo Vai trò', + ], + 'edit' => [ + 'access-control' => 'Kiểm soát truy cập', + 'all' => 'Tất cả', + 'back-btn' => 'Quay lại', + 'custom' => 'Tùy chỉnh', + 'description' => 'Mô tả', + 'general' => 'Thông tin chung', + 'name' => 'Tên', + 'permissions' => 'Quyền', + 'save-btn' => 'Lưu Vai trò', + 'title' => 'Chỉnh sửa Vai trò', + ], + ], + 'types' => [ + 'index' => [ + 'create-btn' => 'Tạo Loại', + 'create-success' => 'Loại đã được tạo thành công.', + 'delete-failed' => 'Không thể xóa loại.', + 'delete-success' => 'Loại đã được xóa thành công.', + 'title' => 'Các Loại', + 'update-success' => 'Loại đã được cập nhật thành công.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'description' => 'Mô tả', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + ], + 'create' => [ + 'name' => 'Tên', + 'save-btn' => 'Lưu Loại', + 'title' => 'Tạo Loại', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Loại', + ], + ], + ], + 'sources' => [ + 'index' => [ + 'title' => 'Nguồn', + 'create-btn' => 'Tạo Nguồn', + 'create-success' => 'Tạo nguồn thành công.', + 'delete-failed' => 'Không thể xóa nguồn.', + 'delete-success' => 'Xóa nguồn thành công.', + 'update-success' => 'Cập nhật nguồn thành công.', + 'delete-failed-associated-leads' => 'Không thể xóa nguồn vì nó đang được liên kết với các khách hàng tiềm năng. Vui lòng hủy liên kết hoặc cập nhật các khách hàng đó trước khi xóa.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + ], + 'create' => [ + 'name' => 'Tên', + 'save-btn' => 'Lưu Nguồn', + 'title' => 'Tạo Nguồn', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Nguồn', + ], + ], + ], + 'workflows' => [ + 'index' => [ + 'title' => 'Quy trình', + 'create-btn' => 'Tạo Quy trình', + 'create-success' => 'Quy trình đã được tạo thành công.', + 'update-success' => 'Quy trình đã được cập nhật thành công.', + 'delete-success' => 'Quy trình đã được xóa thành công.', + 'delete-failed' => 'Không thể xóa quy trình.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'description' => 'Mô tả', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + ], + ], + 'helpers' => [ + 'update-related-leads' => 'Cập nhật các đầu mối liên quan', + 'send-email-to-sales-owner' => 'Gửi email đến người sở hữu bán hàng', + 'send-email-to-participants' => 'Gửi email đến các tham gia', + 'add-webhook' => 'Thêm Webhook', + 'update-lead' => 'Cập nhật đầu mối', + 'update-person' => 'Cập nhật người', + 'send-email-to-person' => 'Gửi email đến người', + 'add-tag' => 'Thêm Thẻ', + 'add-note-as-activity' => 'Thêm Ghi chú như Hoạt động', + 'update-quote' => 'Cập nhật báo giá', + ], + 'create' => [ + 'title' => 'Tạo Quy trình', + 'event' => 'Sự kiện', + 'back-btn' => 'Quay lại', + 'save-btn' => 'Lưu Quy trình', + 'name' => 'Tên', + 'basic-details' => 'Thông tin cơ bản', + 'description' => 'Mô tả', + 'actions' => 'Hành động', + 'basic-details-info' => 'Nhập thông tin cơ bản của quy trình.', + 'event-info' => 'Một sự kiện kích hoạt, kiểm tra, điều kiện và thực hiện các hành động đã được định nghĩa.', + 'conditions' => 'Điều kiện', + 'conditions-info' => 'Điều kiện là các quy tắc kiểm tra kịch bản, được kích hoạt trong các dịp cụ thể.', + 'actions-info' => 'Một hành động không chỉ giảm khối lượng công việc mà còn làm cho tự động hóa CRM dễ dàng hơn.', + 'value' => 'Giá trị', + 'condition-type' => 'Loại điều kiện', + 'all-condition-are-true' => 'Tất cả điều kiện đều đúng', + 'any-condition-are-true' => 'Bất kỳ điều kiện nào cũng đúng', + 'add-condition' => 'Thêm Điều kiện', + 'add-action' => 'Thêm Hành động', + 'no-webhook-found' => 'Không tìm thấy webhook.', + 'create-webhook' => 'Tạo webhook mới.', + 'yes' => 'Có', + 'no' => 'Không', + 'email' => 'Email', + 'is-equal-to' => 'Bằng với', + 'is-not-equal-to' => 'Không bằng với', + 'equals-or-greater-than' => 'Bằng hoặc lớn hơn', + 'equals-or-less-than' => 'Bằng hoặc nhỏ hơn', + 'greater-than' => 'Lớn hơn', + 'less-than' => 'Nhỏ hơn', + 'type' => 'Loại', + 'contain' => 'Chứa', + 'contains' => 'Chứa', + 'does-not-contain' => 'Không chứa', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Quy trình', + 'event' => 'Sự kiện', + 'back-btn' => 'Quay lại', + 'save-btn' => 'Lưu Quy trình', + 'name' => 'Tên', + 'basic-details' => 'Thông tin cơ bản', + 'description' => 'Mô tả', + 'actions' => 'Hành động', + 'type' => 'Loại', + 'basic-details-info' => 'Nhập thông tin cơ bản của quy trình.', + 'event-info' => 'Một sự kiện kích hoạt, kiểm tra, điều kiện và thực hiện các hành động đã được định nghĩa.', + 'conditions' => 'Điều kiện', + 'conditions-info' => 'Điều kiện là các quy tắc kiểm tra kịch bản, được kích hoạt trong các dịp cụ thể.', + 'actions-info' => 'Một hành động không chỉ giảm khối lượng công việc mà còn làm cho tự động hóa CRM dễ dàng hơn.', + 'value' => 'Giá trị', + 'condition-type' => 'Loại điều kiện', + 'all-condition-are-true' => 'Tất cả điều kiện đều đúng', + 'any-condition-are-true' => 'Bất kỳ điều kiện nào cũng đúng', + 'add-condition' => 'Thêm Điều kiện', + 'add-action' => 'Thêm Hành động', + 'yes' => 'Có', + 'no' => 'Không', + 'email' => 'Email', + 'is-equal-to' => 'Bằng với', + 'is-not-equal-to' => 'Không bằng với', + 'equals-or-greater-than' => 'Bằng hoặc lớn hơn', + 'equals-or-less-than' => 'Bằng hoặc nhỏ hơn', + 'greater-than' => 'Lớn hơn', + 'less-than' => 'Nhỏ hơn', + 'contain' => 'Chứa', + 'contains' => 'Chứa', + 'does-not-contain' => 'Không chứa', + ], + ], + 'google-contacts' => [ + 'index' => [ + 'title' => 'Google Contacts', + 'not-connected-info' => 'Kết nối một tài khoản Google để xuất danh bạ CRM sang Google Contacts.', + 'connected-info' => 'Đã kết nối với :email.', + 'connect-btn' => 'Kết Nối Tài Khoản Google', + 'disconnect-btn' => 'Ngắt Kết Nối', + 'connect-success' => 'Kết nối tài khoản Google thành công.', + 'connect-failed' => 'Không thể kết nối tài khoản Google của bạn. Vui lòng thử lại.', + 'not-configured' => 'Google OAuth chưa được cấu hình. Vui lòng thiết lập các biến môi trường cần thiết trước.', + 'disconnect-success' => 'Đã ngắt kết nối tài khoản Google thành công.', + ], + ], + 'webforms' => [ + 'index' => [ + 'title' => 'Biểu mẫu Web', + 'create-btn' => 'Tạo Biểu mẫu Web', + 'create-success' => 'Biểu mẫu Web đã được tạo thành công.', + 'update-success' => 'Biểu mẫu Web đã được cập nhật thành công.', + 'delete-success' => 'Biểu mẫu Web đã được xóa thành công.', + 'delete-failed' => 'Biểu mẫu Web không thể bị xóa.', + 'datagrid' => [ + 'id' => 'ID', + 'title' => 'Tiêu đề', + 'edit' => 'Chỉnh sửa', + 'delete' => 'Xóa', + ], + ], + 'create' => [ + 'title' => 'Tạo Biểu mẫu Web', + 'add-attribute-btn' => 'Thêm Nút Thuộc Tính', + 'attribute-label-color' => 'Màu Nhãn Thuộc Tính', + 'attributes' => 'Thuộc Tính', + 'attributes-info' => 'Thêm các thuộc tính tùy chỉnh vào biểu mẫu.', + 'background-color' => 'Màu Nền', + 'create-lead' => 'Tạo Dẫn Dắt', + 'customize-webform' => 'Tùy Chỉnh Biểu Mẫu Web', + 'customize-webform-info' => 'Tùy chỉnh biểu mẫu web của bạn với màu sắc của các phần tử theo lựa chọn của bạn.', + 'description' => 'Mô tả', + 'display-custom-message' => 'Hiển thị thông điệp tùy chỉnh', + 'form-background-color' => 'Màu Nền Biểu Mẫu', + 'form-submit-btn-color' => 'Màu Nút Gửi Biểu Mẫu', + 'form-submit-button-color' => 'Màu Nút Gửi Biểu Mẫu', + 'form-title-color' => 'Màu Tiêu Đề Biểu Mẫu', + 'general' => 'Chung', + 'leads' => 'Dẫn Dắt', + 'person' => 'Người', + 'pipeline' => 'Quy Trình', + 'save-btn' => 'Lưu Biểu Mẫu Web', + 'submit-button-label' => 'Nhãn Nút Gửi', + 'submit-success-action' => 'Hành Động Thành Công Khi Gửi', + 'redirect-to-url' => 'Chuyển Hướng Đến URL', + 'choose-value' => 'Chọn Giá Trị', + 'select-file' => 'Chọn Tập Tin', + 'select-image' => 'Chọn Hình Ảnh', + 'enter-value' => 'Nhập Giá Trị', + ], + 'edit' => [ + 'add-attribute-btn' => 'Thêm Nút Thuộc Tính', + 'attribute-label-color' => 'Màu Nhãn Thuộc Tính', + 'attributes' => 'Thuộc Tính', + 'attributes-info' => 'Thêm các thuộc tính tùy chỉnh vào biểu mẫu.', + 'background-color' => 'Màu Nền', + 'choose-value' => 'Chọn Giá Trị', + 'code-snippet' => 'Mã Snippet', + 'copied' => 'Đã Sao Chép', + 'copy' => 'Sao Chép', + 'create-lead' => 'Tạo Dẫn Dắt', + 'customize-webform' => 'Tùy Chỉnh Biểu Mẫu Web', + 'customize-webform-info' => 'Tùy chỉnh biểu mẫu web của bạn với màu sắc của các phần tử theo lựa chọn của bạn.', + 'description' => 'Mô tả', + 'display-custom-message' => 'Hiển thị thông điệp tùy chỉnh', + 'embed' => 'Nhúng', + 'enter-value' => 'Nhập Giá Trị', + 'form-background-color' => 'Màu Nền Biểu Mẫu', + 'form-submit-btn-color' => 'Màu Nút Gửi Biểu Mẫu', + 'form-submit-button-color' => 'Màu Nút Gửi Biểu Mẫu', + 'form-title-color' => 'Màu Tiêu Đề Biểu Mẫu', + 'general' => 'Chung', + 'leads' => 'Dẫn Dắt', + 'person' => 'Người', + 'pipeline' => 'Quy Trình', + 'preview' => 'Xem Trước', + 'public-url' => 'URL Công Khai', + 'redirect-to-url' => 'Chuyển Hướng Đến URL', + 'save-btn' => 'Lưu Biểu Mẫu Web', + 'select-file' => 'Chọn Tập Tin', + 'select-image' => 'Chọn Hình Ảnh', + 'submit-button-label' => 'Nhãn Nút Gửi', + 'submit-success-action' => 'Hành Động Thành Công Khi Gửi', + 'title' => 'Chỉnh Sửa Biểu Mẫu Web', + ], + ], + 'email-template' => [ + 'index' => [ + 'create-btn' => 'Tạo Mẫu Email', + 'title' => 'Mẫu Email', + 'create-success' => 'Mẫu Email đã được tạo thành công.', + 'update-success' => 'Mẫu Email đã được cập nhật thành công.', + 'delete-success' => 'Mẫu Email đã được xóa thành công.', + 'delete-failed' => 'Mẫu Email không thể bị xóa.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + 'subject' => 'Chủ đề', + ], + ], + 'create' => [ + 'title' => 'Tạo Mẫu Email', + 'save-btn' => 'Lưu Mẫu Email', + 'email-template' => 'Mẫu Email', + 'subject' => 'Chủ đề', + 'content' => 'Nội dung', + 'subject-placeholders' => 'Biến thể Chủ đề', + 'general' => 'Chung', + 'name' => 'Tên', + ], + 'edit' => [ + 'title' => 'Chỉnh Sửa Mẫu Email', + 'save-btn' => 'Lưu Mẫu Email', + 'email-template' => 'Mẫu Email', + 'subject' => 'Chủ đề', + 'content' => 'Nội dung', + 'subject-placeholders' => 'Biến thể Chủ đề', + 'general' => 'Chung', + 'name' => 'Tên', + ], + ], + 'marketing' => [ + 'events' => [ + 'index' => [ + 'create-btn' => 'Tạo Sự kiện', + 'title' => 'Sự kiện', + 'create-success' => 'Sự kiện đã được tạo thành công.', + 'update-success' => 'Sự kiện đã được cập nhật thành công.', + 'delete-success' => 'Sự kiện đã được xóa thành công.', + 'delete-failed' => 'Không thể xóa sự kiện.', + 'mass-delete-success' => 'Các sự kiện đã được xóa thành công', + 'delete-failed-associated-campaigns' => 'Không thể xóa sự kiện vì nó đang được liên kết với các chiến dịch. Vui lòng hủy liên kết hoặc cập nhật các chiến dịch đó trước khi xóa.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + 'description' => 'Mô tả', + 'date' => 'Ngày', + ], + 'create' => [ + 'title' => 'Tạo Sự kiện', + 'name' => 'Tên', + 'date' => 'Ngày', + 'description' => 'Mô tả', + 'save-btn' => 'Lưu Sự kiện', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Sự kiện', + ], + ], + ], + 'campaigns' => [ + 'index' => [ + 'create-btn' => 'Tạo Chiến dịch', + 'title' => 'Chiến dịch', + 'create-success' => 'Chiến dịch đã được tạo thành công.', + 'update-success' => 'Chiến dịch đã được cập nhật thành công.', + 'delete-success' => 'Chiến dịch đã được xóa thành công.', + 'delete-failed' => 'Không thể xóa chiến dịch.', + 'mass-delete-success' => 'Các chiến dịch đã được xóa thành công', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Tên', + 'subject' => 'Chủ đề', + 'status' => 'Trạng thái', + 'active' => 'Kích hoạt', + 'inactive' => 'Không kích hoạt', + 'edit' => 'Chỉnh sửa', + 'delete' => 'Xóa', + ], + 'create' => [ + 'title' => 'Tạo Chiến dịch', + 'name' => 'Tên', + 'type' => 'Loại', + 'subject' => 'Chủ đề', + 'event' => 'Sự kiện', + 'email-template' => 'Mẫu Email', + 'status' => 'Trạng thái', + ], + 'edit' => [ + 'title' => 'Chỉnh sửa Chiến dịch', + ], + ], + ], + ], + 'tags' => [ + 'index' => [ + 'create-btn' => 'Tạo Thẻ', + 'title' => 'Thẻ', + 'create-success' => 'Thẻ đã được tạo thành công.', + 'update-success' => 'Thẻ đã được cập nhật thành công.', + 'delete-success' => 'Thẻ đã được xóa thành công.', + 'delete-failed' => 'Thẻ không thể bị xóa.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'name' => 'Tên', + 'users' => 'Người dùng', + 'created-at' => 'Ngày tạo', + ], + 'create' => [ + 'name' => 'Tên', + 'save-btn' => 'Lưu Thẻ', + 'title' => 'Tạo Thẻ', + 'color' => 'Màu sắc', + ], + 'edit' => [ + 'title' => 'Chỉnh Sửa Thẻ', + ], + ], + ], + 'users' => [ + 'index' => [ + 'create-btn' => 'Tạo Người Dùng', + 'create-success' => 'Người dùng đã được tạo thành công.', + 'delete-failed' => 'Người dùng không thể bị xóa.', + 'delete-success' => 'Người dùng đã được xóa thành công.', + 'last-delete-error' => 'Cần ít nhất một người dùng.', + 'mass-delete-failed' => 'Người dùng không thể bị xóa.', + 'mass-delete-success' => 'Người dùng đã được xóa thành công.', + 'mass-update-failed' => 'Người dùng không thể được cập nhật.', + 'mass-update-success' => 'Người dùng đã được cập nhật thành công.', + 'title' => 'Người Dùng', + 'update-success' => 'Người dùng đã được cập nhật thành công.', + 'user-define-error' => 'Không thể xóa người dùng hệ thống.', + 'active' => 'Kích hoạt', + 'inactive' => 'Không kích hoạt', + 'datagrid' => [ + 'active' => 'Kích hoạt', + 'created-at' => 'Ngày tạo', + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'email' => 'Email', + 'id' => 'ID', + 'inactive' => 'Không kích hoạt', + 'name' => 'Tên', + 'status' => 'Trạng thái', + 'update-status' => 'Cập nhật Trạng thái', + 'group' => 'Nhóm Liên Kết', + 'no-group' => 'Không Có Nhóm', + 'users' => 'Người dùng', + ], + 'create' => [ + 'confirm-password' => 'Xác nhận Mật khẩu', + 'email' => 'Email', + 'general' => 'Chung', + 'global' => 'Toàn cầu', + 'group' => 'Nhóm', + 'individual' => 'Cá nhân', + 'name' => 'Tên', + 'password' => 'Mật khẩu', + 'permission' => 'Quyền hạn', + 'role' => 'Vai trò', + 'save-btn' => 'Lưu Người Dùng', + 'status' => 'Trạng thái', + 'title' => 'Tạo Người Dùng', + 'view-permission' => 'Xem Quyền Hạn', + 'select-at-lest-one-group' => 'Chọn ít nhất một nhóm', + ], + 'edit' => [ + 'title' => 'Chỉnh Sửa Người Dùng', + ], + ], + ], + 'pipelines' => [ + 'index' => [ + 'title' => 'Quy Trình', + 'create-btn' => 'Tạo Quy Trình', + 'create-success' => 'Quy trình đã được tạo thành công.', + 'update-success' => 'Quy trình đã được cập nhật thành công.', + 'default-required' => 'Cần ít nhất một pipeline mặc định.', + 'delete-success' => 'Quy trình đã được xóa thành công.', + 'delete-failed' => 'Quy trình không thể bị xóa.', + 'default-delete-error' => 'Quy trình mặc định không thể bị xóa.', + 'datagrid' => [ + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'id' => 'ID', + 'is-default' => 'Có phải là mặc định', + 'name' => 'Tên', + 'no' => 'Không', + 'rotten-days' => 'Ngày Hỏng', + 'yes' => 'Có', + ], + ], + 'create' => [ + 'title' => 'Tạo Quy Trình', + 'save-btn' => 'Lưu Quy Trình', + 'name' => 'Tên', + 'rotten-days' => 'Ngày Hỏng', + 'mark-as-default' => 'Đánh dấu là Mặc định', + 'general' => 'Chung', + 'probability' => 'Xác Suất (%)', + 'new-stage' => 'Mới', + 'won-stage' => 'Thắng', + 'lost-stage' => 'Thua', + 'stage-btn' => 'Thêm Giai Đoạn', + 'stages' => 'Các Giai Đoạn', + 'duplicate-name' => 'Trường Tên không được trùng lặp', + 'delete-stage' => 'Xóa Giai Đoạn', + 'add-new-stages' => 'Thêm Giai Đoạn Mới', + 'add-stage-info' => 'Thêm giai đoạn mới cho Quy trình của bạn', + 'newly-added' => 'Mới Thêm', + 'stage-delete-success' => 'Giai Đoạn đã được xóa thành công', + ], + 'edit' => [ + 'title' => 'Chỉnh Sửa Quy Trình', + 'save-btn' => 'Lưu Quy Trình', + 'name' => 'Tên', + 'rotten-days' => 'Ngày Hỏng', + 'mark-as-default' => 'Đánh dấu là Mặc định', + 'general' => 'Chung', + 'probability' => 'Xác Suất (%)', + 'new-stage' => 'Mới', + 'won-stage' => 'Thắng', + 'lost-stage' => 'Thua', + 'stage-btn' => 'Thêm Giai Đoạn', + 'stages' => 'Các Giai Đoạn', + 'duplicate-name' => 'Trường Tên không được trùng lặp', + 'delete-stage' => 'Xóa Giai Đoạn', + 'add-new-stages' => 'Thêm Giai Đoạn Mới', + 'add-stage-info' => 'Thêm giai đoạn mới cho Quy trình của bạn', + 'stage-delete-success' => 'Giai Đoạn đã được xóa thành công', + ], + ], + 'webhooks' => [ + 'index' => [ + 'title' => 'Webhooks', + 'create-btn' => 'Tạo Webhook', + 'create-success' => 'Webhook đã được tạo thành công.', + 'update-success' => 'Webhook đã được cập nhật thành công.', + 'delete-success' => 'Webhook đã được xóa thành công.', + 'delete-failed' => 'Webhook không thể bị xóa.', + 'invalid-endpoint' => 'URL điểm cuối webhook phải là địa chỉ HTTP hoặc HTTPS công khai.', + 'datagrid' => [ + 'id' => 'ID', + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'name' => 'Tên', + 'entity-type' => 'Loại Đối Tượng', + 'end-point' => 'Điểm Kết Thúc', + ], + ], + 'create' => [ + 'title' => 'Tạo Webhook', + 'save-btn' => 'Lưu Webhook', + 'info' => 'Nhập thông tin chi tiết của webhooks', + 'url-and-parameters' => 'URL Và Tham Số', + 'method' => 'Phương Thức', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'Điểm Kết Thúc URL', + 'parameters' => 'Tham Số', + 'add-new-parameter' => 'Thêm Tham Số Mới', + 'url-preview' => 'Xem Trước URL:', + 'headers' => 'Tiêu Đề', + 'add-new-header' => 'Thêm Tiêu Đề Mới', + 'body' => 'Nội Dung', + 'default' => 'Mặc định', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Khóa và Giá trị', + 'add-new-payload' => 'Thêm payload mới', + 'raw' => 'Thô', + 'general' => 'Chung', + 'name' => 'Tên', + 'entity-type' => 'Loại Đối Tượng', + 'insert-placeholder' => 'Chèn Placeholder', + 'description' => 'Mô Tả', + 'json' => 'Json', + 'text' => 'Văn bản', + ], + 'edit' => [ + 'title' => 'Chỉnh Sửa Webhook', + 'edit-btn' => 'Lưu Webhook', + 'save-btn' => 'Lưu Webhook', + 'info' => 'Nhập thông tin chi tiết của webhooks', + 'url-and-parameters' => 'URL Và Tham Số', + 'method' => 'Phương Thức', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'Điểm Kết Thúc URL', + 'parameters' => 'Tham Số', + 'add-new-parameter' => 'Thêm Tham Số Mới', + 'url-preview' => 'Xem Trước URL:', + 'headers' => 'Tiêu Đề', + 'add-new-header' => 'Thêm Tiêu Đề Mới', + 'body' => 'Nội Dung', + 'default' => 'Mặc định', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => 'Khóa và Giá trị', + 'add-new-payload' => 'Thêm payload mới', + 'raw' => 'Thô', + 'general' => 'Chung', + 'name' => 'Tên', + 'entity-type' => 'Loại Đối Tượng', + 'insert-placeholder' => 'Chèn Placeholder', + 'description' => 'Mô Tả', + 'json' => 'Json', + 'text' => 'Văn bản', + ], + ], + 'warehouses' => [ + 'index' => [ + 'title' => 'Kho Hàng', + 'create-btn' => 'Tạo Kho Hàng', + 'create-success' => 'Kho hàng đã được tạo thành công.', + 'name-exists' => 'Tên kho hàng đã tồn tại.', + 'update-success' => 'Kho hàng đã được cập nhật thành công.', + 'delete-success' => 'Kho hàng đã được xóa thành công.', + 'delete-failed' => 'Kho hàng không thể bị xóa.', + 'datagrid' => [ + 'id' => 'ID', + 'name' => 'Tên', + 'contact-name' => 'Tên Liên Hệ', + 'delete' => 'Xóa', + 'edit' => 'Chỉnh sửa', + 'view' => 'Xem', + 'created-at' => 'Ngày Tạo', + 'products' => 'Sản Phẩm', + 'contact-emails' => 'Email Liên Hệ', + 'contact-numbers' => 'Số Điện Thoại Liên Hệ', + ], + ], + 'create' => [ + 'title' => 'Tạo Kho Hàng', + 'save-btn' => 'Lưu Kho Hàng', + 'contact-info' => 'Thông Tin Liên Hệ', + ], + 'edit' => [ + 'title' => 'Chỉnh Sửa Kho Hàng', + 'save-btn' => 'Lưu Kho Hàng', + 'contact-info' => 'Thông Tin Liên Hệ', + ], + 'view' => [ + 'all' => 'Tất Cả', + 'notes' => 'Ghi Chú', + 'files' => 'Tệp', + 'location' => 'Vị Trí', + 'change-logs' => 'Nhật Ký Thay Đổi', + 'locations' => [ + 'action' => 'Hành Động', + 'add-location' => 'Thêm Vị Trí', + 'create-success' => 'Vị trí đã được tạo thành công.', + 'delete' => 'Xóa', + 'delete-failed' => 'Vị trí không thể bị xóa.', + 'delete-success' => 'Vị trí đã được xóa thành công.', + 'name' => 'Tên', + 'save-btn' => 'Lưu', + ], + 'general-information' => [ + 'title' => 'Thông Tin Chung', + ], + 'contact-information' => [ + 'title' => 'Thông Tin Liên Hệ', + ], + ], + ], + 'attributes' => [ + 'index' => [ + 'title' => 'Thuộc Tính', + 'create-btn' => 'Tạo Thuộc Tính', + 'create-success' => 'Thuộc tính đã được tạo thành công.', + 'update-success' => 'Thuộc tính đã được cập nhật thành công.', + 'delete-success' => 'Thuộc tính đã được xóa thành công.', + 'delete-failed' => 'Không thể xóa thuộc tính.', + 'user-define-error' => 'Không thể xóa thuộc tính hệ thống.', + 'mass-delete-failed' => 'Các thuộc tính hệ thống không thể bị xóa.', + 'datagrid' => [ + 'yes' => 'Có', + 'no' => 'Không', + 'id' => 'ID', + 'code' => 'Mã', + 'name' => 'Tên', + 'entity-type' => 'Loại Thực Thể', + 'type' => 'Loại', + 'is-default' => 'Mặc Định', + 'edit' => 'Chỉnh sửa', + 'delete' => 'Xóa', + 'quick-add' => 'Thêm Nhanh', + 'entity-types' => [ + 'leads' => 'Khách hàng tiềm năng', + 'organizations' => 'Tổ chức', + 'persons' => 'Người', + 'products' => 'Sản phẩm', + 'quotes' => 'Báo giá', + 'warehouses' => 'Kho hàng', + ], + 'types' => [ + 'text' => 'Văn bản', + 'textarea' => 'Vùng văn bản', + 'price' => 'Giá', + 'boolean' => 'Boolean', + 'select' => 'Chọn', + 'multiselect' => 'Chọn nhiều', + 'checkbox' => 'Hộp kiểm', + 'email' => 'Email', + 'address' => 'Địa chỉ', + 'phone' => 'Điện thoại', + 'lookup' => 'Tìm kiếm', + 'datetime' => 'Ngày giờ', + 'date' => 'Ngày', + 'image' => 'Hình ảnh', + 'file' => 'Tệp tin', + ], + ], + ], + 'create' => [ + 'title' => 'Tạo Thuộc Tính', + 'save-btn' => 'Lưu Thuộc Tính', + 'code' => 'Mã', + 'name' => 'Tên', + 'quick_add' => 'Thêm Nhanh', + 'entity-type' => 'Loại Thực Thể', + 'type' => 'Loại', + 'validations' => 'Xác Thực', + 'is-required' => 'Bắt Buộc', + 'input-validation' => 'Xác Thực Đầu Vào', + 'is-unique' => 'Là Độc Nhất', + 'labels' => 'Nhãn', + 'general' => 'Chung', + 'none' => 'Không có', + 'numeric' => 'Số', + 'decimal' => 'Thập Phân', + 'url' => 'Url', + 'options' => 'Tùy Chọn', + 'option-type' => 'Loại Tùy Chọn', + 'lookup-type' => 'Loại Tra Cứu', + 'add-option' => 'Thêm Tùy Chọn', + 'save-option' => 'Lưu Tùy Chọn', + 'option-name' => 'Tên Tùy Chọn', + 'add-attribute-options' => 'Thêm Tùy Chọn Thuộc Tính', + 'text' => 'Văn Bản', + 'textarea' => 'Khung Văn Bản', + 'price' => 'Giá', + 'boolean' => 'Boolean', + 'select' => 'Chọn', + 'multiselect' => 'Chọn Nhiều', + 'email' => 'Email', + 'address' => 'Địa Chỉ', + 'phone' => 'Điện Thoại', + 'datetime' => 'Ngày Giờ', + 'date' => 'Ngày', + 'image' => 'Hình Ảnh', + 'file' => 'Tệp', + 'lookup' => 'Tra Cứu', + 'entity_type' => 'Loại thực thể', + 'checkbox' => 'Hộp Kiểm', + 'is_required' => 'Bắt Buộc', + 'is_unique' => 'Là Độc Nhất', + 'actions' => 'Hành Động', + ], + 'edit' => [ + 'actions' => 'Hành Động', + 'add-attribute-options' => 'Thêm Tùy Chọn Thuộc Tính', + 'add-option' => 'Thêm Tùy Chọn', + 'address' => 'Địa Chỉ', + 'boolean' => 'Boolean', + 'checkbox' => 'Hộp Kiểm', + 'code' => 'Mã', + 'date' => 'Ngày', + 'datetime' => 'Ngày Giờ', + 'decimal' => 'Thập Phân', + 'email' => 'Email', + 'entity-type' => 'Loại Thực Thể', + 'entity_type' => 'Loại thực thể', + 'file' => 'Tệp', + 'general' => 'Chung', + 'image' => 'Hình Ảnh', + 'input-validation' => 'Xác Thực Đầu Vào', + 'is-required' => 'Bắt Buộc', + 'is-unique' => 'Là Độc Nhất', + 'is_required' => 'Bắt Buộc', + 'is_unique' => 'Là Độc Nhất', + 'labels' => 'Nhãn', + 'lookup' => 'Tra Cứu', + 'lookup-type' => 'Loại Tra Cứu', + 'multiselect' => 'Chọn Nhiều', + 'name' => 'Tên', + 'quick_add' => 'Thêm Nhanh', + 'none' => 'Không có', + 'numeric' => 'Số', + 'option-deleted' => 'Tùy chọn thuộc tính đã được xóa thành công', + 'option-name' => 'Tên Tùy Chọn', + 'option-type' => 'Loại Tùy Chọn', + 'options' => 'Tùy Chọn', + 'phone' => 'Điện Thoại', + 'price' => 'Giá', + 'save-btn' => 'Lưu Thuộc Tính', + 'save-option' => 'Lưu Tùy Chọn', + 'select' => 'Chọn', + 'text' => 'Văn Bản', + 'textarea' => 'Khung Văn Bản', + 'title' => 'Chỉnh Sửa Thuộc Tính', + 'type' => 'Loại', + 'url' => 'Url', + 'validations' => 'Xác Thực', + ], + ], + 'data-transfer' => [ + 'importers' => [ + 'persons' => [ + 'title' => 'Persons', + 'validation' => [ + 'errors' => [ + 'duplicate-email' => 'Email : \'%s\' is found more than once in the import file.', + 'duplicate-phone' => 'Phone : \'%s\' is found more than once in the import file.', + 'email-not-found' => 'Email : \'%s\' not found in the system.', + 'invalid-date' => 'Ngày : \'%s\' không phải là ngày hợp lệ.', + ], + ], + ], + 'products' => [ + 'title' => 'Products', + 'validation' => [ + 'errors' => [ + 'sku-not-found' => 'Product with specified SKU not found', + ], + ], + ], + 'leads' => [ + 'title' => 'Leads', + 'validation' => [ + 'errors' => [ + 'id-not-found' => 'ID : \'%s\' not found in the system.', + ], + ], + ], + ], + 'validation' => [ + 'errors' => [ + 'column-empty-headers' => 'Columns number "%s" have empty headers.', + 'column-name-invalid' => 'Invalid column names: "%s".', + 'column-not-found' => 'Required columns not found: %s.', + 'column-numbers' => 'Number of columns does not correspond to the number of rows in the header.', + 'invalid-attribute' => 'Header contains invalid attribute(s): "%s".', + 'system' => 'An unexpected system error occurred.', + 'wrong-quotes' => 'Curly quotes used instead of straight quotes.', + 'already-exists' => 'The :attribute already exists.', + ], + ], + 'imports' => [ + 'create' => [ + 'action' => 'Action', + 'allowed-errors' => 'Allowed Errors', + 'back-btn' => 'Back', + 'create-update' => 'Create/Update', + 'delete' => 'Delete', + 'download-sample' => 'Download Sample', + 'field-separator' => 'Field Separator', + 'file' => 'File', + 'general' => 'General', + 'images-directory' => 'Images Directory Path', + 'process-in-queue' => 'Process In Queue', + 'results' => 'Results', + 'save-btn' => 'Save Import', + 'settings' => 'Settings', + 'skip-errors' => 'Skip Errors', + 'stop-on-errors' => 'Stop on Errors', + 'title' => 'Create Import', + 'type' => 'Type', + 'validation-strategy' => 'Validation Strategy', + ], + 'edit' => [ + 'action' => 'Action', + 'allowed-errors' => 'Allowed Errors', + 'back-btn' => 'Back', + 'create-update' => 'Create/Update', + 'delete' => 'Delete', + 'download-sample' => 'Download Sample', + 'field-separator' => 'Field Separator', + 'file' => 'File', + 'general' => 'General', + 'images-directory' => 'Images Directory Path', + 'process-in-queue' => 'Process In Queue', + 'results' => 'Results', + 'save-btn' => 'Save Import', + 'settings' => 'Settings', + 'skip-errors' => 'Skip Errors', + 'stop-on-errors' => 'Stop on Errors', + 'title' => 'Edit Import', + 'type' => 'Type', + 'validation-strategy' => 'Validation Strategy', + ], + 'index' => [ + 'button-title' => 'Create Import', + 'title' => 'Imports', + 'datagrid' => [ + 'actions' => 'Actions', + 'completed-at' => 'Completed At', + 'created' => 'Created', + 'delete' => 'Delete', + 'deleted' => 'Deleted', + 'edit' => 'Edit', + 'error-file' => 'Error File', + 'id' => 'ID', + 'started-at' => 'Started At', + 'state' => 'State', + 'summary' => 'Summary', + 'type' => 'Type', + 'updated' => 'Updated', + 'uploaded-file' => 'Uploaded File', + ], + ], + 'import' => [ + 'back-btn' => 'Back', + 'completed-batches' => 'Total Batches Completed:', + 'download-error-report' => 'Download Full Report', + 'edit-btn' => 'Edit', + 'imported-info' => 'Congratulations! Your import was successful.', + 'importing-info' => 'Import In Process', + 'importing-queue-hint' => 'Nếu lần nhập này vẫn ở mức 0%, queue worker có thể chưa chạy. Hãy nhờ quản trị viên chạy \'php artisan queue:work\', hoặc tắt \'Xử lý trong hàng đợi\' và nhập lại.', + 'indexing-info' => 'Resources Indexing (Price, Inventory and Elastic Search) In Progress', + 'linking-info' => 'Resources Linking In Progress', + 'progress' => 'Progress:', + 'title' => 'Import', + 'total-batches' => 'Total Batches:', + 'total-created' => 'Total Records Created:', + 'total-deleted' => 'Total Records Deleted:', + 'total-errors' => 'Total Errors:', + 'total-invalid-rows' => 'Total Invalid Rows:', + 'total-rows-processed' => 'Total Rows Processed:', + 'total-updated' => 'Total Records Updated:', + 'validate' => 'Validate', + 'validate-info' => 'Click on Validate Data to check your import.', + 'validating-info' => 'The data started reading and Validating', + 'validation-failed-info' => 'Your import is invalid. Please fix the following errors and try again.', + 'validation-success-info' => 'Your import is valid. Click on Import to start the import process.', + ], + 'create-success' => 'Import created successfully.', + 'delete-failed' => 'Import deletion failed unexpectedly.', + 'delete-success' => 'Import deleted successfully.', + 'not-valid' => 'Import is invalid', + 'nothing-to-import' => 'There are no resources to import.', + 'queue-worker-not-running' => 'Queue worker không chạy. Hãy chạy php artisan queue:work để xử lý lần nhập này, hoặc tắt Xử lý trong hàng đợi và thử lại.', + 'setup-queue-error' => 'Please change your queue driver to "database" or "redis" to start the import process.', + 'update-success' => 'Import updated successfully.', + ], + ], + ], + 'activities' => [ + 'index' => [ + 'title' => 'Hoạt Động', + 'datagrid' => [ + 'comment' => 'Ghi Chú', + 'created_at' => 'Thời Gian Tạo', + 'created_by' => 'Người Tạo', + 'edit' => 'Chỉnh Sửa', + 'id' => 'ID', + 'done' => 'Đã Hoàn Thành', + 'not-done' => 'Chưa Hoàn Thành', + 'lead' => 'Người Dẫn Dắt', + 'mass-delete' => 'Xóa Hàng Loạt', + 'mass-update' => 'Cập Nhật Hàng Loạt', + 'schedule-from' => 'Lịch Từ', + 'schedule-to' => 'Lịch Đến', + 'schedule_from' => 'Lịch Từ', + 'schedule_to' => 'Lịch Đến', + 'title' => 'Tiêu Đề', + 'is_done' => 'Đã Hoàn Thành', + 'type' => 'Loại', + 'update' => 'Cập Nhật', + 'call' => 'Cuộc Gọi', + 'meeting' => 'Cuộc Họp', + 'lunch' => 'Bữa Trưa', + ], + ], + 'edit' => [ + 'title' => 'Chỉnh Sửa Hoạt Động', + 'back-btn' => 'Quay Lại', + 'save-btn' => 'Lưu Hoạt Động', + 'type' => 'Loại Hoạt Động', + 'call' => 'Cuộc Gọi', + 'meeting' => 'Cuộc Họp', + 'lunch' => 'Bữa Trưa', + 'schedule_to' => 'Lịch Đến', + 'schedule_from' => 'Lịch Từ', + 'location' => 'Địa Điểm', + 'comment' => 'Ghi Chú', + 'lead' => 'Người Dẫn Dắt', + 'participants' => 'Người Tham Gia', + 'general' => 'Chung', + 'persons' => 'Người', + 'no-result-found' => 'Không tìm thấy bản ghi.', + 'users' => 'Người Dùng', + ], + 'updated' => 'Đã cập nhật :attribute', + 'created' => 'Đã tạo', + 'duration-overlapping' => 'Người tham gia có một cuộc họp khác vào thời điểm này. Bạn có muốn tiếp tục không?', + 'create-success' => 'Hoạt động được tạo thành công.', + 'update-success' => 'Hoạt động được cập nhật thành công.', + 'overlapping-error' => 'Người tham gia có một cuộc họp khác vào thời điểm này.', + 'destroy-success' => 'Hoạt động đã được xóa thành công.', + 'delete-failed' => 'Không thể xóa hoạt động.', + 'mass-update-success' => 'Hoạt động được cập nhật thành công.', + 'mass-destroy-success' => 'Hoạt động đã được xóa thành công.', + 'mass-delete-failed' => 'Không thể xóa hoạt động.', + ], + 'mail' => [ + 'index' => [ + 'compose' => 'Soạn Thư', + 'draft' => 'Thư Nháp', + 'inbox' => 'Hộp Thư Đến', + 'outbox' => 'Hộp Thư Đi', + 'sent' => 'Đã Gửi', + 'trash' => 'Thùng Rác', + 'compose-mail-btn' => 'Soạn Thư', + 'btn' => 'Thư', + 'mail' => [ + 'title' => 'Soạn Thư', + 'to' => 'Đến', + 'enter-emails' => 'Nhấn enter để thêm email', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'subject' => 'Chủ Đề', + 'send-btn' => 'Gửi', + 'message' => 'Tin Nhắn', + 'draft' => 'Thư Nháp', + ], + 'datagrid' => [ + 'id' => 'ID', + 'from' => 'Từ', + 'to' => 'Đến', + 'subject' => 'Chủ đề', + 'tags' => 'Thẻ', + 'content' => 'Nội dung', + 'attachments' => 'Tệp đính kèm', + 'date' => 'Ngày', + 'move-to-inbox' => 'Di chuyển vào hộp thư đến', + 'move-to-trash' => 'Đã chuyển vào thùng rác', + 'edit' => 'Chỉnh sửa', + 'view' => 'Xem', + 'delete' => 'Xóa', + ], + ], + 'create-success' => 'Email đã được gửi thành công.', + 'update-success' => 'Email đã được cập nhật thành công.', + 'mass-update-success' => 'Các email đã được cập nhật thành công.', + 'delete-success' => 'Email đã được xóa thành công.', + 'delete-failed' => 'Email không thể bị xóa.', + 'invalid-route' => 'Đường dẫn không hợp lệ cho email.', + 'unauthorized' => 'Hành động này không được phép.', + 'view' => [ + 'title' => 'Thư', + 'subject' => ':subject', + 'link-mail' => 'Liên Kết Thư', + 'to' => 'Đến', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'reply' => 'Trả Lời', + 'reply-all' => 'Trả Lời Tất Cả', + 'forward' => 'Chuyển Tiếp', + 'delete' => 'Xóa', + 'enter-mails' => 'Nhập ID email', + 'rotten-days' => 'Người dẫn dắt đã hỏng trong :days ngày', + 'search-an-existing-lead' => 'Tìm kiếm người dẫn dắt hiện có', + 'search-an-existing-contact' => 'Tìm kiếm liên hệ hiện có', + 'message' => 'Tin Nhắn', + 'add-attachments' => 'Thêm Tệp Đính Kèm', + 'discard' => 'Bỏ Qua', + 'send' => 'Gửi', + 'no-result-found' => 'Không tìm thấy kết quả', + 'add-new-contact' => 'Thêm Liên Hệ Mới', + 'description' => 'Mô Tả', + 'search' => 'Tìm kiếm...', + 'add-new-lead' => 'Thêm Người Dẫn Dắt Mới', + 'create-new-contact' => 'Tạo Liên Hệ Mới', + 'save-contact' => 'Lưu Liên Hệ', + 'create-lead' => 'Tạo Người Dẫn Dắt', + 'linked-contact' => 'Liên Hệ Đã Liên Kết', + 'link-to-contact' => 'Liên Kết Với Liên Hệ', + 'link-to-lead' => 'Liên Kết Với Người Dẫn Dắt', + 'linked-lead' => 'Người Dẫn Dắt Đã Liên Kết', + 'lead-details' => 'Chi Tiết Người Dẫn Dắt', + 'contact-person' => 'Người Liên Hệ', + 'product' => 'Sản Phẩm', + 'tags' => [ + 'create-success' => 'Thẻ đã được tạo thành công.', + 'destroy-success' => 'Thẻ đã được xóa thành công.', + ], + ], + ], + 'common' => [ + 'custom-attributes' => [ + 'add-more' => 'Thêm Nữa', + 'address' => 'Địa Chỉ', + 'city' => 'Thành Phố', + 'contact' => 'Số Liên Lạc', + 'country' => 'Quốc Gia', + 'email' => 'Email', + 'home' => 'Nhà', + 'postcode' => 'Mã Bưu Chính', + 'save' => 'Lưu', + 'select' => 'Chọn', + 'select-country' => 'Chọn Quốc Gia', + 'select-state' => 'Chọn Bang', + 'state' => 'Bang', + 'update-contact-title' => 'Cập Nhật Số Liên Lạc', + 'update-emails-title' => 'Cập Nhật Email Liên Hệ', + 'work' => 'Công Việc', + ], + ], + 'leads' => [ + 'create-success' => 'Tạo khách hàng tiềm năng thành công.', + 'update-success' => 'Cập nhật khách hàng tiềm năng thành công.', + 'update-failed' => 'Leads can not be deleted.', + 'destroy-success' => 'Xóa khách hàng tiềm năng thành công.', + 'destroy-failed' => 'Không thể xóa khách hàng tiềm năng.', + 'file' => [ + 'data-not-found' => 'Không tìm thấy dữ liệu.', + 'empty-content' => 'Nội dung PDF trống hoặc không thể trích xuất.', + 'failed-extract' => 'Không thể trích xuất văn bản từ tệp.', + 'insufficient-info' => 'Do dữ liệu không đủ, chúng tôi không thể xử lý yêu cầu của bạn vào lúc này.', + 'invalid-base64' => 'Định dạng base64 không hợp lệ.', + 'invalid-format' => 'Định dạng JSON không hợp lệ.', + 'invalid-response' => 'Định dạng phản hồi AI không hợp lệ.', + 'missing-api-key' => 'Thiếu khóa API hoặc cấu hình mô hình.', + 'not-found' => 'Không tìm thấy tệp.', + 'recursive-call' => 'Phát hiện cuộc gọi đệ quy.', + 'text-generation-failed' => 'Trích xuất văn bản thất bại. Tệp có thể trống hoặc không đọc được.', + ], + 'index' => [ + 'title' => 'Khách Hàng Tiềm Năng', + 'create-btn' => 'Tạo Khách Hàng Tiềm Năng', + 'datagrid' => [ + 'id' => 'ID', + 'sales-person' => 'Nhân Viên Kinh Doanh', + 'subject' => 'Chủ Đề', + 'source' => 'Nguồn', + 'lead-value' => 'Giá Trị Ước Tính Khách Hàng', + 'lead-type' => 'Loại Khách Hàng', + 'tag-name' => 'Tên Thẻ', + 'contact-person' => 'Người Liên Hệ', + 'stage' => 'Giai Đoạn', + 'rotten-lead' => 'Khách Hàng Tiềm Năng Hết Hạn', + 'date-to' => 'Ngày Đến', + 'created-at' => 'Tạo Vào Lúc', + 'no' => 'Không', + 'yes' => 'Có', + 'delete' => 'Xóa', + 'mass-delete' => 'Xóa Hàng Loạt', + 'mass-update' => 'Cập Nhật Hàng Loạt', + ], + 'kanban' => [ + 'rotten-days' => 'Khách hàng tiềm năng đã hết hạn trong :days ngày', + 'empty-list' => 'Danh sách khách hàng tiềm năng của bạn trống', + 'empty-list-description' => 'Tạo một khách hàng tiềm năng để tổ chức các mục tiêu của bạn.', + 'create-lead-btn' => 'Tạo Khách Hàng Tiềm Năng', + 'columns' => [ + 'contact-person' => 'Người Liên Hệ', + 'id' => 'ID', + 'lead-type' => 'Loại Khách Hàng', + 'lead-value' => 'Giá Trị Ước Tính Khách Hàng', + 'sales-person' => 'Nhân Viên Kinh Doanh', + 'source' => 'Nguồn', + 'title' => 'Tiêu Đề', + 'tags' => 'Thẻ', + 'expected-close-date' => 'Ngày Dự Kiến Đóng', + 'date-to' => 'Ngày Đến', + 'created-at' => 'Tạo Vào Lúc', + ], + 'toolbar' => [ + 'search' => [ + 'title' => 'Tìm kiếm theo tiêu đề', + ], + 'filters' => [ + 'apply-filters' => 'Áp Dụng Bộ Lọc', + 'clear-all' => 'Xóa Tất Cả', + 'filter' => 'Bộ Lọc', + 'filters' => 'Bộ Lọc', + 'from' => 'Từ', + 'select' => 'Chọn', + 'to' => 'Đến', + ], + 'card-settings' => [ + 'title' => 'Cài Đặt Thẻ', + 'contact-person' => 'Người Liên Hệ', + 'rotten-indicator' => 'Chỉ Báo Hết Hạn', + 'sales-person' => 'Nhân Viên Kinh Doanh', + 'estimated-lead-value' => 'Giá Trị Ước Tính Khách Hàng', + 'source' => 'Nguồn', + 'lead-type' => 'Loại Khách Hàng', + 'tags' => 'Thẻ', + ], + ], + 'stages' => [ + 'won' => 'Thắng', + 'lost' => 'Thua', + 'need-more-info' => 'Cần Thêm Thông Tin', + 'closed-at' => 'Đóng Vào', + 'won-value' => 'Giá Trị Thắng', + 'lost-reason' => 'Lý Do Thua', + 'save-btn' => 'Lưu', + ], + ], + 'view-switcher' => [ + 'all-pipelines' => 'Tất Cả Các Quy Trình', + 'create-new-pipeline' => 'Tạo Quy Trình Mới', + ], + 'upload' => [ + 'create-lead' => 'Tạo Khách Hàng Tiềm Năng Bằng AI', + 'file' => 'Tải tệp lên', + 'file-info' => 'Chỉ chấp nhận các tệp định dạng pdf, bmp, jpg, jpeg, png.', + 'file-required' => 'Vui lòng chọn ít nhất một tệp hợp lệ để tiếp tục.', + 'save-btn' => 'Lưu', + 'upload-file' => 'Tải lên tệp', + ], + ], + 'create' => [ + 'title' => 'Tạo Khách Hàng Tiềm Năng', + 'save-btn' => 'Lưu', + 'details' => 'Chi Tiết', + 'details-info' => 'Nhập Thông Tin Cơ Bản Của Khách Hàng Tiềm Năng', + 'contact-person' => 'Người Liên Hệ', + 'contact-info' => 'Thông Tin Về Người Liên Hệ', + 'products' => 'Sản Phẩm', + 'products-info' => 'Thông Tin Về Sản Phẩm', + ], + 'edit' => [ + 'title' => 'Chỉnh Sửa Khách Hàng Tiềm Năng', + 'save-btn' => 'Lưu', + 'details' => 'Chi Tiết', + 'details-info' => 'Nhập Thông Tin Cơ Bản Của Khách Hàng Tiềm Năng', + 'contact-person' => 'Người Liên Hệ', + 'contact-info' => 'Thông Tin Về Người Liên Hệ', + 'products' => 'Sản Phẩm', + 'products-info' => 'Thông Tin Về Sản Phẩm', + ], + 'common' => [ + 'contact' => [ + 'name' => 'Tên', + 'name-search-placeholder' => 'Tìm kiếm theo tên, email và số điện thoại', + 'email' => 'Email', + 'contact-number' => 'Số Liên Lạc', + 'organization' => 'Tổ Chức', + ], + 'products' => [ + 'product-name' => 'Tên Sản Phẩm', + 'quantity' => 'Số Lượng', + 'price' => 'Giá', + 'amount' => 'Tổng Tiền', + 'action' => 'Hành Động', + 'add-more' => 'Thêm Nữa', + 'total' => 'Tổng Cộng', + ], + ], + 'view' => [ + 'title' => 'Khách Hàng Tiềm Năng: :title', + 'rotten-days' => ':days Ngày', + 'tabs' => [ + 'description' => 'Mô Tả', + 'products' => 'Sản Phẩm', + 'quotes' => 'Báo Giá', + ], + 'attributes' => [ + 'title' => 'Về Khách Hàng Tiềm Năng', + ], + 'quotes' => [ + 'id' => 'ID', + 'subject' => 'Chủ Đề', + 'expired-at' => 'Hết Hạn Vào', + 'sub-total' => 'Tạm Tính', + 'discount' => 'Giảm Giá', + 'tax' => 'Thuế', + 'adjustment' => 'Điều Chỉnh', + 'grand-total' => 'Tổng Cộng', + 'delete' => 'Xóa', + 'edit' => 'Chỉnh Sửa', + 'download' => 'Tải Xuống', + 'destroy-success' => 'Xóa báo giá thành công.', + 'empty-title' => 'Không Có Báo Giá', + 'empty-info' => 'Không Có Báo Giá Cho Khách Hàng Tiềm Năng Này', + 'add-btn' => 'Thêm Báo Giá', + 'mail' => 'Gửi Email', + 'quote-not-found' => 'Không tìm thấy báo giá cho khách hàng tiềm năng này.', + 'person-email-unavailable' => 'Email của người liên hệ trong khách hàng tiềm năng không khả dụng.', + 'mail-subject' => 'Báo giá: :subject', + 'mail-body' => 'Vui lòng xem tệp PDF báo giá được đính kèm.', + 'mail-send-failed' => 'Không thể gửi email báo giá.', + ], + 'products' => [ + 'product-name' => 'Tên Sản Phẩm', + 'quantity' => 'Số Lượng', + 'price' => 'Giá', + 'amount' => 'Tổng Tiền', + 'action' => 'Hành Động', + 'add-more' => 'Thêm Nữa', + 'total' => 'Tổng Cộng', + 'empty-title' => 'Không Có Sản Phẩm', + 'empty-info' => 'Không Có Sản Phẩm Cho Khách Hàng Tiềm Năng Này', + 'add-product' => 'Thêm Sản Phẩm', + ], + 'persons' => [ + 'title' => 'Về Người Liên Hệ', + 'job-title' => ':job_title tại :organization', + 'no-person' => 'Chưa có người liên hệ nào được gán cho lead này.', + 'attach-btn' => 'Gắn Người Liên Hệ', + 'change-btn' => 'Thay Đổi', + 'change-title' => 'Thay Đổi Người Liên Hệ', + 'save-btn' => 'Lưu', + 'cancel-btn' => 'Hủy', + ], + 'stages' => [ + 'won' => 'Thắng', + 'lost' => 'Thua', + 'need-more-info' => 'Cần Thêm Thông Tin', + 'closed-at' => 'Đóng Vào', + 'won-value' => 'Giá Trị Thắng', + 'lost-reason' => 'Lý Do Thua', + 'save-btn' => 'Lưu', + ], + 'tags' => [ + 'create-success' => 'Tạo thẻ thành công.', + 'destroy-success' => 'Xóa thẻ thành công.', + ], + ], + ], + 'configuration' => [ + 'index' => [ + 'back' => 'Quay lại', + 'delete' => 'Xóa', + 'save-btn' => 'Lưu Cấu hình', + 'save-success' => 'Cấu hình đã được lưu thành công.', + 'search' => 'Tìm kiếm', + 'select-country' => 'Chọn Quốc gia', + 'select-state' => 'Chọn Bang', + 'title' => 'Cấu hình', + 'general' => [ + 'title' => 'Chung', + 'info' => 'Cấu hình chung', + 'general' => [ + 'title' => 'Chung', + 'info' => 'Cập nhật cài đặt chung của bạn tại đây.', + 'locale-settings' => [ + 'title' => 'Cài đặt ngôn ngữ', + 'title-info' => 'Xác định ngôn ngữ được sử dụng trong giao diện bảng quản trị. Danh sách thả xuống hiển thị tất cả ngôn ngữ có sẵn trong bản cài đặt của bạn.', + ], + 'admin-logo' => [ + 'logo-image' => 'Hình ảnh Logo', + 'title' => 'Logo Quản trị', + 'title-info' => 'Cấu hình hình ảnh logo cho bảng điều khiển quản trị của bạn.', + 'favicon-image' => 'Hình ảnh Favicon', + 'favicon-image-info' => 'Cấu hình hình ảnh favicon cho bảng điều khiển quản trị của bạn.', + ], + ], + 'settings' => [ + 'title' => 'Cài đặt', + 'info' => 'Cập nhật cài đặt của bạn tại đây.', + 'footer' => [ + 'show' => 'Hiển thị "Powered By"', + 'show-info' => 'Hiển thị hoặc ẩn thanh "Powered by" ở cuối bảng quản trị.', + 'info' => 'Chúng ta có thể cấu hình phần "Powered by" ở đây.', + 'powered-by' => 'Trình soạn thảo văn bản "Powered by"', + 'title' => 'Cấu hình phần "Powered by"', + ], + 'menu' => [ + 'activities' => 'Hoạt động', + 'configuration' => 'Cấu hình', + 'contacts' => 'Liên hệ', + 'dashboard' => 'Bảng điều khiển', + 'draft' => 'Bản nháp', + 'inbox' => 'Hộp thư đến', + 'info' => 'Chúng ta có thể cấu hình tên các mục menu ở đây.', + 'leads' => 'Khách hàng tiềm năng', + 'mail' => 'Thư', + 'organizations' => 'Tổ chức', + 'outbox' => 'Hộp thư đi', + 'persons' => 'Cá nhân', + 'products' => 'Sản phẩm', + 'quotes' => 'Báo giá', + 'sent' => 'Đã gửi', + 'settings' => 'Cài đặt', + 'title' => 'Cấu hình mục menu', + 'trash' => 'Thùng rác', + ], + 'dashboard' => [ + 'title' => 'Cấu Hình Bảng Điều Khiển', + 'info' => 'Chúng ta có thể cấu hình khoảng thời gian mặc định của bảng điều khiển tại đây.', + 'date-range' => 'Khoảng Thời Gian Mặc Định', + 'date-range-info' => 'Khoảng thời gian bảng điều khiển tải theo mặc định. Bộ chọn ngày trên bảng điều khiển vẫn có thể điều chỉnh cho lần xem tạm thời.', + 'custom-days' => 'Khoảng Tùy Chỉnh (Ngày)', + 'custom-days-info' => 'Số ngày truy xuất khi khoảng thời gian được đặt là Tùy chỉnh.', + 'ranges' => [ + '1-month' => '1 Tháng', + '3-months' => '3 Tháng', + '9-months' => '9 Tháng', + '1-year' => '1 Năm', + '2-years' => '2 Năm', + 'custom' => 'Tùy chỉnh', + ], + ], + 'menu-color' => [ + 'brand-color' => 'Màu thương hiệu', + 'info' => 'Chúng ta có thể thay đổi màu của các mục menu ở đây.', + 'title' => 'Cấu hình màu mục menu', + ], + ], + ], + 'email' => [ + 'title' => 'Cài đặt Email', + 'info' => 'Cấu hình email cho ứng dụng.', + 'imap' => [ + 'title' => 'Cài đặt IMAP', + 'info' => 'Cấu hình email IMAP để nhận email.', + 'account' => [ + 'title' => 'Tài khoản IMAP', + 'title-info' => 'Cấu hình cài đặt tài khoản IMAP của bạn tại đây.', + 'host' => 'Máy chủ', + 'port' => 'Cổng', + 'encryption' => 'Loại mã hóa', + 'validate-cert' => 'Xác thực chứng chỉ SSL', + 'username' => 'Tên người dùng IMAP', + 'password' => 'Mật khẩu IMAP', + ], + ], + ], + 'magic-ai' => [ + 'title' => 'Magic AI', + 'info' => 'Cấu hình Magic AI cho ứng dụng.', + 'settings' => [ + 'api-key' => 'Khóa API', + 'api-key-info' => 'Nhớ sử dụng khóa API OpenRouter cho mỗi mô hình. Đây là một bước đơn giản để tăng cường bảo mật và hiệu suất.', + 'enable' => 'Kích hoạt', + 'info' => 'Nâng cao trải nghiệm Magic AI của bạn với Khóa API OpenRouter. Tích hợp ngay bây giờ để có một cuộc phiêu lưu AI liền mạch và cá nhân hóa chỉ dành cho bạn! Dễ dàng tùy chỉnh cài đặt và kiểm soát hành trình AI của bạn.', + 'other' => 'Mô hình khác', + 'other-model' => 'Đối với các mô hình khác, sử dụng ID Mô hình từ OpenRouter.', + 'doc-generation' => 'Tạo DOC', + 'doc-generation-info' => 'Bật tính năng Tạo DOC để tự động trích xuất dữ liệu từ các tệp DOC và chuyển đổi chúng sang định dạng văn bản. Nâng cao năng suất và hiệu quả công việc bằng cách bật tính năng này để đơn giản hóa quy trình làm việc của bạn.', + 'title' => 'Cài đặt chung', + 'models' => [ + 'deepseek-r1' => 'Deepseek R1 Distill-llama-8b', + 'gemini-2-0-flash-001' => 'Gemini 2.0 flash-001', + 'gpt-4o' => 'GPT-4.0', + 'gpt-4o-mini' => 'GPT-4.0 mini', + 'grok-2-1212' => 'Grok 2.12', + 'llama-3-2-3b-instruct' => 'Llama 3.2 3b Instruct', + 'title' => 'Mô hình', + ], + ], + ], + ], + ], + 'dashboard' => [ + 'index' => [ + 'title' => 'Bảng Điều Khiển', + 'start-date' => 'Ngày bắt đầu', + 'end-date' => 'Ngày kết thúc', + 'export-pdf' => 'Xuất PDF', + 'pdf' => [ + 'title' => 'Báo Cáo Bảng Điều Khiển', + 'filtered-date' => 'Ngày Đã Lọc', + 'revenue' => 'Doanh Thu', + 'overview' => 'Tổng Quan', + 'total-leads' => 'Tổng Số Lead', + 'metric' => 'Chỉ Số', + 'current' => 'Hiện Tại', + 'previous' => 'Trước Đó', + 'progress' => 'Tiến Độ', + 'date' => 'Ngày', + 'all' => 'Tất Cả', + 'total' => 'Tổng', + 'won' => 'Đã Đạt', + 'lost' => 'Bị Mất', + 'source' => 'Nguồn', + 'type' => 'Loại', + 'stage' => 'Giai Đoạn', + 'product' => 'Sản Phẩm', + 'person' => 'Người Liên Hệ', + 'price' => 'Giá', + 'revenue-by-sources' => 'Doanh Thu Theo Nguồn', + 'revenue-by-types' => 'Doanh Thu Theo Loại', + 'open-leads-by-stages' => 'Khách hàng tiềm năng mở theo giai đoạn', + 'top-selling-products' => 'Sản Phẩm Bán Chạy Nhất', + 'top-persons' => 'Người Liên Hệ Hàng Đầu', + ], + 'revenue' => [ + 'lost-revenue' => 'Doanh Thu Bị Mất', + 'won-revenue' => 'Doanh Thu Đã Đạt', + ], + 'over-all' => [ + 'average-lead-value' => 'Giá Trị Lead Trung Bình', + 'total-leads' => 'Tổng Số Lead', + 'average-leads-per-day' => 'Số Lead Trung Bình Mỗi Ngày', + 'total-quotations' => 'Tổng Số Báo Giá', + 'total-persons' => 'Tổng Số Người Liên Hệ', + 'total-organizations' => 'Tổng Số Tổ Chức', + ], + 'total-leads' => [ + 'title' => 'Leads', + 'total' => 'Tổng Số Lead', + 'won' => 'Lead Đã Đạt', + 'lost' => 'Lead Bị Mất', + ], + 'revenue-by-sources' => [ + 'title' => 'Doanh Thu Theo Nguồn', + 'empty-title' => 'Không Có Dữ Liệu', + 'empty-info' => 'Không có dữ liệu cho khoảng thời gian được chọn', + ], + 'revenue-by-types' => [ + 'title' => 'Doanh Thu Theo Loại', + 'empty-title' => 'Không Có Dữ Liệu', + 'empty-info' => 'Không có dữ liệu cho khoảng thời gian được chọn', + ], + 'top-selling-products' => [ + 'title' => 'Sản Phẩm Bán Chạy Nhất', + 'empty-title' => 'Không Tìm Thấy Sản Phẩm', + 'empty-info' => 'Không có sản phẩm nào cho khoảng thời gian được chọn', + ], + 'top-persons' => [ + 'title' => 'Người Liên Hệ Hàng Đầu', + 'empty-title' => 'Không Tìm Thấy Người Liên Hệ', + 'empty-info' => 'Không có người liên hệ nào cho khoảng thời gian được chọn', + ], + 'open-leads-by-states' => [ + 'title' => 'Khách hàng tiềm năng mở theo giai đoạn', + 'empty-title' => 'Không Có Dữ Liệu', + 'empty-info' => 'Không có dữ liệu cho khoảng thời gian được chọn', + ], + ], + ], + 'help' => [ + 'index' => [ + 'title' => 'Trợ giúp & Tài nguyên', + 'description' => 'Mọi thứ bạn cần để tận dụng tối đa Krayin Admin — lưu trữ, hỗ trợ và dịch vụ chuyên nghiệp, cùng với tiện ích mở rộng và tài liệu dành cho nhà phát triển.', + 'contact-us' => 'Liên hệ với chúng tôi', + 'view-all' => 'Xem tất cả', + 'still-need-help-title' => 'Vẫn cần trợ giúp?', + 'still-need-help-description' => 'Trao đổi với đội ngũ Krayin về lưu trữ, phát triển tùy chỉnh hoặc bất kỳ vấn đề nào khác.', + + 'services' => [ + 'title' => 'Dịch vụ & Tài nguyên', + 'cloud-hosting' => [ + 'title' => 'Lưu trữ đám mây', + 'description' => 'Dịch vụ lưu trữ đám mây được quản lý, tiết kiệm chi phí — dùng thử và khởi chạy Krayin trên đám mây trong vài phút, được tối ưu hóa toàn diện và có khả năng mở rộng.', + ], + 'support' => [ + 'title' => 'Hỗ trợ & Bảo trì', + 'description' => 'Hỗ trợ kỹ thuật chuyên dụng và các gói bảo trì liên tục để giữ cho CRM của bạn an toàn, được cập nhật và hoạt động trơn tru.', + ], + 'extensions' => [ + 'title' => 'Tiện ích mở rộng', + 'description' => 'Duyệt các tiện ích bổ sung chính thức và từ cộng đồng để mở rộng Krayin với các trình kết nối, kênh và tính năng mới.', + ], + ], + + 'resources' => [ + 'title' => 'Tài liệu', + 'user-docs' => [ + 'title' => 'Hướng dẫn sử dụng', + 'description' => 'Hỗ trợ chuyên môn cho việc tích hợp mô-đun, tùy chỉnh, di chuyển dữ liệu, nâng cấp phiên bản và phát triển theo yêu cầu.', + ], + 'dev-docs' => [ + 'title' => 'Tài liệu nhà phát triển & Blog', + 'description' => 'Hướng dẫn dành cho nhà phát triển, các bài hướng dẫn và những bài viết mới nhất giúp bạn xây dựng, cấu hình và luôn cập nhật.', + ], + 'api-docs' => [ + 'title' => 'Tài liệu API', + 'description' => 'Tài liệu tham khảo API REST đầy đủ với các điểm cuối, xác thực và ví dụ để tích hợp Krayin vào hệ thống của bạn.', + ], + ], + + 'community' => [ + 'forums' => [ + 'title' => 'Diễn đàn cộng đồng', + 'description' => 'Kết nối với hàng nghìn người dùng và nhà phát triển Krayin khác để chia sẻ mẹo và giải quyết vấn đề.', + 'link' => 'Tham gia cộng đồng', + ], + 'tutorials' => [ + 'title' => 'Video hướng dẫn', + 'description' => 'Xem các hướng dẫn bằng video từng bước về cách thiết lập CRM, cấu hình quy trình và quản lý người dùng.', + 'link' => 'Xem kênh YouTube', + ], + ], + ], + ], + 'layouts' => [ + 'app-version' => 'Phiên Bản : :version', + 'dashboard' => 'Bảng Điều Khiển', + 'help' => 'Trợ giúp & Tài nguyên', + 'leads' => 'Leads', + 'quotes' => 'Báo Giá', + 'quote' => 'Báo Giá', + 'mail' => [ + 'title' => 'Thư', + 'compose' => 'Soạn Thư', + 'inbox' => 'Hộp Thư Đến', + 'draft' => 'Thư Nháp', + 'outbox' => 'Hộp Thư Đi', + 'sent' => 'Đã Gửi', + 'trash' => 'Thùng Rác', + 'setting' => 'Cài Đặt', + ], + 'activities' => 'Hoạt Động', + 'contacts' => 'Liên Hệ', + 'persons' => 'Người Liên Hệ', + 'person' => 'Người Liên Hệ', + 'organizations' => 'Tổ Chức', + 'organization' => 'Tổ Chức', + 'products' => 'Sản Phẩm', + 'product' => 'Sản Phẩm', + 'settings' => 'Cài Đặt', + 'user' => 'Người Dùng', + 'user-info' => 'Quản lý tất cả người dùng và quyền hạn của họ trong CRM, những gì họ được phép làm.', + 'groups' => 'Nhóm', + 'groups-info' => 'Thêm, chỉnh sửa hoặc xóa nhóm khỏi CRM', + 'roles' => 'Vai Trò', + 'role' => 'Vai Trò', + 'roles-info' => 'Thêm, chỉnh sửa hoặc xóa vai trò khỏi CRM', + 'users' => 'Người Dùng', + 'users-info' => 'Thêm, chỉnh sửa hoặc xóa người dùng khỏi CRM', + 'lead' => 'Lead', + 'lead-info' => 'Quản lý tất cả các cài đặt liên quan đến leads trong CRM', + 'sidebar' => [ + 'collapse' => 'Thu gọn', + 'expand' => 'Mở rộng', + ], + 'quick-add' => [ + 'title' => 'Thêm nhanh', + 'save' => 'Lưu', + 'to' => 'Đến', + 'subject' => 'Tiêu đề', + 'message' => 'Tin nhắn', + ], + 'pipelines' => 'Pipelines', + 'pipelines-info' => 'Thêm, chỉnh sửa hoặc xóa pipelines khỏi CRM', + 'sources' => 'Nguồn', + 'sources-info' => 'Thêm, chỉnh sửa hoặc xóa nguồn khỏi CRM', + 'types' => 'Loại', + 'types-info' => 'Thêm, chỉnh sửa hoặc xóa loại khỏi CRM', + 'automation' => 'Tự Động Hóa', + 'automation-info' => 'Quản lý tất cả các cài đặt liên quan đến tự động hóa trong CRM', + 'attributes' => 'Thuộc Tính', + 'attribute' => 'Thuộc Tính', + 'attributes-info' => 'Thêm, chỉnh sửa hoặc xóa thuộc tính khỏi CRM', + 'email-templates' => 'Mẫu Email', + 'email' => 'Email', + 'email-templates-info' => 'Thêm, chỉnh sửa hoặc xóa mẫu email khỏi CRM', + 'events' => 'Sự kiện', + 'events-info' => 'Thêm, chỉnh sửa hoặc xóa sự kiện từ CRM', + 'campaigns' => 'Chiến dịch', + 'campaigns-info' => 'Thêm, chỉnh sửa hoặc xóa chiến dịch từ CRM', + 'workflows' => 'Quy Trình', + 'workflows-info' => 'Thêm, chỉnh sửa hoặc xóa quy trình khỏi CRM', + 'webhooks' => 'Webhook', + 'webhooks-info' => 'Thêm, chỉnh sửa hoặc xóa webhook từ CRM', + 'other-settings' => 'Cài Đặt Khác', + 'other-settings-info' => 'Quản lý tất cả các cài đặt khác trong CRM', + 'tags' => 'Thẻ', + 'tags-info' => 'Thêm, chỉnh sửa hoặc xóa thẻ khỏi CRM', + 'my-account' => 'Tài Khoản Của Tôi', + 'sign-out' => 'Đăng Xuất', + 'back' => 'Quay lại', + 'name' => 'Tên', + 'configuration' => 'Cấu hình', + 'howdy' => 'Xin chào!', + 'warehouses' => 'Kho hàng', + 'warehouse' => 'Kho hàng', + 'warehouses-info' => 'Thêm, chỉnh sửa hoặc xóa kho hàng từ CRM', + 'inventory' => 'Hàng tồn kho', + 'inventory-info' => 'Quản lý tất cả các cài đặt liên quan đến hàng tồn kho trong CRM', + 'data_transfer' => 'Data Transfer', + 'data_transfer_info' => 'Manage persons, products and leads data transfer related settings in the CRM', + ], + 'user' => [ + 'account' => [ + 'name' => 'Tên', + 'email' => 'Email', + 'password' => 'Mật Khẩu', + 'my_account' => 'Tài Khoản Của Tôi', + 'update_details' => 'Cập Nhật Thông Tin', + 'current_password' => 'Mật Khẩu Hiện Tại', + 'confirm_password' => 'Xác Nhận Mật Khẩu', + 'password-match' => 'Mật khẩu hiện tại không khớp.', + 'account-save' => 'Thay đổi tài khoản đã được lưu thành công.', + 'permission-denied' => 'Từ Chối Quyền Truy Cập', + 'remove-image' => 'Xóa Hình Ảnh', + 'upload_image_pix' => 'Tải Lên Ảnh Hồ Sơ (100px x 100px)', + 'upload_image_format' => 'Định Dạng PNG hoặc JPG', + 'image_upload_message' => 'Chỉ chấp nhận hình ảnh (.jpeg, .jpg, .png, ..).', + ], + ], + 'emails' => [ + 'common' => [ + 'dear' => 'Kính gửi :name', + 'cheers' => 'Trân trọng,
    Đội ngũ :app_name', + 'user' => [ + 'dear' => 'Kính gửi :username', + 'create-subject' => 'Bạn đã được thêm làm thành viên.', + 'create-body' => 'Chúc mừng! Bạn đã trở thành thành viên của đội ngũ chúng tôi.', + 'forget-password' => [ + 'subject' => 'Khách hàng yêu cầu đặt lại mật khẩu', + 'dear' => 'Kính gửi :username', + 'reset-password' => 'Đặt Lại Mật Khẩu', + 'info' => 'Bạn nhận được email này vì chúng tôi đã nhận được yêu cầu đặt lại mật khẩu cho tài khoản của bạn.', + 'final-summary' => 'Nếu bạn không yêu cầu đặt lại mật khẩu, không cần thực hiện thêm hành động nào.', + 'thanks' => 'Cảm ơn!', + ], + ], + ], + ], + 'validations' => [ + 'message' => [ + 'code' => 'Trường phải là một mã hợp lệ.', + 'decimal' => 'The :attribute must be a decimal.', + ], + ], + 'errors' => [ + 'dashboard' => 'Bảng điều khiển', + 'go-back' => 'Quay lại', + 'support' => 'Nếu sự cố vẫn tiếp diễn, vui lòng liên hệ với chúng tôi tại :email để được hỗ trợ.', + 'unauthorized' => 'Hành động này không được phép.', + 'role-exceeds-own' => 'You can only assign a role whose permissions are the same as or fewer than your own.', + 'user-exceeds-own' => 'You can only manage users whose permissions are the same as or fewer than your own.', + 'own-privileges' => 'You cannot change your own role or data scope.', + 'scope-exceeds-own' => 'You cannot grant a data scope wider than your own.', + 'primary-admin-protected' => 'The primary administrator account is protected and cannot be modified here.', + 'role-permissions-exceed-own' => 'You can only create or edit roles whose permissions are the same as or fewer than your own.', + + '404' => [ + 'description' => 'Rất tiếc! Trang bạn đang tìm kiếm hiện không có ở đây. Có vẻ như chúng tôi không thể tìm thấy những gì bạn đang tìm kiếm.', + 'title' => '404 Không Tìm Thấy Trang', + ], + '401' => [ + 'description' => 'Rất tiếc! Có vẻ như bạn không được phép truy cập vào trang này. Có vẻ bạn đang thiếu thông tin xác thực cần thiết.', + 'title' => '401 Không Được Phép', + ], + '403' => [ + 'description' => 'Rất tiếc! Trang này bị hạn chế. Có vẻ bạn không có quyền truy cập vào nội dung này.', + 'title' => '403 Cấm Truy Cập', + ], + '500' => [ + 'description' => 'Rất tiếc! Đã xảy ra sự cố. Có vẻ như chúng tôi đang gặp khó khăn trong việc tải trang mà bạn đang tìm kiếm.', + 'title' => '500 Lỗi Máy Chủ Nội Bộ', + ], + '503' => [ + 'description' => 'Rất tiếc! Có vẻ chúng tôi đang tạm ngừng để bảo trì. Vui lòng quay lại sau.', + 'title' => '503 Dịch Vụ Không Khả Dụng', + ], + ], + 'export' => [ + 'csv' => 'CSV', + 'download' => 'Tải Xuống', + 'export' => 'Xuất', + 'google-contacts' => 'Google Contacts', + 'google-contacts-in-progress' => 'Đang xuất danh bạ của bạn sang Google... việc này có thể mất một chút thời gian.', + 'google-contacts-total' => 'Tổng Số Liên Hệ:', + 'google-contacts-exported' => 'Đã Xuất:', + 'google-contacts-duplicate' => 'Đã Tồn Tại:', + 'google-contacts-failed' => 'Thất Bại:', + 'google-contacts-summary' => 'Xuất hoàn tất: :exported đã xuất, :duplicate đã tồn tại, :failed thất bại.', + 'no-records' => 'Không có bản ghi nào được tìm thấy.', + 'xls' => 'XLS', + 'xlsx' => 'XLSX', + ], +]; diff --git a/packages/Webkul/Admin/src/Resources/lang/zh_CN/app.php b/packages/Webkul/Admin/src/Resources/lang/zh_CN/app.php new file mode 100644 index 0000000..9a74032 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/lang/zh_CN/app.php @@ -0,0 +1,2477 @@ + [ + 'help' => '帮助', + 'leads' => '线索', + 'lead' => '线索', + 'quotes' => '报价单', + 'mail' => '邮件', + 'inbox' => '收件箱', + 'draft' => '草稿箱', + 'outbox' => '发件箱', + 'sent' => '已发送', + 'trash' => '回收站', + 'activities' => '活动', + 'webhook' => 'Webhook', + 'contacts' => '联系人', + 'persons' => '联系人', + 'organizations' => '组织', + 'products' => '产品', + 'settings' => '设置', + 'groups' => '用户组', + 'roles' => '角色', + 'users' => '用户', + 'user' => '用户', + 'automation' => '自动化', + 'attributes' => '属性', + 'pipelines' => '销售流程', + 'sources' => '来源', + 'types' => '类型', + 'email-templates' => '邮件模板', + 'workflows' => '工作流', + 'other-settings' => '其他设置', + 'tags' => '标签', + 'configuration' => '配置', + 'create' => '创建', + 'quick_add' => '快速添加', + 'edit' => '编辑', + 'view' => '查看', + 'print' => '打印', + 'delete' => '删除', + 'export' => '导出', + 'mass-delete' => '批量删除', + 'data-transfer' => '数据传输', + 'imports' => '导入记录', + 'import' => '导入', + 'event' => '事件', + 'campaigns' => '营销活动', + 'warehouses' => '仓库', + 'inventory' => '库存', + ], + 'users' => [ + 'activate-warning' => '您的账户尚未激活,请联系管理员。', + 'login-error' => '凭据与我们的记录不匹配。', + 'not-permission' => '您没有权限访问管理面板。', + 'login' => [ + 'email' => '邮箱地址', + 'forget-password-link' => '忘记密码?', + 'password' => '密码', + 'submit-btn' => '登录', + 'title' => '登录', + ], + 'forget-password' => [ + 'create' => [ + 'email' => '注册邮箱', + 'email-not-exist' => '邮箱不存在', + 'page-title' => '忘记密码', + 'reset-link-sent' => '重置密码链接已发送', + 'sign-in-link' => '返回登录?', + 'submit-btn' => '重置', + 'title' => '找回密码', + ], + ], + 'reset-password' => [ + 'back-link-title' => '返回登录?', + 'confirm-password' => '确认密码', + 'email' => '注册邮箱', + 'password' => '密码', + 'submit-btn' => '重置密码', + 'title' => '重置密码', + ], + ], + 'account' => [ + 'edit' => [ + 'back-btn' => '返回', + 'change-password' => '修改密码', + 'confirm-password' => '确认密码', + 'current-password' => '当前密码', + 'email' => '邮箱', + 'general' => '常规', + 'invalid-password' => '您输入的当前密码不正确。', + 'name' => '姓名', + 'password' => '密码', + 'profile-image' => '头像', + 'save-btn' => '保存账户', + 'title' => '我的账户', + 'update-success' => '账户更新成功', + 'upload-image-info' => '上传 PNG 或 JPG 格式的头像(110px X 110px)', + ], + ], + 'components' => [ + 'activities' => [ + 'actions' => [ + 'mail' => [ + 'btn' => '邮件', + 'title' => '写邮件', + 'to' => '收件人', + 'enter-emails' => '按回车键添加邮箱', + 'cc' => '抄送', + 'bcc' => '密送', + 'subject' => '主题', + 'send-btn' => '发送', + 'message' => '正文', + ], + 'file' => [ + 'btn' => '文件', + 'title' => '添加文件', + 'title-control' => '标题', + 'name' => '名称', + 'description' => '描述', + 'file' => '文件', + 'save-btn' => '保存文件', + ], + 'note' => [ + 'btn' => '备注', + 'title' => '添加备注', + 'comment' => '内容', + 'save-btn' => '保存备注', + ], + 'activity' => [ + 'btn' => '活动', + 'title' => '添加活动', + 'title-control' => '标题', + 'description' => '描述', + 'schedule-from' => '开始时间', + 'schedule-to' => '结束时间', + 'location' => '地点', + 'call' => '通话', + 'meeting' => '会议', + 'lunch' => '午餐', + 'save-btn' => '保存活动', + 'participants' => [ + 'title' => '参与者', + 'placeholder' => '输入以搜索参与者', + 'users' => '用户', + 'persons' => '联系人', + 'no-results' => '未找到结果……', + ], + ], + ], + 'index' => [ + 'all' => '全部', + 'bcc' => '密送', + 'by-user' => '由 :user', + 'calls' => '通话', + 'cc' => '抄送', + 'change-log' => '变更日志', + 'delete' => '删除', + 'edit' => '编辑', + 'emails' => '邮件', + 'empty' => '空', + 'files' => '文件', + 'from' => '发件人', + 'location' => '地点', + 'lunches' => '午餐', + 'mark-as-done' => '标记为已完成', + 'meetings' => '会议', + 'notes' => '备注', + 'participants' => '参与者', + 'planned' => '已计划', + 'quotes' => '报价单', + 'scheduled-on' => '计划于', + 'system' => '系统', + 'to' => '收件人', + 'unlink' => '解除关联', + 'view' => '查看', + 'empty-placeholders' => [ + 'all' => [ + 'title' => '未找到活动', + 'description' => '未找到相关活动。您可以点击左侧面板的“活动”按钮来添加活动。', + ], + 'planned' => [ + 'title' => '未找到已计划的活动', + 'description' => '未找到相关的已计划活动。您可以点击左侧面板的“活动”按钮来添加已计划的活动。', + ], + 'notes' => [ + 'title' => '未找到备注', + 'description' => '未找到相关备注。您可以点击左侧面板的“备注”按钮来添加备注。', + ], + 'calls' => [ + 'title' => '未找到通话', + 'description' => '未找到相关通话。您可以点击左侧面板的“活动”按钮并选择“通话”类型来添加通话。', + ], + 'meetings' => [ + 'title' => '未找到会议', + 'description' => '未找到相关会议。您可以点击左侧面板的“活动”按钮并选择“会议”类型来添加会议。', + ], + 'lunches' => [ + 'title' => '未找到午餐', + 'description' => '未找到相关午餐。您可以点击左侧面板的“活动”按钮并选择“午餐”类型来添加午餐。', + ], + 'files' => [ + 'title' => '未找到文件', + 'description' => '未找到相关文件。您可以点击左侧面板的“文件”按钮来添加文件。', + ], + 'emails' => [ + 'title' => '未找到邮件', + 'description' => '未找到相关邮件。您可以点击左侧面板的“邮件”按钮来添加邮件。', + ], + 'system' => [ + 'title' => '未找到变更日志', + 'description' => '未找到相关的变更日志。', + ], + ], + ], + ], + 'media' => [ + 'images' => [ + 'add-image-btn' => '添加图片', + 'ai-add-image-btn' => 'Magic AI', + 'allowed-types' => 'png, jpeg, jpg', + 'not-allowed-error' => '仅允许上传图片文件(.jpeg、.jpg、.png 等)。', + 'placeholders' => [ + 'front' => '正面', + 'next' => '下一个', + 'size' => '尺寸', + 'use-cases' => '使用场景', + 'zoom' => '缩放', + ], + ], + 'videos' => [ + 'add-video-btn' => '添加视频', + 'allowed-types' => 'mp4, webm, mkv', + 'not-allowed-error' => '仅允许上传视频文件(.mp4、.mov、.ogg 等)。', + ], + ], + 'datagrid' => [ + 'index' => [ + 'no-records-selected' => '未选择任何记录。', + 'must-select-a-mass-action-option' => '您必须选择一个批量操作选项。', + 'must-select-a-mass-action' => '您必须选择一项批量操作。', + ], + 'toolbar' => [ + 'length-of' => ':length 条,共', + 'of' => '共', + 'per-page' => '每页', + 'results' => '共 :total 条结果', + 'delete' => '删除', + 'selected' => '已选择 :total 项', + 'mass-actions' => [ + 'submit' => '提交', + 'select-option' => '选择选项', + 'select-action' => '选择操作', + ], + 'filter' => [ + 'apply-filters-btn' => '应用筛选', + 'back-btn' => '返回', + 'create-new-filter' => '创建新筛选器', + 'custom-filters' => '自定义筛选器', + 'delete-error' => '删除筛选器时出现问题,请重试。', + 'delete-success' => '筛选器删除成功。', + 'empty-description' => '没有可保存的已选筛选条件,请先选择筛选条件。', + 'empty-title' => '添加筛选条件以保存', + 'name' => '名称', + 'quick-filters' => '快速筛选', + 'save-btn' => '保存', + 'save-filter' => '保存筛选器', + 'saved-success' => '筛选器保存成功。', + 'selected-filters' => '已选筛选条件', + 'title' => '筛选', + 'update' => '更新', + 'update-filter' => '更新筛选器', + 'updated-success' => '筛选器更新成功。', + ], + 'search' => [ + 'title' => '搜索', + ], + 'column-settings' => [ + 'title' => '列设置', + ], + ], + 'filters' => [ + 'select' => '选择', + 'title' => '筛选条件', + 'dropdown' => [ + 'searchable' => [ + 'at-least-two-chars' => '请至少输入 2 个字符……', + 'no-results' => '未找到结果……', + ], + ], + 'custom-filters' => [ + 'clear-all' => '全部清除', + 'title' => '自定义筛选器', + ], + 'boolean-options' => [ + 'false' => '否', + 'true' => '是', + ], + 'date-options' => [ + 'last-month' => '上个月', + 'last-six-months' => '最近 6 个月', + 'last-three-months' => '最近 3 个月', + 'this-month' => '本月', + 'this-week' => '本周', + 'this-year' => '今年', + 'today' => '今天', + 'yesterday' => '昨天', + ], + ], + 'table' => [ + 'actions' => '操作', + 'no-records-available' => '暂无记录。', + ], + ], + 'modal' => [ + 'confirm' => [ + 'agree-btn' => '同意', + 'disagree-btn' => '不同意', + 'message' => '您确定要执行此操作吗?', + 'title' => '您确定吗?', + ], + ], + 'tags' => [ + 'index' => [ + 'title' => '标签', + 'added-tags' => '已添加的标签', + 'save-btn' => '保存标签', + 'placeholder' => '输入以搜索标签', + 'add-tag' => '添加 \\":term\\"……', + 'aquarelle-red' => '水彩红', + 'crushed-cashew' => '腰果棕', + 'beeswax' => '蜂蜡黄', + 'lemon-chiffon' => '柠檬雪纺黄', + 'snow-flurry' => '飞雪绿', + 'honeydew' => '蜜瓜绿', + ], + ], + 'layouts' => [ + 'powered-by' => [ + 'description' => '由 :krayin 提供支持,这是 :webkul 的开源项目。', + ], + 'header' => [ + 'mega-search' => [ + 'title' => '全局搜索', + 'tabs' => [ + 'leads' => '线索', + 'quotes' => '报价单', + 'persons' => '联系人', + 'products' => '产品', + 'settings' => '设置', + 'configurations' => '配置', + ], + 'explore-all-products' => '浏览全部产品', + 'explore-all-leads' => '浏览全部线索', + 'explore-all-contacts' => '浏览全部联系人', + 'explore-all-quotes' => '浏览全部报价单', + 'explore-all-settings' => '浏览全部设置', + 'explore-all-configurations' => '浏览全部配置', + 'explore-all-matching-products' => '浏览与 ":query" 匹配的全部产品 (:count)', + 'explore-all-matching-leads' => '浏览与 ":query" 匹配的全部线索 (:count)', + 'explore-all-matching-contacts' => '浏览与 ":query" 匹配的全部联系人 (:count)', + 'explore-all-matching-quotes' => '浏览与 ":query" 匹配的全部报价单 (:count)', + ], + ], + ], + 'attributes' => [ + 'edit' => [ + 'delete' => '删除', + ], + 'lookup' => [ + 'click-to-add' => '点击添加', + 'search' => '搜索……', + 'no-result-found' => '未找到结果', + ], + ], + 'lookup' => [ + 'click-to-add' => '点击添加', + 'no-results' => '未找到结果', + 'add-as-new' => '添加为新记录', + 'search' => '搜索……', + ], + 'flash-group' => [ + 'success' => '成功', + 'error' => '错误', + 'warning' => '警告', + 'info' => '提示', + ], + 'tiny-mce' => [ + 'http-error' => 'HTTP 错误', + 'invalid-json' => '服务器返回的 JSON 响应无效。', + 'upload-failed' => '文件上传失败,请重试。', + ], + ], + 'quotes' => [ + 'index' => [ + 'title' => '报价单', + 'create-btn' => '创建报价单', + 'create-success' => '报价单创建成功。', + 'update-success' => '报价单更新成功。', + 'delete-success' => '报价单删除成功。', + 'delete-failed' => '报价单无法删除。', + 'datagrid' => [ + 'subject' => '主题', + 'sales-person' => '销售人员', + 'expired-at' => '过期时间', + 'created-at' => '创建时间', + 'person' => '联系人', + 'subtotal' => '小计', + 'discount' => '折扣', + 'tax' => '税费', + 'adjustment' => '调整', + 'grand-total' => '总计', + 'edit' => '编辑', + 'delete' => '删除', + 'print' => '打印', + ], + 'pdf' => [ + 'adjustment' => '调整', + 'amount' => '金额', + 'billing-address' => '账单地址', + 'date' => '日期', + 'discount' => '折扣', + 'description' => '描述', + 'expired-at' => '过期时间', + 'grand-total' => '总计', + 'person' => '联系人', + 'price' => '价格', + 'product-name' => '产品名称', + 'quantity' => '数量', + 'quote-id' => '报价单编号', + 'sales-person' => '销售人员', + 'shipping-address' => '收货地址', + 'sku' => 'SKU', + 'sub-total' => '小计', + 'subject' => '主题', + 'tax' => '税费', + 'title' => '报价单', + ], + ], + 'create' => [ + 'title' => '创建报价单', + 'save-btn' => '保存报价单', + 'quote-info' => '报价单信息', + 'quote-info-info' => '填写报价单的基本信息。', + 'address-info' => '地址信息', + 'address-info-info' => '与报价单相关的地址信息。', + 'same-as-billing' => '收货地址与账单地址相同', + 'quote-items' => '报价单条目', + 'search-products' => '搜索产品', + 'link-to-lead' => '关联到线索', + 'quote-item-info' => '为此报价单添加产品需求。', + 'quote-name' => '报价单名称', + 'quantity' => '数量', + 'price' => '价格', + 'discount' => '折扣', + 'tax' => '税费', + 'total' => '合计', + 'amount' => '金额', + 'add-item' => '+ 添加条目', + 'sub-total' => '小计 (:symbol)', + 'total-discount' => '折扣 (:symbol)', + 'total-tax' => '税费 (:symbol)', + 'total-adjustment' => '调整 (:symbol)', + 'grand-total' => '总计 (:symbol)', + 'discount-amount' => '折扣金额', + 'tax-amount' => '税额', + 'adjustment-amount' => '调整金额', + 'product-name' => '产品名称', + 'action' => '操作', + ], + 'edit' => [ + 'title' => '编辑报价单', + 'save-btn' => '保存报价单', + 'quote-info' => '报价单信息', + 'quote-info-info' => '填写报价单的基本信息。', + 'address-info' => '地址信息', + 'address-info-info' => '与报价单相关的地址信息。', + 'quote-items' => '报价单条目', + 'link-to-lead' => '关联到线索', + 'quote-item-info' => '为此报价单添加产品需求。', + 'quote-name' => '报价单名称', + 'quantity' => '数量', + 'price' => '价格', + 'search-products' => '搜索产品', + 'discount' => '折扣', + 'tax' => '税费', + 'total' => '合计', + 'amount' => '金额', + 'add-item' => '+ 添加条目', + 'sub-total' => '小计 (:symbol)', + 'total-discount' => '折扣 (:symbol)', + 'total-tax' => '税费 (:symbol)', + 'total-adjustment' => '调整 (:symbol)', + 'grand-total' => '总计 (:symbol)', + 'discount-amount' => '折扣金额', + 'tax-amount' => '税额', + 'adjustment-amount' => '调整金额', + 'product-name' => '产品名称', + 'action' => '操作', + ], + ], + 'contacts' => [ + 'persons' => [ + 'index' => [ + 'title' => '联系人', + 'create-btn' => '创建联系人', + 'create-success' => '联系人创建成功。', + 'update-success' => '联系人更新成功。', + 'all-delete-success' => '所有选中的联系人均已删除成功。', + 'partial-delete-warning' => '部分联系人已删除成功,其余联系人因关联了线索而无法删除。', + 'none-delete-warning' => '选中的联系人均因关联了线索而无法删除。', + 'no-selection' => '未选择要删除的联系人。', + 'delete-failed' => '删除选中的联系人失败。', + 'datagrid' => [ + 'contact-numbers' => '联系电话', + 'delete' => '删除', + 'edit' => '编辑', + 'emails' => '邮箱', + 'id' => 'ID', + 'view' => '查看', + 'name' => '姓名', + 'organization-name' => '组织名称', + 'tag-name' => '标签名称', + 'google-export-not-connected' => '请先在“设置 > 其他设置 > Google 通讯录”中连接您的 Google 账户。', + 'google-export-started' => '已开始导出到 Google 通讯录。', + ], + ], + 'view' => [ + 'title' => ':name', + 'about-person' => '联系人信息', + 'about-organization' => '组织信息', + 'activities' => [ + 'index' => [ + 'all' => '全部', + 'calls' => '通话', + 'meetings' => '会议', + 'lunches' => '午餐', + 'files' => '文件', + 'quotes' => '报价单', + 'notes' => '备注', + 'emails' => '邮件', + 'by-user' => '由 :user', + 'scheduled-on' => '计划于', + 'location' => '地点', + 'participants' => '参与者', + 'mark-as-done' => '标记为已完成', + 'delete' => '删除', + 'edit' => '编辑', + ], + 'actions' => [ + 'mail' => [ + 'btn' => '邮件', + 'title' => '写邮件', + 'to' => '收件人', + 'cc' => '抄送', + 'bcc' => '密送', + 'subject' => '主题', + 'send-btn' => '发送', + 'message' => '正文', + ], + 'file' => [ + 'btn' => '文件', + 'title' => '添加文件', + 'title-control' => '标题', + 'name' => '文件名称', + 'description' => '描述', + 'file' => '文件', + 'save-btn' => '保存文件', + ], + 'note' => [ + 'btn' => '备注', + 'title' => '添加备注', + 'comment' => '内容', + 'save-btn' => '保存备注', + ], + 'activity' => [ + 'btn' => '活动', + 'title' => '添加活动', + 'title-control' => '标题', + 'description' => '描述', + 'schedule-from' => '开始时间', + 'schedule-to' => '结束时间', + 'location' => '地点', + 'call' => '通话', + 'meeting' => '会议', + 'lunch' => '午餐', + 'save-btn' => '保存活动', + ], + ], + ], + 'tags' => [ + 'create-success' => '标签创建成功。', + 'destroy-success' => '标签删除成功。', + ], + ], + 'create' => [ + 'title' => '创建联系人', + 'save-btn' => '保存联系人', + ], + 'edit' => [ + 'title' => '编辑联系人', + 'save-btn' => '保存联系人', + ], + ], + 'organizations' => [ + 'index' => [ + 'title' => '组织', + 'create-btn' => '创建组织', + 'create-success' => '组织创建成功。', + 'update-success' => '组织更新成功。', + 'delete-success' => '组织删除成功。', + 'delete-failed' => '组织无法删除。', + 'datagrid' => [ + 'delete' => '删除', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + 'persons-count' => '联系人数量', + ], + ], + 'create' => [ + 'title' => '创建组织', + 'save-btn' => '保存组织', + ], + 'edit' => [ + 'title' => '编辑组织', + 'save-btn' => '保存组织', + ], + ], + ], + 'products' => [ + 'index' => [ + 'title' => '产品', + 'create-btn' => '创建产品', + 'create-success' => '产品创建成功。', + 'update-success' => '产品更新成功。', + 'delete-success' => '产品删除成功。', + 'delete-failed' => '产品无法删除。', + 'datagrid' => [ + 'allocated' => '已分配', + 'delete' => '删除', + 'edit' => '编辑', + 'id' => 'ID', + 'in-stock' => '库存量', + 'name' => '名称', + 'on-hand' => '现有量', + 'tag-name' => '标签名称', + 'price' => '价格', + 'sku' => 'SKU', + 'view' => '查看', + ], + ], + 'create' => [ + 'save-btn' => '保存产品', + 'title' => '创建产品', + 'general' => '常规', + 'price' => '价格', + ], + 'edit' => [ + 'title' => '编辑产品', + 'save-btn' => '保存产品', + 'general' => '常规', + 'price' => '价格', + ], + 'view' => [ + 'sku' => 'SKU', + 'all' => '全部', + 'notes' => '备注', + 'files' => '文件', + 'inventories' => '库存', + 'change-logs' => '变更日志', + 'attributes' => [ + 'about-product' => '产品信息', + ], + 'inventory' => [ + 'warehouse' => '仓库', + 'in-stock' => '库存量', + 'allocated' => '已分配', + 'on-hand' => '现有量', + 'actions' => '操作', + 'assign' => '分配', + 'add-warehouse' => '添加仓库', + 'location' => '位置', + 'add-more' => '继续添加', + 'save' => '保存', + ], + ], + ], + 'settings' => [ + 'title' => '设置', + 'groups' => [ + 'index' => [ + 'create-btn' => '创建用户组', + 'title' => '用户组', + 'create-success' => '用户组创建成功。', + 'update-success' => '用户组更新成功。', + 'destroy-success' => '用户组删除成功。', + 'delete-failed' => '用户组无法删除。', + 'delete-failed-associated-users' => '该用户组正在被用户使用,无法删除。', + 'datagrid' => [ + 'delete' => '删除', + 'description' => '描述', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + ], + 'edit' => [ + 'title' => '编辑用户组', + ], + 'create' => [ + 'name' => '名称', + 'title' => '创建用户组', + 'description' => '描述', + 'save-btn' => '保存用户组', + ], + ], + ], + 'roles' => [ + 'index' => [ + 'being-used' => '该角色正在被管理员用户使用,无法删除。', + 'create-btn' => '创建角色', + 'create-success' => '角色创建成功。', + 'current-role-delete-error' => '无法删除已分配给当前用户的角色。', + 'current-role-edit-error' => '无法编辑分配给您自己账户的角色。', + 'delete-failed' => '角色无法删除。', + 'delete-success' => '角色删除成功。', + 'last-delete-error' => '至少需要保留一个角色。', + 'settings' => '设置', + 'title' => '角色', + 'update-success' => '角色更新成功。', + 'user-define-error' => '无法删除系统角色。', + 'datagrid' => [ + 'all' => '全部', + 'custom' => '自定义', + 'delete' => '删除', + 'description' => '描述', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + 'permission-type' => '权限类型', + ], + ], + 'create' => [ + 'access-control' => '访问控制', + 'all' => '全部', + 'back-btn' => '返回', + 'custom' => '自定义', + 'description' => '描述', + 'general' => '常规', + 'name' => '名称', + 'permissions' => '权限', + 'save-btn' => '保存角色', + 'title' => '创建角色', + ], + 'edit' => [ + 'access-control' => '访问控制', + 'all' => '全部', + 'back-btn' => '返回', + 'custom' => '自定义', + 'description' => '描述', + 'general' => '常规', + 'name' => '名称', + 'permissions' => '权限', + 'save-btn' => '保存角色', + 'title' => '编辑角色', + ], + ], + 'types' => [ + 'index' => [ + 'create-btn' => '创建类型', + 'create-success' => '类型创建成功。', + 'delete-failed' => '类型无法删除。', + 'delete-success' => '类型删除成功。', + 'title' => '类型', + 'update-success' => '类型更新成功。', + 'datagrid' => [ + 'delete' => '删除', + 'description' => '描述', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + ], + 'create' => [ + 'name' => '名称', + 'save-btn' => '保存类型', + 'title' => '创建类型', + ], + 'edit' => [ + 'title' => '编辑类型', + ], + ], + ], + 'sources' => [ + 'index' => [ + 'title' => '来源', + 'create-btn' => '创建来源', + 'create-success' => '来源创建成功。', + 'delete-failed' => '来源无法删除。', + 'delete-success' => '来源删除成功。', + 'update-success' => '来源更新成功。', + 'delete-failed-associated-leads' => '该来源已与现有线索关联,无法删除。请先解除关联或更新这些线索后再删除。', + 'datagrid' => [ + 'delete' => '删除', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + ], + 'create' => [ + 'name' => '名称', + 'save-btn' => '保存来源', + 'title' => '创建来源', + ], + 'edit' => [ + 'title' => '编辑来源', + ], + ], + ], + 'workflows' => [ + 'index' => [ + 'title' => '工作流', + 'create-btn' => '创建工作流', + 'create-success' => '工作流创建成功。', + 'update-success' => '工作流更新成功。', + 'delete-success' => '工作流删除成功。', + 'delete-failed' => '工作流无法删除。', + 'datagrid' => [ + 'delete' => '删除', + 'description' => '描述', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + ], + ], + 'helpers' => [ + 'update-related-leads' => '更新相关线索', + 'send-email-to-sales-owner' => '向销售负责人发送邮件', + 'send-email-to-participants' => '向参与者发送邮件', + 'add-webhook' => '添加 Webhook', + 'update-lead' => '更新线索', + 'update-person' => '更新联系人', + 'send-email-to-person' => '向联系人发送邮件', + 'add-tag' => '添加标签', + 'add-note-as-activity' => '将备注添加为活动', + 'update-quote' => '更新报价单', + ], + 'create' => [ + 'title' => '创建工作流', + 'event' => '事件', + 'back-btn' => '返回', + 'save-btn' => '保存工作流', + 'name' => '名称', + 'basic-details' => '基本信息', + 'description' => '描述', + 'actions' => '操作', + 'basic-details-info' => '填写工作流的基本信息。', + 'event-info' => '事件用于触发、检查条件并执行预定义的操作。', + 'conditions' => '条件', + 'conditions-info' => '条件是在特定情形下触发的场景检查规则。', + 'actions-info' => '操作不仅能减少工作量,还能让 CRM 自动化变得更加轻松', + 'value' => '值', + 'condition-type' => '条件类型', + 'all-condition-are-true' => '所有条件均成立', + 'any-condition-are-true' => '任一条件成立', + 'add-condition' => '添加条件', + 'add-action' => '添加操作', + 'no-webhook-found' => '未找到 Webhook。', + 'create-webhook' => '创建新的 Webhook。', + 'yes' => '是', + 'no' => '否', + 'email' => '邮箱', + 'is-equal-to' => '等于', + 'is-not-equal-to' => '不等于', + 'equals-or-greater-than' => '大于或等于', + 'equals-or-less-than' => '小于或等于', + 'greater-than' => '大于', + 'less-than' => '小于', + 'type' => '类型', + 'contain' => '包含', + 'contains' => '包含', + 'does-not-contain' => '不包含', + ], + 'edit' => [ + 'title' => '编辑工作流', + 'event' => '事件', + 'back-btn' => '返回', + 'save-btn' => '保存工作流', + 'name' => '名称', + 'basic-details' => '基本信息', + 'description' => '描述', + 'actions' => '操作', + 'type' => '类型', + 'basic-details-info' => '填写工作流的基本信息。', + 'event-info' => '事件用于触发、检查条件并执行预定义的操作。', + 'conditions' => '条件', + 'conditions-info' => '条件是在特定情形下触发的场景检查规则。', + 'actions-info' => '操作不仅能减少工作量,还能让 CRM 自动化变得更加轻松', + 'value' => '值', + 'condition-type' => '条件类型', + 'all-condition-are-true' => '所有条件均成立', + 'any-condition-are-true' => '任一条件成立', + 'add-condition' => '添加条件', + 'add-action' => '添加操作', + 'yes' => '是', + 'no' => '否', + 'email' => '邮箱', + 'is-equal-to' => '等于', + 'is-not-equal-to' => '不等于', + 'equals-or-greater-than' => '大于或等于', + 'equals-or-less-than' => '小于或等于', + 'greater-than' => '大于', + 'less-than' => '小于', + 'contain' => '包含', + 'contains' => '包含', + 'does-not-contain' => '不包含', + ], + ], + 'google-contacts' => [ + 'index' => [ + 'title' => 'Google 通讯录', + 'not-connected-info' => '连接 Google 账户,以将 CRM 联系人导出到 Google 通讯录。', + 'connected-info' => '已连接为 :email。', + 'connect-btn' => '连接 Google 账户', + 'disconnect-btn' => '断开连接', + 'connect-success' => 'Google 账户连接成功。', + 'connect-failed' => '无法连接您的 Google 账户,请重试。', + 'not-configured' => '尚未配置 Google OAuth。请先设置所需的环境变量。', + 'disconnect-success' => 'Google 账户已成功断开连接。', + ], + ], + 'webforms' => [ + 'index' => [ + 'title' => '网页表单', + 'create-btn' => '创建网页表单', + 'create-success' => '网页表单创建成功。', + 'update-success' => '网页表单更新成功。', + 'delete-success' => '网页表单删除成功。', + 'delete-failed' => '网页表单无法删除。', + 'datagrid' => [ + 'id' => 'ID', + 'title' => '标题', + 'edit' => '编辑', + 'delete' => '删除', + ], + ], + 'create' => [ + 'title' => '创建网页表单', + 'add-attribute-btn' => '添加属性按钮', + 'attribute-label-color' => '属性标签颜色', + 'attributes' => '属性', + 'attributes-info' => '为表单添加自定义属性。', + 'background-color' => '背景颜色', + 'create-lead' => '创建线索', + 'customize-webform' => '自定义网页表单', + 'customize-webform-info' => '使用您选择的元素颜色来自定义网页表单。', + 'description' => '描述', + 'display-custom-message' => '显示自定义消息', + 'form-background-color' => '表单背景颜色', + 'form-submit-btn-color' => '表单提交按钮颜色', + 'form-submit-button-color' => '表单提交按钮颜色', + 'form-title-color' => '表单标题颜色', + 'general' => '常规', + 'leads' => '线索', + 'person' => '联系人', + 'pipeline' => '销售流程', + 'save-btn' => '保存网页表单', + 'submit-button-label' => '提交按钮文字', + 'submit-success-action' => '提交成功后的操作', + 'redirect-to-url' => '跳转到 URL', + 'choose-value' => '选择值', + 'select-file' => '选择文件', + 'select-image' => '选择图片', + 'enter-value' => '输入值', + ], + 'edit' => [ + 'add-attribute-btn' => '添加属性按钮', + 'attribute-label-color' => '属性标签颜色', + 'attributes' => '属性', + 'attributes-info' => '为表单添加自定义属性。', + 'background-color' => '背景颜色', + 'choose-value' => '选择值', + 'code-snippet' => '代码片段', + 'copied' => '已复制', + 'copy' => '复制', + 'create-lead' => '创建线索', + 'customize-webform' => '自定义网页表单', + 'customize-webform-info' => '使用您选择的元素颜色来自定义网页表单。', + 'description' => '描述', + 'display-custom-message' => '显示自定义消息', + 'embed' => '嵌入', + 'enter-value' => '输入值', + 'form-background-color' => '表单背景颜色', + 'form-submit-btn-color' => '表单提交按钮颜色', + 'form-submit-button-color' => '表单提交按钮颜色', + 'form-title-color' => '表单标题颜色', + 'general' => '常规', + 'leads' => '线索', + 'person' => '联系人', + 'pipeline' => '销售流程', + 'preview' => '预览', + 'public-url' => '公开 URL', + 'redirect-to-url' => '跳转到 URL', + 'save-btn' => '保存网页表单', + 'select-file' => '选择文件', + 'select-image' => '选择图片', + 'submit-button-label' => '提交按钮文字', + 'submit-success-action' => '提交成功后的操作', + 'title' => '编辑网页表单', + ], + ], + 'email-template' => [ + 'index' => [ + 'create-btn' => '创建邮件模板', + 'title' => '邮件模板', + 'create-success' => '邮件模板创建成功。', + 'update-success' => '邮件模板更新成功。', + 'delete-success' => '邮件模板删除成功。', + 'delete-failed' => '邮件模板无法删除。', + 'datagrid' => [ + 'delete' => '删除', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + 'subject' => '主题', + ], + ], + 'create' => [ + 'title' => '创建邮件模板', + 'save-btn' => '保存邮件模板', + 'email-template' => '邮件模板', + 'subject' => '主题', + 'content' => '内容', + 'subject-placeholders' => '主题占位符', + 'general' => '常规', + 'name' => '名称', + ], + 'edit' => [ + 'title' => '编辑邮件模板', + 'save-btn' => '保存邮件模板', + 'email-template' => '邮件模板', + 'subject' => '主题', + 'content' => '内容', + 'subject-placeholders' => '主题占位符', + 'general' => '常规', + 'name' => '名称', + ], + ], + 'marketing' => [ + 'events' => [ + 'index' => [ + 'create-btn' => '创建事件', + 'title' => '事件', + 'create-success' => '事件创建成功。', + 'update-success' => '事件更新成功。', + 'delete-success' => '事件删除成功。', + 'delete-failed' => '事件无法删除。', + 'mass-delete-success' => '事件删除成功', + 'delete-failed-associated-campaigns' => '该事件已与现有营销活动关联,无法删除。请先解除关联或更新这些营销活动后再删除。', + 'datagrid' => [ + 'delete' => '删除', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + 'description' => '描述', + 'date' => '日期', + ], + 'create' => [ + 'title' => '创建事件', + 'name' => '名称', + 'date' => '日期', + 'description' => '描述', + 'save-btn' => '保存事件', + ], + 'edit' => [ + 'title' => '编辑事件', + ], + ], + ], + 'campaigns' => [ + 'index' => [ + 'create-btn' => '创建营销活动', + 'title' => '营销活动', + 'create-success' => '营销活动创建成功。', + 'update-success' => '营销活动更新成功。', + 'delete-success' => '营销活动删除成功。', + 'delete-failed' => '营销活动无法删除。', + 'mass-delete-success' => '营销活动删除成功。', + 'datagrid' => [ + 'id' => 'ID', + 'name' => '名称', + 'subject' => '主题', + 'status' => '状态', + 'active' => '启用', + 'inactive' => '停用', + 'edit' => '编辑', + 'delete' => '删除', + ], + 'create' => [ + 'title' => '创建营销活动', + 'name' => '名称', + 'type' => '类型', + 'subject' => '主题', + 'event' => '事件', + 'email-template' => '邮件模板', + 'status' => '状态', + ], + 'edit' => [ + 'title' => '编辑营销活动', + ], + ], + ], + ], + 'tags' => [ + 'index' => [ + 'create-btn' => '创建标签', + 'title' => '标签', + 'create-success' => '标签创建成功。', + 'update-success' => '标签更新成功。', + 'delete-success' => '标签删除成功。', + 'delete-failed' => '标签无法删除。', + 'datagrid' => [ + 'delete' => '删除', + 'edit' => '编辑', + 'id' => 'ID', + 'name' => '名称', + 'users' => '用户', + 'created-at' => '创建时间', + ], + 'create' => [ + 'name' => '名称', + 'save-btn' => '保存标签', + 'title' => '创建标签', + 'color' => '颜色', + ], + 'edit' => [ + 'title' => '编辑标签', + ], + ], + ], + 'users' => [ + 'index' => [ + 'create-btn' => '创建用户', + 'create-success' => '用户创建成功。', + 'delete-failed' => '用户无法删除。', + 'delete-success' => '用户删除成功。', + 'last-delete-error' => '至少需要保留一个用户。', + 'mass-delete-failed' => '用户无法删除。', + 'mass-delete-success' => '用户删除成功。', + 'mass-update-failed' => '用户无法更新。', + 'mass-update-success' => '用户更新成功。', + 'title' => '用户', + 'update-success' => '用户更新成功。', + 'user-define-error' => '无法删除系统用户。', + 'active' => '启用', + 'inactive' => '停用', + 'datagrid' => [ + 'active' => '启用', + 'created-at' => '创建时间', + 'delete' => '删除', + 'edit' => '编辑', + 'email' => '邮箱', + 'group' => '关联用户组', + 'id' => 'ID', + 'inactive' => '停用', + 'name' => '名称', + 'no-group' => '无用户组', + 'status' => '状态', + 'update-status' => '更新状态', + 'users' => '用户', + ], + 'create' => [ + 'confirm-password' => '确认密码', + 'email' => '邮箱', + 'general' => '常规', + 'global' => '全局', + 'group' => '用户组', + 'individual' => '个人', + 'name' => '名称', + 'password' => '密码', + 'permission' => '权限', + 'role' => '角色', + 'save-btn' => '保存用户', + 'status' => '状态', + 'title' => '创建用户', + 'view-permission' => '查看权限', + 'select-at-lest-one-group' => '请至少选择一个用户组', + ], + 'edit' => [ + 'title' => '编辑用户', + ], + ], + ], + 'pipelines' => [ + 'index' => [ + 'title' => '销售流程', + 'create-btn' => '创建销售流程', + 'create-success' => '销售流程创建成功。', + 'update-success' => '销售流程更新成功。', + 'default-required' => '至少需要保留一个默认销售流程。', + 'delete-success' => '销售流程删除成功。', + 'delete-failed' => '销售流程无法删除。', + 'default-delete-error' => '默认销售流程无法删除。', + 'datagrid' => [ + 'delete' => '删除', + 'edit' => '编辑', + 'id' => 'ID', + 'is-default' => '是否默认', + 'name' => '名称', + 'no' => '否', + 'rotten-days' => '滞留天数', + 'yes' => '是', + ], + ], + 'create' => [ + 'title' => '创建销售流程', + 'save-btn' => '保存销售流程', + 'name' => '名称', + 'rotten-days' => '滞留天数', + 'mark-as-default' => '设为默认', + 'general' => '常规', + 'probability' => '成功率(%)', + 'new-stage' => '新建', + 'won-stage' => '赢单', + 'lost-stage' => '失单', + 'stage-btn' => '添加阶段', + 'stages' => '阶段', + 'duplicate-name' => '名称字段不能重复', + 'delete-stage' => '删除阶段', + 'add-new-stages' => '添加新阶段', + 'add-stage-info' => '为您的销售流程添加新阶段', + 'newly-added' => '新增', + 'stage-delete-success' => '阶段删除成功', + ], + 'edit' => [ + 'title' => '编辑销售流程', + 'save-btn' => '保存销售流程', + 'name' => '名称', + 'rotten-days' => '滞留天数', + 'mark-as-default' => '设为默认', + 'general' => '常规', + 'probability' => '成功率(%)', + 'new-stage' => '新建', + 'won-stage' => '赢单', + 'lost-stage' => '失单', + 'stage-btn' => '添加阶段', + 'stages' => '阶段', + 'duplicate-name' => '名称字段不能重复', + 'delete-stage' => '删除阶段', + 'add-new-stages' => '添加新阶段', + 'add-stage-info' => '为您的销售流程添加新阶段', + 'stage-delete-success' => '阶段删除成功', + ], + ], + 'webhooks' => [ + 'index' => [ + 'title' => 'Webhook', + 'create-btn' => '创建 Webhook', + 'create-success' => 'Webhook 创建成功。', + 'update-success' => 'Webhook 更新成功。', + 'delete-success' => 'Webhook 删除成功。', + 'delete-failed' => 'Webhook 无法删除。', + 'invalid-endpoint' => 'Webhook 端点 URL 必须是可公开访问的 HTTP 或 HTTPS 地址。', + 'datagrid' => [ + 'id' => 'ID', + 'delete' => '删除', + 'edit' => '编辑', + 'name' => '名称', + 'entity-type' => '实体类型', + 'end-point' => '端点', + ], + ], + 'create' => [ + 'title' => '创建 Webhook', + 'save-btn' => '保存 Webhook', + 'info' => '输入 Webhook 的详细信息', + 'url-and-parameters' => 'URL 与参数', + 'method' => '请求方法', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'URL 端点', + 'parameters' => '参数', + 'add-new-parameter' => '添加新参数', + 'url-preview' => 'URL 预览:', + 'headers' => '请求头', + 'add-new-header' => '添加新请求头', + 'body' => '请求体', + 'default' => '默认', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => '键与值', + 'add-new-payload' => '添加新载荷', + 'raw' => '原始数据', + 'general' => '常规', + 'name' => '名称', + 'entity-type' => '实体类型', + 'insert-placeholder' => '插入占位符', + 'description' => '描述', + 'json' => 'Json', + 'text' => '文本', + ], + 'edit' => [ + 'title' => '编辑 Webhook', + 'edit-btn' => '保存 Webhook', + 'save-btn' => '保存 Webhook', + 'info' => '输入 Webhook 的详细信息', + 'url-and-parameters' => 'URL 与参数', + 'method' => '请求方法', + 'post' => 'Post', + 'put' => 'Put', + 'url-endpoint' => 'URL 端点', + 'parameters' => '参数', + 'add-new-parameter' => '添加新参数', + 'url-preview' => 'URL 预览:', + 'headers' => '请求头', + 'add-new-header' => '添加新请求头', + 'body' => '请求体', + 'default' => '默认', + 'x-www-form-urlencoded' => 'x-www-form-urlencoded', + 'key-and-value' => '键与值', + 'add-new-payload' => '添加新载荷', + 'raw' => '原始数据', + 'general' => '常规', + 'name' => '名称', + 'entity-type' => '实体类型', + 'insert-placeholder' => '插入占位符', + 'description' => '描述', + 'json' => 'Json', + 'text' => '文本', + ], + ], + 'warehouses' => [ + 'index' => [ + 'title' => '仓库', + 'create-btn' => '创建仓库', + 'create-success' => '仓库创建成功。', + 'name-exists' => '仓库名称已存在。', + 'update-success' => '仓库更新成功。', + 'delete-success' => '仓库删除成功。', + 'delete-failed' => '仓库无法删除。', + 'datagrid' => [ + 'id' => 'ID', + 'name' => '名称', + 'contact-name' => '联系人姓名', + 'delete' => '删除', + 'edit' => '编辑', + 'view' => '查看', + 'created-at' => '创建时间', + 'products' => '产品', + 'contact-emails' => '联系邮箱', + 'contact-numbers' => '联系电话', + ], + ], + 'create' => [ + 'title' => '创建仓库', + 'save-btn' => '保存仓库', + 'contact-info' => '联系信息', + ], + 'edit' => [ + 'title' => '编辑仓库', + 'save-btn' => '保存仓库', + 'contact-info' => '联系信息', + ], + 'view' => [ + 'all' => '全部', + 'notes' => '备注', + 'files' => '文件', + 'location' => '位置', + 'change-logs' => '变更日志', + 'locations' => [ + 'action' => '操作', + 'add-location' => '添加位置', + 'create-success' => '位置创建成功。', + 'delete' => '删除', + 'delete-failed' => '位置无法删除。', + 'delete-success' => '位置删除成功。', + 'name' => '名称', + 'save-btn' => '保存', + ], + 'general-information' => [ + 'title' => '基本信息', + ], + 'contact-information' => [ + 'title' => '联系信息', + ], + ], + ], + 'attributes' => [ + 'index' => [ + 'title' => '属性', + 'create-btn' => '创建属性', + 'create-success' => '属性创建成功。', + 'update-success' => '属性更新成功。', + 'delete-success' => '属性删除成功。', + 'delete-failed' => '属性无法删除。', + 'user-define-error' => '无法删除系统属性。', + 'mass-delete-failed' => '系统属性无法删除。', + 'datagrid' => [ + 'yes' => '是', + 'no' => '否', + 'id' => 'ID', + 'code' => '代码', + 'name' => '名称', + 'entity-type' => '实体类型', + 'type' => '类型', + 'is-default' => '是否默认', + 'edit' => '编辑', + 'delete' => '删除', + 'quick-add' => '快速添加', + 'entity-types' => [ + 'leads' => '线索', + 'organizations' => '组织', + 'persons' => '联系人', + 'products' => '产品', + 'quotes' => '报价单', + 'warehouses' => '仓库', + ], + 'types' => [ + 'text' => '单行文本', + 'textarea' => '多行文本', + 'price' => '价格', + 'boolean' => '布尔值', + 'select' => '下拉选择', + 'multiselect' => '多选', + 'checkbox' => '复选框', + 'email' => '邮箱', + 'address' => '地址', + 'phone' => '电话', + 'lookup' => '查找', + 'datetime' => '日期时间', + 'date' => '日期', + 'image' => '图片', + 'file' => '文件', + ], + ], + ], + 'create' => [ + 'title' => '创建属性', + 'save-btn' => '保存属性', + 'code' => '代码', + 'name' => '名称', + 'quick_add' => '快速添加', + 'entity-type' => '实体类型', + 'type' => '类型', + 'validations' => '验证规则', + 'is-required' => '是否必填', + 'input-validation' => '输入验证', + 'is-unique' => '是否唯一', + 'labels' => '标签', + 'general' => '常规', + 'none' => '无', + 'numeric' => '数字', + 'decimal' => '小数', + 'url' => 'URL', + 'options' => '选项', + 'option-type' => '选项类型', + 'lookup-type' => '查找类型', + 'add-option' => '添加选项', + 'save-option' => '保存选项', + 'option-name' => '选项名称', + 'add-attribute-options' => '添加属性选项', + 'text' => '单行文本', + 'textarea' => '多行文本', + 'price' => '价格', + 'boolean' => '布尔值', + 'select' => '下拉选择', + 'multiselect' => '多选', + 'email' => '邮箱', + 'address' => '地址', + 'phone' => '电话', + 'datetime' => '日期时间', + 'date' => '日期', + 'image' => '图片', + 'file' => '文件', + 'lookup' => '查找', + 'entity_type' => '实体类型', + 'checkbox' => '复选框', + 'is_required' => '是否必填', + 'is_unique' => '是否唯一', + 'actions' => '操作', + ], + 'edit' => [ + 'actions' => '操作', + 'add-attribute-options' => '添加属性选项', + 'add-option' => '添加选项', + 'address' => '地址', + 'boolean' => '布尔值', + 'checkbox' => '复选框', + 'code' => '代码', + 'date' => '日期', + 'datetime' => '日期时间', + 'decimal' => '小数', + 'email' => '邮箱', + 'entity-type' => '实体类型', + 'entity_type' => '实体类型', + 'file' => '文件', + 'general' => '常规', + 'image' => '图片', + 'input-validation' => '输入验证', + 'is-required' => '是否必填', + 'is-unique' => '是否唯一', + 'is_required' => '是否必填', + 'is_unique' => '是否唯一', + 'labels' => '标签', + 'lookup' => '查找', + 'lookup-type' => '查找类型', + 'multiselect' => '多选', + 'name' => '名称', + 'quick_add' => '快速添加', + 'none' => '无', + 'numeric' => '数字', + 'option-deleted' => '属性选项删除成功', + 'option-name' => '选项名称', + 'option-type' => '选项类型', + 'options' => '选项', + 'phone' => '电话', + 'price' => '价格', + 'save-btn' => '保存属性', + 'save-option' => '保存选项', + 'select' => '下拉选择', + 'text' => '单行文本', + 'textarea' => '多行文本', + 'title' => '编辑属性', + 'type' => '类型', + 'url' => 'URL', + 'validations' => '验证规则', + ], + ], + 'data-transfer' => [ + 'importers' => [ + 'persons' => [ + 'title' => '联系人', + 'validation' => [ + 'errors' => [ + 'duplicate-email' => '邮箱:\'%s\' 在导入文件中出现了多次。', + 'duplicate-phone' => '电话:\'%s\' 在导入文件中出现了多次。', + 'email-not-found' => '邮箱:\'%s\' 在系统中未找到。', + 'invalid-date' => '日期:\'%s\' 不是有效的日期。', + ], + ], + ], + 'products' => [ + 'title' => '产品', + 'validation' => [ + 'errors' => [ + 'sku-not-found' => '未找到指定 SKU 的产品', + ], + ], + ], + 'leads' => [ + 'title' => '线索', + 'validation' => [ + 'errors' => [ + 'id-not-found' => 'ID:\'%s\' 在系统中未找到。', + ], + ], + ], + ], + 'validation' => [ + 'errors' => [ + 'column-empty-headers' => '第 "%s" 列的表头为空。', + 'column-name-invalid' => '列名无效:"%s"。', + 'column-not-found' => '未找到必填列:%s。', + 'column-numbers' => '列数与表头中的行数不一致。', + 'invalid-attribute' => '表头包含无效属性:"%s"。', + 'system' => '发生了意外的系统错误。', + 'wrong-quotes' => '使用了弯引号而非直引号。', + 'already-exists' => ':attribute 已存在。', + ], + ], + 'imports' => [ + 'create' => [ + 'action' => '操作', + 'allowed-errors' => '允许的错误数', + 'back-btn' => '返回', + 'create-update' => '创建/更新', + 'delete' => '删除', + 'download-sample' => '下载示例文件', + 'field-separator' => '字段分隔符', + 'file' => '文件', + 'general' => '常规', + 'images-directory' => '图片目录路径', + 'process-in-queue' => '在队列中处理', + 'results' => '结果', + 'save-btn' => '保存导入', + 'settings' => '设置', + 'skip-errors' => '跳过错误', + 'stop-on-errors' => '出错时停止', + 'title' => '创建导入', + 'type' => '类型', + 'validation-strategy' => '验证策略', + ], + 'edit' => [ + 'action' => '操作', + 'allowed-errors' => '允许的错误数', + 'back-btn' => '返回', + 'create-update' => '创建/更新', + 'delete' => '删除', + 'download-sample' => '下载示例文件', + 'field-separator' => '字段分隔符', + 'file' => '文件', + 'general' => '常规', + 'images-directory' => '图片目录路径', + 'process-in-queue' => '在队列中处理', + 'results' => '结果', + 'save-btn' => '保存导入', + 'settings' => '设置', + 'skip-errors' => '跳过错误', + 'stop-on-errors' => '出错时停止', + 'title' => '编辑导入', + 'type' => '类型', + 'validation-strategy' => '验证策略', + ], + 'index' => [ + 'button-title' => '创建导入', + 'title' => '导入记录', + 'datagrid' => [ + 'actions' => '操作', + 'completed-at' => '完成时间', + 'created' => '已创建', + 'delete' => '删除', + 'deleted' => '已删除', + 'edit' => '编辑', + 'error-file' => '错误文件', + 'id' => 'ID', + 'started-at' => '开始时间', + 'state' => '状态', + 'summary' => '摘要', + 'type' => '类型', + 'updated' => '已更新', + 'uploaded-file' => '上传的文件', + ], + ], + 'import' => [ + 'back-btn' => '返回', + 'completed-batches' => '已完成批次总数:', + 'download-error-report' => '下载完整报告', + 'edit-btn' => '编辑', + 'imported-info' => '恭喜!导入成功。', + 'importing-info' => '导入进行中', + 'importing-queue-hint' => '如果此导入始终停留在 0%,队列工作进程可能未运行。请让管理员运行 \'php artisan queue:work\',或关闭“队列处理”后重新导入。', + 'indexing-info' => '资源索引(价格、库存和 Elastic Search)进行中', + 'linking-info' => '资源关联进行中', + 'progress' => '进度:', + 'title' => '导入', + 'total-batches' => '批次总数:', + 'total-created' => '创建的记录总数:', + 'total-deleted' => '删除的记录总数:', + 'total-errors' => '错误总数:', + 'total-invalid-rows' => '无效行总数:', + 'total-rows-processed' => '已处理行总数:', + 'total-updated' => '更新的记录总数:', + 'validate' => '验证', + 'validate-info' => '点击“验证数据”以检查您的导入内容。', + 'validating-info' => '已开始读取并验证数据', + 'validation-failed-info' => '您的导入内容无效。请修正以下错误后重试。', + 'validation-success-info' => '您的导入内容有效。点击“导入”开始导入流程。', + ], + 'create-success' => '导入创建成功。', + 'delete-failed' => '导入删除失败,发生意外错误。', + 'delete-success' => '导入删除成功。', + 'not-valid' => '导入内容无效', + 'nothing-to-import' => '没有可导入的资源。', + 'queue-worker-not-running' => '队列工作进程未运行。请运行 php artisan queue:work 来处理此导入,或关闭队列处理后重试。', + 'setup-queue-error' => '请将队列驱动改为 "database" 或 "redis" 后再开始导入流程。', + 'update-success' => '导入更新成功。', + ], + ], + ], + 'activities' => [ + 'index' => [ + 'title' => '活动', + 'datagrid' => [ + 'comment' => '内容', + 'created_at' => '创建时间', + 'created_by' => '创建者', + 'edit' => '编辑', + 'id' => 'ID', + 'done' => '已完成', + 'not-done' => '未完成', + 'lead' => '线索', + 'mass-delete' => '批量删除', + 'mass-update' => '批量更新', + 'schedule-from' => '开始时间', + 'schedule-to' => '结束时间', + 'schedule_from' => '开始时间', + 'schedule_to' => '结束时间', + 'title' => '标题', + 'is_done' => '已完成', + 'type' => '类型', + 'update' => '更新', + 'call' => '通话', + 'meeting' => '会议', + 'lunch' => '午餐', + ], + ], + 'edit' => [ + 'title' => '编辑活动', + 'back-btn' => '返回', + 'save-btn' => '保存活动', + 'type' => '活动类型', + 'call' => '通话', + 'meeting' => '会议', + 'lunch' => '午餐', + 'schedule_to' => '结束时间', + 'schedule_from' => '开始时间', + 'location' => '地点', + 'comment' => '内容', + 'lead' => '线索', + 'participants' => '参与者', + 'general' => '常规', + 'persons' => '联系人', + 'no-result-found' => '未找到记录。', + 'users' => '用户', + ], + 'updated' => '已更新 :attribute', + 'created' => '已创建', + 'duration-overlapping' => '参与者在该时间段还有另一场会议,是否继续?', + 'create-success' => '活动创建成功。', + 'update-success' => '活动更新成功。', + 'overlapping-error' => '参与者在该时间段还有另一场会议。', + 'destroy-success' => '活动删除成功。', + 'delete-failed' => '活动无法删除。', + 'mass-update-success' => '活动更新成功。', + 'mass-destroy-success' => '活动删除成功。', + 'mass-delete-failed' => '活动无法删除。', + ], + 'mail' => [ + 'index' => [ + 'compose' => '写邮件', + 'draft' => '草稿箱', + 'inbox' => '收件箱', + 'outbox' => '发件箱', + 'sent' => '已发送', + 'trash' => '回收站', + 'compose-mail-btn' => '写邮件', + 'btn' => '邮件', + 'mail' => [ + 'title' => '写邮件', + 'to' => '收件人', + 'enter-emails' => '按回车键添加邮箱', + 'cc' => '抄送', + 'bcc' => '密送', + 'subject' => '主题', + 'send-btn' => '发送', + 'message' => '正文', + 'draft' => '草稿', + ], + 'datagrid' => [ + 'id' => 'ID', + 'from' => '发件人', + 'to' => '收件人', + 'subject' => '主题', + 'tags' => '标签', + 'content' => '内容', + 'attachments' => '附件', + 'date' => '日期', + 'move-to-inbox' => '移至收件箱', + 'move-to-trash' => '移至回收站', + 'edit' => '编辑', + 'view' => '查看', + 'delete' => '删除', + ], + ], + 'create-success' => '邮件发送成功。', + 'update-success' => '邮件更新成功。', + 'mass-update-success' => '邮件更新成功。', + 'delete-success' => '邮件删除成功。', + 'delete-failed' => '邮件无法删除。', + 'invalid-route' => '邮件路由无效。', + 'unauthorized' => '此操作未获授权。', + 'view' => [ + 'title' => '邮件', + 'subject' => ':subject', + 'link-mail' => '关联邮件', + 'to' => '收件人', + 'cc' => '抄送', + 'bcc' => '密送', + 'reply' => '回复', + 'reply-all' => '全部回复', + 'forward' => '转发', + 'delete' => '删除', + 'enter-mails' => '输入邮箱地址', + 'rotten-days' => '该线索已滞留 :days 天', + 'search-an-existing-lead' => '搜索现有线索', + 'search-an-existing-contact' => '搜索现有联系人', + 'message' => '正文', + 'add-attachments' => '添加附件', + 'discard' => '放弃', + 'send' => '发送', + 'no-result-found' => '未找到结果', + 'add-new-contact' => '添加新联系人', + 'description' => '描述', + 'search' => '搜索……', + 'add-new-lead' => '添加新线索', + 'create-new-contact' => '创建新联系人', + 'save-contact' => '保存联系人', + 'create-lead' => '创建线索', + 'linked-contact' => '已关联的联系人', + 'link-to-contact' => '关联到联系人', + 'link-to-lead' => '关联到线索', + 'linked-lead' => '已关联的线索', + 'lead-details' => '线索详情', + 'contact-person' => '联系人', + 'product' => '产品', + 'tags' => [ + 'create-success' => '标签创建成功。', + 'destroy-success' => '标签删除成功。', + ], + ], + ], + 'common' => [ + 'custom-attributes' => [ + 'add-more' => '继续添加', + 'address' => '地址', + 'city' => '城市', + 'contact' => '联系电话', + 'country' => '国家', + 'email' => '邮箱', + 'home' => '住宅', + 'postcode' => '邮政编码', + 'save' => '保存', + 'select' => '选择', + 'select-country' => '选择国家', + 'select-state' => '选择省/州', + 'state' => '省/州', + 'update-contact-title' => '更新联系电话', + 'update-emails-title' => '更新联系邮箱', + 'work' => '办公', + ], + ], + 'leads' => [ + 'create-success' => '线索创建成功。', + 'update-success' => '线索更新成功。', + 'update-failed' => '线索无法删除。', + 'destroy-success' => '线索删除成功。', + 'destroy-failed' => '线索无法删除。', + 'file' => [ + 'data-not-found' => '未找到数据。', + 'empty-content' => 'PDF 内容为空或无法提取。', + 'failed-extract' => '从文件中提取文本失败。', + 'insufficient-info' => '由于数据不足,我们目前无法处理您的请求。', + 'invalid-base64' => 'base64 格式无效。', + 'invalid-format' => 'JSON 格式无效。', + 'invalid-response' => 'AI 响应格式无效。', + 'missing-api-key' => '缺少 API 密钥或模型配置。', + 'not-found' => '未找到文件。', + 'recursive-call' => '检测到递归调用。', + 'text-generation-failed' => '文本提取失败。文件可能为空或无法读取。', + ], + 'index' => [ + 'title' => '线索', + 'create-btn' => '创建线索', + 'datagrid' => [ + 'id' => 'ID', + 'sales-person' => '销售人员', + 'subject' => '主题', + 'source' => '来源', + 'lead-value' => '预估线索价值', + 'lead-type' => '线索类型', + 'tag-name' => '标签名称', + 'contact-person' => '联系人', + 'stage' => '阶段', + 'rotten-lead' => '滞留线索', + 'date-to' => '截止日期', + 'created-at' => '创建时间', + 'no' => '否', + 'yes' => '是', + 'delete' => '删除', + 'mass-delete' => '批量删除', + 'mass-update' => '批量更新', + ], + 'kanban' => [ + 'rotten-days' => '该线索已滞留 :days 天', + 'empty-list' => '您的线索列表为空', + 'empty-list-description' => '创建一条线索以梳理您的目标。', + 'create-lead-btn' => '创建线索', + 'columns' => [ + 'contact-person' => '联系人', + 'id' => 'ID', + 'lead-type' => '线索类型', + 'lead-value' => '预估线索价值', + 'sales-person' => '销售人员', + 'source' => '来源', + 'title' => '标题', + 'tags' => '标签', + 'expected-close-date' => '预计成交日期', + 'date-to' => '截止日期', + 'created-at' => '创建时间', + ], + 'toolbar' => [ + 'search' => [ + 'title' => '按标题搜索', + ], + 'filters' => [ + 'apply-filters' => '应用筛选', + 'clear-all' => '全部清除', + 'filter' => '筛选', + 'filters' => '筛选条件', + 'from' => '从', + 'select' => '选择', + 'to' => '至', + ], + 'card-settings' => [ + 'title' => '卡片设置', + 'contact-person' => '联系人', + 'rotten-indicator' => '滞留标识', + 'sales-person' => '销售人员', + 'estimated-lead-value' => '预估线索价值', + 'source' => '来源', + 'lead-type' => '线索类型', + 'tags' => '标签', + ], + ], + 'stages' => [ + 'won' => '赢单', + 'lost' => '失单', + 'need-more-info' => '需要更多信息', + 'closed-at' => '关闭时间', + 'won-value' => '成交金额', + 'lost-reason' => '失单原因', + 'save-btn' => '保存', + ], + ], + 'view-switcher' => [ + 'all-pipelines' => '全部销售流程', + 'create-new-pipeline' => '创建新的销售流程', + ], + 'upload' => [ + 'create-lead' => '使用 AI 创建线索', + 'file' => '文件上传', + 'file-info' => '仅接受 pdf、bmp、jpg、jpeg、png 格式的文件。', + 'file-required' => '请至少选择一个有效文件以继续。', + 'save-btn' => '保存', + 'upload-file' => '上传文件', + ], + ], + 'create' => [ + 'title' => '创建线索', + 'save-btn' => '保存', + 'details' => '详细信息', + 'details-info' => '填写线索的基本信息', + 'contact-person' => '联系人', + 'contact-info' => '关于联系人的信息', + 'products' => '产品', + 'products-info' => '关于产品的信息', + ], + 'edit' => [ + 'title' => '编辑线索', + 'save-btn' => '保存', + 'details' => '详细信息', + 'details-info' => '填写线索的基本信息', + 'contact-person' => '联系人', + 'contact-info' => '关于联系人的信息', + 'products' => '产品', + 'products-info' => '关于产品的信息', + ], + 'common' => [ + 'contact' => [ + 'name' => '姓名', + 'email' => '邮箱', + 'name-search-placeholder' => '按姓名、邮箱或电话搜索', + 'contact-number' => '联系电话', + 'organization' => '组织', + ], + 'products' => [ + 'product-name' => '产品名称', + 'quantity' => '数量', + 'price' => '价格', + 'amount' => '金额', + 'action' => '操作', + 'add-more' => '继续添加', + 'total' => '合计', + ], + ], + 'view' => [ + 'title' => '线索::title', + 'rotten-days' => ':days 天', + 'tabs' => [ + 'description' => '描述', + 'products' => '产品', + 'quotes' => '报价单', + ], + 'attributes' => [ + 'title' => '线索信息', + ], + 'quotes' => [ + 'id' => 'ID', + 'subject' => '主题', + 'expired-at' => '过期时间', + 'sub-total' => '小计', + 'discount' => '折扣', + 'tax' => '税费', + 'adjustment' => '调整', + 'grand-total' => '总计', + 'delete' => '删除', + 'edit' => '编辑', + 'download' => '下载', + 'destroy-success' => '报价单删除成功。', + 'empty-title' => '未找到报价单', + 'empty-info' => '该线索下未找到报价单', + 'add-btn' => '添加报价单', + 'mail' => '邮件', + 'quote-not-found' => '该线索下未找到报价单。', + 'person-email-unavailable' => '线索联系人的邮箱不可用。', + 'mail-subject' => '报价单::subject', + 'mail-body' => '请查收附件中的报价单 PDF。此邮件由系统自动生成。', + 'mail-send-failed' => '无法发送报价单邮件。', + ], + 'products' => [ + 'product-name' => '产品名称', + 'quantity' => '数量', + 'price' => '价格', + 'amount' => '金额', + 'action' => '操作', + 'add-more' => '继续添加', + 'total' => '合计', + 'empty-title' => '未找到产品', + 'empty-info' => '该线索下未找到产品', + 'add-product' => '添加产品', + ], + 'persons' => [ + 'title' => '联系人信息', + 'job-title' => ':organization 的 :job_title', + 'no-person' => '该线索尚未分配联系人。', + 'attach-btn' => '关联联系人', + 'change-btn' => '更换', + 'change-title' => '更换联系人', + 'save-btn' => '保存', + 'cancel-btn' => '取消', + ], + 'stages' => [ + 'won' => '赢单', + 'lost' => '失单', + 'need-more-info' => '需要更多信息', + 'closed-at' => '关闭时间', + 'won-value' => '成交金额', + 'lost-reason' => '失单原因', + 'save-btn' => '保存', + ], + 'tags' => [ + 'create-success' => '标签创建成功。', + 'destroy-success' => '标签删除成功。', + ], + ], + ], + 'configuration' => [ + 'index' => [ + 'back' => '返回', + 'delete' => '删除', + 'save-btn' => '保存配置', + 'save-success' => '配置保存成功。', + 'search' => '搜索', + 'select-country' => '选择国家', + 'select-state' => '选择省/州', + 'title' => '配置', + 'general' => [ + 'title' => '常规', + 'info' => '常规配置', + 'general' => [ + 'title' => '常规', + 'info' => '在此更新您的常规设置。', + 'locale-settings' => [ + 'title' => '语言设置', + 'title-info' => '定义管理面板界面所使用的语言。下拉列表中会显示您的安装中所有可用的语言。', + ], + 'admin-logo' => [ + 'logo-image' => 'Logo 图片', + 'title' => '管理面板 Logo', + 'title-info' => '为您的管理面板配置 Logo 图片。', + 'favicon-image' => 'Favicon 图片', + 'favicon-image-info' => '为您的管理面板配置 Favicon 图片。', + ], + ], + 'settings' => [ + 'title' => '设置', + 'info' => '在此更新您的设置。', + 'footer' => [ + 'show' => '显示“技术支持”', + 'show-info' => '显示或隐藏管理面板底部的“技术支持”栏。', + 'info' => '我们可以在此配置“技术支持”区域。', + 'powered-by' => '“技术支持”文本编辑器', + 'title' => '“技术支持”区域配置', + ], + 'menu' => [ + 'activities' => '活动', + 'configuration' => '配置', + 'contacts' => '联系人', + 'dashboard' => '仪表盘', + 'draft' => '草稿箱', + 'inbox' => '收件箱', + 'info' => '我们可以在此配置菜单项的名称。', + 'leads' => '线索', + 'mail' => '邮件', + 'organizations' => '组织', + 'outbox' => '发件箱', + 'persons' => '联系人', + 'products' => '产品', + 'quotes' => '报价单', + 'sent' => '已发送', + 'settings' => '设置', + 'title' => '菜单项配置', + 'trash' => '回收站', + ], + 'dashboard' => [ + 'title' => '仪表盘配置', + 'info' => '我们可以在此配置仪表盘的默认日期范围。', + 'date-range' => '默认日期范围', + 'date-range-info' => '仪表盘加载时使用的时间段。仍可在仪表盘上调整日期选择器以进行临时查看。', + 'custom-days' => '自定义范围(天)', + 'custom-days-info' => '当日期范围设为“自定义”时,向前追溯的天数。', + 'ranges' => [ + '1-month' => '1 个月', + '3-months' => '3 个月', + '9-months' => '9 个月', + '1-year' => '1 年', + '2-years' => '2 年', + 'custom' => '自定义', + ], + ], + 'menu-color' => [ + 'brand-color' => '品牌色', + 'info' => '我们可以在此更改菜单项的颜色。', + 'title' => '菜单项颜色配置', + ], + ], + ], + 'email' => [ + 'title' => '邮件设置', + 'info' => '应用的邮件配置。', + 'imap' => [ + 'title' => 'IMAP 设置', + 'info' => '用于接收邮件的 IMAP 配置。', + 'account' => [ + 'title' => 'IMAP 账户', + 'title-info' => '在此配置您的 IMAP 账户设置。', + 'host' => '主机', + 'port' => '端口', + 'encryption' => '加密类型', + 'validate-cert' => '验证 SSL 证书', + 'username' => 'IMAP 用户名', + 'password' => 'IMAP 密码', + ], + ], + ], + 'magic-ai' => [ + 'title' => 'Magic AI', + 'info' => '应用的 Magic AI 配置。', + 'settings' => [ + 'api-key' => 'API 密钥', + 'api-key-info' => '请记得为每个模型使用 OpenRouter API 密钥。这是提升安全性和性能的简单一步。', + 'enable' => '启用', + 'info' => '使用您的 OpenRouter API 密钥来增强 Magic AI 体验。立即接入,开启为您量身定制的流畅个性化 AI 之旅!轻松自定义设置,全面掌控您的 AI 使用体验。', + 'other' => '其他模型', + 'other-model' => '对于其他模型,请使用 OpenRouter 提供的模型 ID。', + 'doc-generation' => 'DOC 生成', + 'doc-generation-info' => '启用 DOC 生成功能,可自动从 DOC 文件中提取数据并转换为文本格式。启用该功能可简化工作流程,提升您的生产力和效率。', + 'title' => '常规设置', + 'models' => [ + 'deepseek-r1' => 'Deepseek R1 Distill-llama-8b', + 'gemini-2-0-flash-001' => 'Gemini 2.0 flash-001', + 'gpt-4o' => 'GPT-4.0', + 'gpt-4o-mini' => 'GPT-4.0 mini', + 'grok-2-1212' => 'Grok 2.12', + 'llama-3-2-3b-instruct' => 'Llama 3.2 3b Instruct', + 'title' => '模型', + ], + ], + ], + ], + ], + 'dashboard' => [ + 'index' => [ + 'title' => '仪表盘', + 'start-date' => '开始日期', + 'end-date' => '结束日期', + 'export-pdf' => '导出 PDF', + 'pdf' => [ + 'title' => '仪表盘报告', + 'filtered-date' => '筛选日期', + 'revenue' => '收入', + 'overview' => '概览', + 'total-leads' => '线索总数', + 'metric' => '指标', + 'current' => '当前', + 'previous' => '上一期', + 'progress' => '进度', + 'date' => '日期', + 'all' => '全部', + 'total' => '合计', + 'won' => '赢单', + 'lost' => '失单', + 'source' => '来源', + 'type' => '类型', + 'stage' => '阶段', + 'product' => '产品', + 'person' => '联系人', + 'price' => '价格', + 'revenue-by-sources' => '按来源统计收入', + 'revenue-by-types' => '按类型统计收入', + 'open-leads-by-stages' => '按阶段统计进行中的线索', + 'top-selling-products' => '热销产品', + 'top-persons' => '重点联系人', + ], + 'revenue' => [ + 'lost-revenue' => '流失收入', + 'won-revenue' => '成交收入', + ], + 'over-all' => [ + 'average-lead-value' => '平均线索价值', + 'total-leads' => '线索总数', + 'average-leads-per-day' => '日均线索数', + 'total-quotations' => '报价单总数', + 'total-persons' => '联系人总数', + 'total-organizations' => '组织总数', + ], + 'total-leads' => [ + 'title' => '线索', + 'total' => '线索总数', + 'won' => '赢单线索', + 'lost' => '失单线索', + ], + 'revenue-by-sources' => [ + 'title' => '按来源统计收入', + 'empty-title' => '暂无数据', + 'empty-info' => '所选时间段内暂无数据', + ], + 'revenue-by-types' => [ + 'title' => '按类型统计收入', + 'empty-title' => '暂无数据', + 'empty-info' => '所选时间段内暂无数据', + ], + 'top-selling-products' => [ + 'title' => '热销产品', + 'empty-title' => '未找到产品', + 'empty-info' => '所选时间段内暂无产品', + ], + 'top-persons' => [ + 'title' => '重点联系人', + 'empty-title' => '未找到联系人', + 'empty-info' => '所选时间段内暂无联系人', + ], + 'open-leads-by-states' => [ + 'title' => '按阶段统计进行中的线索', + 'empty-title' => '暂无数据', + 'empty-info' => '所选时间段内暂无数据', + ], + ], + ], + 'help' => [ + 'index' => [ + 'title' => '帮助与资源', + 'description' => '充分发挥 Krayin 管理面板价值所需的一切——托管服务、技术支持与专业服务,以及扩展和开发者文档。', + 'contact-us' => '联系我们', + 'view-all' => '查看全部', + 'still-need-help-title' => '仍需帮助?', + 'still-need-help-description' => '就托管、定制开发或其他任何问题与 Krayin 团队交流。', + 'services' => [ + 'title' => '服务与资源', + 'cloud-hosting' => [ + 'title' => '云托管', + 'description' => '高性价比的托管式云服务——几分钟内即可在云端试用并上线 Krayin,全面优化且可弹性扩展。', + ], + 'support' => [ + 'title' => '支持与维护', + 'description' => '专属技术支持与持续维护方案,让您的 CRM 始终安全、及时更新并稳定运行。', + ], + 'extensions' => [ + 'title' => '扩展', + 'description' => '浏览官方与社区插件,为 Krayin 增添新的连接器、渠道和功能。', + ], + ], + 'resources' => [ + 'title' => '文档', + 'user-docs' => [ + 'title' => '用户指南', + 'description' => '在模块集成、定制开发、数据迁移、版本升级和定制化开发方面提供专业帮助。', + ], + 'dev-docs' => [ + 'title' => '开发文档与博客', + 'description' => '开发者指南、教程和最新文章,助您构建、配置并保持同步。', + ], + 'api-docs' => [ + 'title' => 'API 文档', + 'description' => '完整的 REST API 参考,包含接口、认证方式与示例,助您将 Krayin 集成到您的技术栈中。', + ], + ], + 'community' => [ + 'forums' => [ + 'title' => '社区论坛', + 'description' => '与成千上万的 Krayin 用户和开发者交流,分享经验并解决问题。', + 'link' => '加入社区', + ], + 'tutorials' => [ + 'title' => '视频教程', + 'description' => '观看分步视频指南,了解如何搭建 CRM、配置销售流程和管理用户。', + 'link' => '浏览 YouTube 频道', + ], + ], + ], + ], + 'layouts' => [ + 'app-version' => '版本::version', + 'dashboard' => '仪表盘', + 'help' => '帮助与资源', + 'leads' => '线索', + 'quotes' => '报价单', + 'quote' => '报价单', + 'mail' => [ + 'title' => '邮件', + 'compose' => '写邮件', + 'inbox' => '收件箱', + 'draft' => '草稿箱', + 'outbox' => '发件箱', + 'sent' => '已发送', + 'trash' => '回收站', + 'setting' => '设置', + ], + 'activities' => '活动', + 'contacts' => '联系人', + 'persons' => '联系人', + 'person' => '联系人', + 'organizations' => '组织', + 'organization' => '组织', + 'products' => '产品', + 'product' => '产品', + 'settings' => '设置', + 'user' => '用户', + 'user-info' => '在 CRM 中管理所有用户及其权限,明确他们可以执行的操作。', + 'groups' => '用户组', + 'groups-info' => '在 CRM 中添加、编辑或删除用户组', + 'roles' => '角色', + 'role' => '角色', + 'roles-info' => '在 CRM 中添加、编辑或删除角色', + 'users' => '用户', + 'users-info' => '在 CRM 中添加、编辑或删除用户', + 'lead' => '线索', + 'lead-info' => '在 CRM 中管理所有与线索相关的设置', + 'sidebar' => [ + 'collapse' => '收起', + 'expand' => '展开', + ], + 'quick-add' => [ + 'title' => '快速添加', + 'save' => '保存', + 'to' => '收件人', + 'subject' => '主题', + 'message' => '正文', + ], + 'pipelines' => '销售流程', + 'pipelines-info' => '在 CRM 中添加、编辑或删除销售流程', + 'sources' => '来源', + 'sources-info' => '在 CRM 中添加、编辑或删除来源', + 'types' => '类型', + 'types-info' => '在 CRM 中添加、编辑或删除类型', + 'automation' => '自动化', + 'automation-info' => '在 CRM 中管理所有与自动化相关的设置', + 'attributes' => '属性', + 'attribute' => '属性', + 'attributes-info' => '在 CRM 中添加、编辑或删除属性', + 'email-templates' => '邮件模板', + 'email' => '邮件', + 'email-templates-info' => '在 CRM 中添加、编辑或删除邮件模板', + 'events' => '事件', + 'events-info' => '在 CRM 中添加、编辑或删除事件', + 'campaigns' => '营销活动', + 'campaigns-info' => '在 CRM 中添加、编辑或删除营销活动', + 'workflows' => '工作流', + 'workflows-info' => '在 CRM 中添加、编辑或删除工作流', + 'webhooks' => 'Webhook', + 'webhooks-info' => '在 CRM 中添加、编辑或删除 Webhook', + 'other-settings' => '其他设置', + 'other-settings-info' => '在 CRM 中管理您的所有附加设置', + 'tags' => '标签', + 'tags-info' => '在 CRM 中添加、编辑或删除标签', + 'my-account' => '我的账户', + 'sign-out' => '退出登录', + 'back' => '返回', + 'name' => '名称', + 'configuration' => '配置', + 'howdy' => '你好!', + 'warehouses' => '仓库', + 'warehouse' => '仓库', + 'warehouses-info' => '在 CRM 中添加、编辑或删除仓库', + 'inventory' => '库存', + 'inventory-info' => '在 CRM 中管理所有与库存相关的设置', + 'data_transfer' => '数据传输', + 'data_transfer_info' => '在 CRM 中管理与联系人、产品和线索数据传输相关的设置', + ], + 'user' => [ + 'account' => [ + 'name' => '姓名', + 'email' => '邮箱', + 'password' => '密码', + 'my_account' => '我的账户', + 'update_details' => '更新信息', + 'current_password' => '当前密码', + 'confirm_password' => '确认密码', + 'password-match' => '当前密码不匹配。', + 'account-save' => '账户更改保存成功。', + 'permission-denied' => '权限不足', + 'remove-image' => '移除图片', + 'upload_image_pix' => '上传头像(100px x 100px)', + 'upload_image_format' => 'PNG 或 JPG 格式', + 'image_upload_message' => '仅允许上传图片(.jpeg、.jpg、.png 等)。', + ], + ], + 'emails' => [ + 'common' => [ + 'dear' => '尊敬的 :name', + 'cheers' => '此致,
    :app_name 团队', + 'user' => [ + 'dear' => '尊敬的 :username', + 'create-subject' => '您已被添加为成员。', + 'create-body' => '恭喜!您现在已成为我们团队的一员。', + 'forget-password' => [ + 'subject' => '客户重置密码', + 'dear' => '尊敬的 :username', + 'reset-password' => '重置密码', + 'info' => '您收到此邮件是因为我们收到了您账户的密码重置请求', + 'final-summary' => '如果您并未申请重置密码,则无需进行任何操作', + 'thanks' => '谢谢!', + ], + ], + ], + ], + 'validations' => [ + 'message' => [ + 'code' => '该字段必须是有效的代码。', + 'decimal' => ':attribute 必须是小数。', + ], + ], + 'errors' => [ + 'dashboard' => '仪表盘', + 'go-back' => '返回', + 'support' => '如果问题仍然存在,请通过 :email 联系我们获取帮助。', + 'unauthorized' => '此操作未获授权。', + 'role-exceeds-own' => 'You can only assign a role whose permissions are the same as or fewer than your own.', + 'user-exceeds-own' => 'You can only manage users whose permissions are the same as or fewer than your own.', + 'own-privileges' => 'You cannot change your own role or data scope.', + 'scope-exceeds-own' => 'You cannot grant a data scope wider than your own.', + 'primary-admin-protected' => 'The primary administrator account is protected and cannot be modified here.', + 'role-permissions-exceed-own' => 'You can only create or edit roles whose permissions are the same as or fewer than your own.', + '404' => [ + 'description' => '哎呀!您要找的页面去度假了。我们似乎找不到您所搜索的内容。', + 'title' => '404 页面未找到', + ], + '401' => [ + 'description' => '哎呀!看来您无权访问此页面。您似乎缺少必要的凭据。', + 'title' => '401 未授权', + ], + '403' => [ + 'description' => '哎呀!此页面禁止访问。您似乎没有查看该内容所需的权限。', + 'title' => '403 禁止访问', + ], + '500' => [ + 'description' => '哎呀!出了点问题。我们在加载您所查找的页面时遇到了故障。', + 'title' => '500 服务器内部错误', + ], + '503' => [ + 'description' => '哎呀!我们似乎正在临时维护中。请稍后再试。', + 'title' => '503 服务不可用', + ], + ], + 'export' => [ + 'csv' => 'CSV', + 'download' => '下载', + 'export' => '导出', + 'google-contacts' => 'Google 通讯录', + 'google-contacts-in-progress' => '正在将您的联系人导出到 Google……这可能需要一些时间。', + 'google-contacts-total' => '联系人总数:', + 'google-contacts-exported' => '已导出:', + 'google-contacts-duplicate' => '已存在:', + 'google-contacts-failed' => '失败:', + 'google-contacts-summary' => '导出完成:已导出 :exported 个,已存在 :duplicate 个,失败 :failed 个。', + 'no-records' => '没有可导出的内容', + 'xls' => 'XLS', + 'xlsx' => 'XLSX', + ], +]; diff --git a/packages/Webkul/Admin/src/Resources/views/activities/datagrid/is-done.blade.php b/packages/Webkul/Admin/src/Resources/views/activities/datagrid/is-done.blade.php new file mode 100644 index 0000000..3a3ea88 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/activities/datagrid/is-done.blade.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/activities/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/activities/edit.blade.php new file mode 100644 index 0000000..d2bfc7a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/activities/edit.blade.php @@ -0,0 +1,463 @@ + + + + @lang('admin::app.activities.edit.title') + + + {!! view_render_event('admin.activities.edit.form.before') !!} + + +
    +
    +
    + + + + +
    + @lang('admin::app.activities.edit.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.activities.edit.save_button.before') !!} + + + + + {!! view_render_event('admin.activities.edit.save_button.after') !!} +
    +
    +
    + + +
    + +
    + {!! view_render_event('admin.activities.edit.form_controls.before') !!} + + + +
    +
    + + @lang('admin::app.activities.edit.schedule_from') + + + + + +
    + +
    + + @lang('admin::app.activities.edit.schedule_to') + + + + + +
    +
    +
    + + + + + @lang('admin::app.activities.edit.comment') + + + + + + + + + + + @lang('admin::app.activities.edit.participants') + + + + +
    +
      +
    • + +
    • +
    + + +
    +
    +
    + + + + + @lang('admin::app.activities.edit.lead') + + + + + + + + + + + {!! view_render_event('admin.activities.edit.form_controls.after') !!} +
    + + +
    + {!! view_render_event('admin.activities.edit.accordion.general.before') !!} + + + +
    +

    + @lang('admin::app.activities.edit.general') +

    +
    + + + + + + + @lang('admin::app.activities.edit.title') + + + + + + + + + + + @lang('admin::app.activities.edit.type') + + + + + + + + + + + + + + + + + @lang('admin::app.activities.edit.location') + + + + + + + +
    + + {!! view_render_event('admin.activities.edit.accordion.general.after') !!} +
    +
    +
    +
    + + {!! view_render_event('admin.activities.edit.form.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/activities/index.blade.php b/packages/Webkul/Admin/src/Resources/views/activities/index.blade.php new file mode 100644 index 0000000..c192c40 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/activities/index.blade.php @@ -0,0 +1,1161 @@ + + + {{ menu()->getLabel('activities', 'admin::app.activities.index.title') }} + + + {!! view_render_event('admin.activities.index.activities.before') !!} + + + +
    +
    +
    + + +
    + {{ menu()->getLabel('activities', 'admin::app.activities.index.title') }} +
    +
    + +
    + + + +
    +
    + + + @if ( + request()->get('view-type') == 'table' + || ! request()->has('view-type') + ) + + @endif +
    +
    + + {!! view_render_event('admin.activities.index.activities.after') !!} + + @pushOnce('scripts') + + + + + + + + + + @endPushOnce + + @pushOnce('styles') + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/accordion/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/accordion/index.blade.php new file mode 100644 index 0000000..f369769 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/accordion/index.blade.php @@ -0,0 +1,84 @@ +@props([ + 'isActive' => true, +]) + +
    merge(['class' => 'box-shadow rounded-lg border border-gray-300 dark:border-gray-800 bg-white dark:bg-gray-900']) }}> + + + + @isset($header) + + @endisset + + @isset($content) + + @endisset + +
    + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/activities/actions/activity.blade.php b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/activity.blade.php new file mode 100644 index 0000000..bcb7ace --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/activity.blade.php @@ -0,0 +1,285 @@ +@props([ + 'entity' => null, + 'entityControlName' => null, +]) + + +
    + {!! view_render_event('admin.components.activities.actions.activity.create_btn.before') !!} + + + + {!! view_render_event('admin.components.activities.actions.activity.create_btn.after') !!} + + {!! view_render_event('admin.components.activities.actions.activity.before') !!} + + + + + {!! view_render_event('admin.components.activities.actions.activity.after') !!} +
    + + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/activities/actions/activity/participants.blade.php b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/activity/participants.blade.php new file mode 100644 index 0000000..36eea2b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/activity/participants.blade.php @@ -0,0 +1,245 @@ +{!! view_render_event('admin.components.activities.actions.activity.participants.before') !!} + + + + +{!! view_render_event('admin.components.activities.actions.activity.participants.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/activities/actions/file.blade.php b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/file.blade.php new file mode 100644 index 0000000..c2f91a0 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/file.blade.php @@ -0,0 +1,216 @@ +@props([ + 'entity' => null, + 'entityControlName' => null, +]) + + +
    + {!! view_render_event('admin.components.activities.actions.file.create_btn.before') !!} + + + + {!! view_render_event('admin.components.activities.actions.file.create_btn.after') !!} + + {!! view_render_event('admin.components.activities.actions.file.before') !!} + + + + + {!! view_render_event('admin.components.activities.actions.file.after') !!} +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/activities/actions/mail.blade.php b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/mail.blade.php new file mode 100644 index 0000000..8f61f65 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/mail.blade.php @@ -0,0 +1,300 @@ +@props([ + 'entity' => null, + 'entityControlName' => null, + 'emails' => [], +]) + + +
    + {!! view_render_event('admin.components.activities.actions.mail.create_btn.before') !!} + + + + {!! view_render_event('admin.components.activities.actions.mail.create_btn.after') !!} + + {!! view_render_event('admin.components.activities.actions.mail.before') !!} + + + + + {!! view_render_event('admin.components.activities.actions.mail.after') !!} +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/activities/actions/note.blade.php b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/note.blade.php new file mode 100644 index 0000000..18f9d6a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/activities/actions/note.blade.php @@ -0,0 +1,175 @@ +@props([ + 'entity' => null, + 'entityControlName' => null, +]) + + +
    + {!! view_render_event('admin.components.activities.actions.note.create_btn.before') !!} + + + + {!! view_render_event('admin.components.activities.actions.note.create_btn.after') !!} + + {!! view_render_event('admin.components.activities.actions.note.before') !!} + + + + + {!! view_render_event('admin.components.activities.actions.note.after') !!} +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/activities/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/activities/index.blade.php new file mode 100644 index 0000000..ca9e067 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/activities/index.blade.php @@ -0,0 +1,741 @@ +@props([ + 'endpoint', + 'emailDetachEndpoint' => null, + 'activeType' => 'all', + 'types' => null, + 'extraTypes' => null, +]) + +{!! view_render_event('admin.components.activities.before') !!} + + + + + + + @foreach ($extraTypes ?? [] as $type) + + @endforeach + + +{!! view_render_event('admin.components.activities.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/attachments/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attachments/index.blade.php new file mode 100644 index 0000000..7b1052b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attachments/index.blade.php @@ -0,0 +1,184 @@ +@props([ + 'name' => 'attachments', + 'validations' => null, + 'uploadedAttachments' => [], + 'allowMultiple' => false, + 'hideButton' => false, +]) + + + + +@pushOnce('scripts') + + + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/address.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/address.blade.php new file mode 100644 index 0000000..ac1d4e4 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/address.blade.php @@ -0,0 +1,165 @@ +@if (isset($attribute)) + + + + +@endif + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/boolean.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/boolean.blade.php new file mode 100755 index 0000000..d8e354d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/boolean.blade.php @@ -0,0 +1,20 @@ +code) ?: $value ?> + + + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/checkbox.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/checkbox.blade.php new file mode 100644 index 0000000..c83a2a8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/checkbox.blade.php @@ -0,0 +1,32 @@ +@php + $options = $attribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type) + : $attribute->options()->orderBy('sort_order')->get(); + + $selectedOption = old($attribute->code, $value); + + $selectedOption = is_array($selectedOption) ? $selectedOption : explode(',', $selectedOption); +@endphp + + + +@foreach ($options as $option) + + + + + +@endforeach diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/date.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/date.blade.php new file mode 100755 index 0000000..62d9699 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/date.blade.php @@ -0,0 +1,19 @@ +@php + if (! empty($value)) { + if ($value instanceof \Carbon\Carbon) { + $value = $value->format('Y-m-d'); + } elseif (is_string($value)) { + $value = \Carbon\Carbon::parse($value)->format('Y-m-d'); + } + } + $value = old($attribute->code, $value); +@endphp + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/datetime.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/datetime.blade.php new file mode 100755 index 0000000..e04e80b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/datetime.blade.php @@ -0,0 +1,8 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/email.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/email.blade.php new file mode 100644 index 0000000..3d359b3 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/email.blade.php @@ -0,0 +1,179 @@ +@if (isset($attribute)) + +
    + + +
    + +
    +
    + + + + + @lang("admin::app.common.custom-attributes.add-more") + +
    +@endif + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/file.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/file.blade.php new file mode 100644 index 0000000..116ab0c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/file.blade.php @@ -0,0 +1,39 @@ +
    + @if ($value) + +
    + +
    +
    + @endif + + +
    + +@if ($value) +
    + + + +
    +@endif \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/image.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/image.blade.php new file mode 100644 index 0000000..dbfeba6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/image.blade.php @@ -0,0 +1,42 @@ +
    + @if ($value) + + {{ $attribute->code }} + + @endif + + +
    + +@if ($value) +
    + + + +
    +@endif \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/index.blade.php new file mode 100644 index 0000000..c2b40f0 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/index.blade.php @@ -0,0 +1,144 @@ +@props([ + 'attribute' => '', + 'value' => '', + 'validations' => '', + 'canAddNew' => true, +]) + +@switch($attribute->type) + @case('text') + + + @break + + @case('email') + + + @break + + @case('phone') + + + @break + + @case('lookup') + + + @break + + @case('select') + + + @break + + @case('multiselect') + + + @break + + @case('price') + + + @break + + @case('image') + + + @break + + @case('file') + + + @break + + @case('textarea') + + + @break + + @case('address') + + + @break + + @case('date') + + + @break + + @case('datetime') + + + @break + + @case('boolean') + + + @break + + @case('checkbox') + + + @break +@endswitch \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/lookup.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/lookup.blade.php new file mode 100644 index 0000000..4a9cf97 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/lookup.blade.php @@ -0,0 +1,314 @@ +@if (isset($attribute)) + @php + $lookUpEntityData = app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpEntity($attribute->lookup_type, old($attribute->code) ?: $value); + @endphp + + +
    + +
    + @lang('admin::app.components.attributes.lookup.click-to-add') + + +
    + + +
    +
    +
    +
    +@endif + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/multiselect.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/multiselect.blade.php new file mode 100755 index 0000000..506a1c4 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/multiselect.blade.php @@ -0,0 +1,32 @@ +@php + $options = $attribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type) + : $attribute->options()->orderBy('sort_order')->get(); + + $selectedOption = old($attribute->code) ?: $value; +@endphp + + + + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/phone.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/phone.blade.php new file mode 100644 index 0000000..7cd2514 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/phone.blade.php @@ -0,0 +1,186 @@ +@if (isset($attribute)) + +
    + + +
    + +
    +
    + + + + + @lang("admin::app.common.custom-attributes.add-more") + +
    +@endif + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/price.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/price.blade.php new file mode 100755 index 0000000..c0b2bb9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/price.blade.php @@ -0,0 +1,32 @@ +@if (isset($attribute)) + + +@endif + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/select.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/select.blade.php new file mode 100755 index 0000000..e7771e5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/select.blade.php @@ -0,0 +1,21 @@ +@php + $options = $attribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type) + : $attribute->options()->orderBy('sort_order')->get(); +@endphp + + + @foreach ($options as $option) + + @endforeach + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/text.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/text.blade.php new file mode 100755 index 0000000..59eafb9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/text.blade.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/textarea.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/textarea.blade.php new file mode 100755 index 0000000..3bf326b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/edit/textarea.blade.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/index.blade.php new file mode 100644 index 0000000..4435028 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/index.blade.php @@ -0,0 +1,53 @@ +@props([ + 'customAttributes' => [], + 'entity' => null, + 'canAddNew' => true, +]) + +@foreach ($customAttributes as $attribute) + @php + $validations = []; + + if ($attribute->is_required) { + $validations[] = 'required'; + } + + if ($attribute->type == 'price') { + $validations[] = 'decimal'; + } + + $validations[] = $attribute->validation; + + $validations = implode('|', array_filter($validations)); + + $key = 'installer::app.seeders.attributes.'.$attribute->entity_type.'.'.str_replace('_', '-', $attribute->code); + $label = trans($key); + if ($label === $key) { + $label = $attribute->name; + } + @endphp + + + + {{ $label }} + + @if ($attribute->type == 'price') + ({{ core()->currencySymbol(config('app.currency')) }}) + @endif + + + @if (isset($attribute)) + + @endif + + + +@endforeach \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view.blade.php new file mode 100644 index 0000000..ded65d7 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view.blade.php @@ -0,0 +1,25 @@ +@props([ + 'customAttributes' => [], + 'entity' => null, + 'allowEdit' => false, + 'url' => null, +]) + +
    + @foreach ($customAttributes as $attribute) + @if (view()->exists($typeView = 'admin::components.attributes.view.' . $attribute->type)) +
    +
    {{ $attribute->name }}
    + +
    + @include ($typeView, [ + 'attribute' => $attribute, + 'value' => isset($entity) ? $entity[$attribute->code] : null, + 'allowEdit' => $allowEdit, + 'url' => $url, + ]) +
    +
    + @endif + @endforeach +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/address.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/address.blade.php new file mode 100644 index 0000000..610e260 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/address.blade.php @@ -0,0 +1,11 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/boolean.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/boolean.blade.php new file mode 100755 index 0000000..8a8ec54 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/boolean.blade.php @@ -0,0 +1,11 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/checkbox.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/checkbox.blade.php new file mode 100644 index 0000000..079494e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/checkbox.blade.php @@ -0,0 +1,20 @@ +@php + $options = $attribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type) + : $attribute->options()->orderBy('sort_order')->get(); + + $selectedOption = old($attribute->code) ?: $value; +@endphp + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/date.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/date.blade.php new file mode 100755 index 0000000..c2d0851 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/date.blade.php @@ -0,0 +1,21 @@ +@php + if (! empty($value)) { + if ($value instanceof \Carbon\Carbon) { + $value = $value->format('Y-m-d'); + } elseif (is_string($value)) { + $value = \Carbon\Carbon::parse($value)->format('Y-m-d'); + } + } +@endphp + + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/datetime.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/datetime.blade.php new file mode 100755 index 0000000..e1c030a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/datetime.blade.php @@ -0,0 +1,11 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/email.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/email.blade.php new file mode 100644 index 0000000..b745f43 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/email.blade.php @@ -0,0 +1,11 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/file.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/file.blade.php new file mode 100644 index 0000000..fcda0c3 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/file.blade.php @@ -0,0 +1,11 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/image.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/image.blade.php new file mode 100644 index 0000000..47e3c3e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/image.blade.php @@ -0,0 +1,11 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/lookup.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/lookup.blade.php new file mode 100644 index 0000000..9fa70cb --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/lookup.blade.php @@ -0,0 +1,16 @@ +@php + $lookUpEntity = app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpEntity($attribute->lookup_type, $value); +@endphp + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/multiselect.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/multiselect.blade.php new file mode 100755 index 0000000..079494e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/multiselect.blade.php @@ -0,0 +1,20 @@ +@php + $options = $attribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type) + : $attribute->options()->orderBy('sort_order')->get(); + + $selectedOption = old($attribute->code) ?: $value; +@endphp + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/phone.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/phone.blade.php new file mode 100644 index 0000000..fd6d544 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/phone.blade.php @@ -0,0 +1,11 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/price.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/price.blade.php new file mode 100755 index 0000000..7f8237f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/price.blade.php @@ -0,0 +1,14 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/select.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/select.blade.php new file mode 100755 index 0000000..5fc0e38 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/select.blade.php @@ -0,0 +1,18 @@ +@php + $options = $attribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type) + : $attribute->options()->orderBy('sort_order')->get(); +@endphp + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/text.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/text.blade.php new file mode 100755 index 0000000..1be1e8b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/text.blade.php @@ -0,0 +1,13 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/attributes/view/textarea.blade.php b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/textarea.blade.php new file mode 100755 index 0000000..b66f5b8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/attributes/view/textarea.blade.php @@ -0,0 +1,12 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/avatar/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/avatar/index.blade.php new file mode 100644 index 0000000..fbe7182 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/avatar/index.blade.php @@ -0,0 +1,69 @@ + +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/breadcrumbs/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/breadcrumbs/index.blade.php new file mode 100644 index 0000000..be6595c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/breadcrumbs/index.blade.php @@ -0,0 +1,15 @@ +@props([ + 'name' => '', + 'entity' => null, + 'route' => null, +]) + +
    +
    + @if($route) + {{ Breadcrumbs::view('admin::partials.breadcrumbs', $name, $route, $entity) }} + @else + {{ Breadcrumbs::view('admin::partials.breadcrumbs', $name, $entity) }} + @endif +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/button/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/button/index.blade.php new file mode 100644 index 0000000..e4bc772 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/button/index.blade.php @@ -0,0 +1,40 @@ + + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/charts/bar.blade.php b/packages/Webkul/Admin/src/Resources/views/components/charts/bar.blade.php new file mode 100644 index 0000000..3dc549f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/charts/bar.blade.php @@ -0,0 +1,100 @@ + + +@pushOnce('scripts') + + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/charts/doughnut.blade.php b/packages/Webkul/Admin/src/Resources/views/components/charts/doughnut.blade.php new file mode 100644 index 0000000..33e4bab --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/charts/doughnut.blade.php @@ -0,0 +1,68 @@ + + +@pushOnce('scripts') + + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/charts/line.blade.php b/packages/Webkul/Admin/src/Resources/views/components/charts/line.blade.php new file mode 100644 index 0000000..928a815 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/charts/line.blade.php @@ -0,0 +1,97 @@ + + +@pushOnce('scripts') + + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/column-settings.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/column-settings.blade.php new file mode 100644 index 0000000..5c0bc8a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/column-settings.blade.php @@ -0,0 +1,93 @@ +{!! view_render_event('admin.components.datagrid.column_settings.before') !!} + + + + +{!! view_render_event('admin.components.datagrid.column_settings.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/export/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/export/index.blade.php new file mode 100644 index 0000000..537af3e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/export/index.blade.php @@ -0,0 +1,308 @@ + +
    + + + @lang('admin::app.export.export') +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce + +@pushOnce('styles') + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/export/temp.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/export/temp.blade.php new file mode 100755 index 0000000..257cf65 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/export/temp.blade.php @@ -0,0 +1,23 @@ + + + + @foreach ($columns as $column) + + @endforeach + + + + + @foreach ($records as $record) + + @foreach($columns as $column) + @if ($closure = $column->getClosure()) + + @else + + @endif + @endforeach + + @endforeach + +
    {{ $column->getLabel() }}
    {!! $closure($record) !!}{{ $record->{$column->getIndex()} }}
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/index.blade.php new file mode 100644 index 0000000..445bf70 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/index.blade.php @@ -0,0 +1,635 @@ +@props([ + 'isMultiRow' => false, + 'toolbarLeftBefore' => null, + 'toolbarLeftAfter' => null, + 'toolbarRightBefore' => null, + 'toolbarRightAfter' => null, +]) + + + {{ $slot }} + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/table.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/table.blade.php new file mode 100644 index 0000000..381d463 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/table.blade.php @@ -0,0 +1,377 @@ +@props(['isMultiRow' => false]) + + + {{ $slot }} + + +@pushOnce('scripts') + + + +@endpushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar.blade.php new file mode 100644 index 0000000..5a4e708 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar.blade.php @@ -0,0 +1,94 @@ + + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/filter.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/filter.blade.php new file mode 100644 index 0000000..3fa328a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/filter.blade.php @@ -0,0 +1,1530 @@ + + {{ $slot }} + + +@pushOnce('scripts') + + + + + + + +@endpushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/mass-action.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/mass-action.blade.php new file mode 100644 index 0000000..c47c44f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/mass-action.blade.php @@ -0,0 +1,220 @@ + + {{ $slot }} + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/pagination.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/pagination.blade.php new file mode 100644 index 0000000..3bb4e6d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/pagination.blade.php @@ -0,0 +1,157 @@ + + {{ $slot }} + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/search.blade.php b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/search.blade.php new file mode 100644 index 0000000..b3a3f6b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/datagrid/toolbar/search.blade.php @@ -0,0 +1,157 @@ + + {{ $slot }} + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/drawer/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/drawer/index.blade.php new file mode 100644 index 0000000..e96ac84 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/drawer/index.blade.php @@ -0,0 +1,201 @@ +@props([ + 'isActive' => false, + 'position' => 'right', + 'width' => '500px', +]) + + + @isset($toggle) + + @endisset + + @isset($header) + + @endisset + + @isset($content) + + @endisset + + @isset($footer) + + @endisset + + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/dropdown/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/dropdown/index.blade.php new file mode 100644 index 0000000..4faf14c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/dropdown/index.blade.php @@ -0,0 +1,170 @@ +@props(['position' => 'bottom-left']) + +merge(['class' => 'relative']) }}> + @isset($toggle) + {{ $toggle }} + + + @endisset + + @isset($content) + + @endisset + + @isset($menu) + + @endisset + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/dropdown/menu/item.blade.php b/packages/Webkul/Admin/src/Resources/views/components/dropdown/menu/item.blade.php new file mode 100644 index 0000000..513b575 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/dropdown/menu/item.blade.php @@ -0,0 +1,3 @@ +
  1. merge(['class' => 'cursor-pointer px-5 py-2 text-sm text-gray-800 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-950 ']) }}> + {{ $slot }} +
  2. diff --git a/packages/Webkul/Admin/src/Resources/views/components/example.blade.php b/packages/Webkul/Admin/src/Resources/views/components/example.blade.php new file mode 100644 index 0000000..374ccde --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/example.blade.php @@ -0,0 +1,12 @@ + +@php + $bgColors = ['bg-orange-100', 'bg-red-100', 'bg-green-100', 'bg-blue-100', 'bg-purple-100']; + $textColors = ['text-orange-800', 'text-red-800', 'text-green-800', 'text-blue-800', 'text-purple-800']; +@endphp +@foreach ($bgColors as $bgColor) +
    +@endforeach + +@foreach ($textColors as $textColor) +
    +@endforeach \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/flash-group/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/flash-group/index.blade.php new file mode 100644 index 0000000..f62a1d8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/flash-group/index.blade.php @@ -0,0 +1,64 @@ + + +@pushOnce('scripts') + + + +@endpushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/flash-group/item.blade.php b/packages/Webkul/Admin/src/Resources/views/components/flash-group/item.blade.php new file mode 100644 index 0000000..81ccaa0 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/flash-group/item.blade.php @@ -0,0 +1,205 @@ + + + +@pushOnce('scripts') + + + +@endpushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/flat-picker/date.blade.php b/packages/Webkul/Admin/src/Resources/views/components/flat-picker/date.blade.php new file mode 100644 index 0000000..f5b3954 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/flat-picker/date.blade.php @@ -0,0 +1,81 @@ + + {{ $slot }} + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/flat-picker/datetime.blade.php b/packages/Webkul/Admin/src/Resources/views/components/flat-picker/datetime.blade.php new file mode 100644 index 0000000..7c60790 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/flat-picker/datetime.blade.php @@ -0,0 +1,83 @@ + + {{ $slot }} + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/control.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/control.blade.php new file mode 100644 index 0000000..48fb964 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/control.blade.php @@ -0,0 +1,361 @@ +@props([ + 'type' => 'text', + 'name' => '', +]) + +@switch($type) + @case('hidden') + @case('text') + @case('email') + @case('password') + @case('number') + only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }} + name="{{ $name }}" + > + except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-300 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }} + /> + + + @break + + @case('price') + only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }} + name="{{ $name }}" + > +
    + @if (isset($currency)) + attributes->merge(['class' => 'py-2.5 text-gray-500 ltr:pl-4 rtl:pr-4']) }}> + {{ $currency }} + + @else + + {{ config('app.currency') }} + + @endif + + except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full p-2.5 text-sm text-gray-600 dark:bg-gray-900 dark:text-gray-300']) }} + /> +
    +
    + + @break + + @case('file') + only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }} + name="{{ $name }}" + > + except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full dark:file:bg-gray-800 dark:file:dark:text-white rounded border border-gray-300 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }} + @change="handleChange" + @blur="handleBlur" + /> + + + @break + + @case('color') + except('class') }} + > + except(['value'])->merge(['class' => 'w-full appearance-none rounded-md border text-sm text-gray-600 transition-all hover:border-gray-400 dark:text-gray-300 dark:hover:border-gray-400']) }} + > + + @break + + @case('textarea') + only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }} + name="{{ $name }}" + > + @php + $defaultAttributes = [ + 'class' => 'w-full rounded border border-gray-300 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400' + ]; + + if ($attributes->get('tinymce', false) || $attributes->get(':tinymce', false)) { + $defaultAttributes['id'] = $attributes->get(':id', 'id'); + } + @endphp + + + + @if ($attributes->get('tinymce', false) || $attributes->get(':tinymce', false)) + + @endif + + + @break + + @case('date') + only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['rules' => 'regex:^\d{4}-\d{2}-\d{2}$']) }} + name="{{ $name }}" + > + + except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-300 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }} + autocomplete="off" + /> + + + + @break + + @case('datetime') + only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['rules' => 'regex:^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$']) }} + name="{{ $name }}" + > + + except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-300 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }} + autocomplete="off" + > + + + @break + + @case('select') + only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }} + name="{{ $name }}" + > + + + + @break + + @case('multiselect') + except([])->merge(['class' => 'flex w-full flex-col rounded-md border bg-white px-3 py-2.5 text-sm font-normal text-gray-600 transition-all hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400']) }} + name="{{ $name }}" + multiple + > + {{ $slot }} + + + @break + + @case('checkbox') + + + + + @break + + @case('radio') + + + + + @break + + @case('switch') + + + @break + + @case('image') + + + @break + + @case('inline') + + + @break + + @case('custom') + + {{ $slot }} + + + @break + + @case('tags') + + @break +@endswitch + +@pushOnce('scripts') + + + +@endpushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/address.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/address.blade.php new file mode 100644 index 0000000..ba4e2e4 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/address.blade.php @@ -0,0 +1,370 @@ +@props([ + 'allowEdit' => true, +]) + +except('value') }} + :value='@json($attributes->get('value'))' + :allow-edit="{{ $allowEdit ? 'true' : 'false' }}" +> +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/boolean.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/boolean.blade.php new file mode 100644 index 0000000..6f5a8b2 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/boolean.blade.php @@ -0,0 +1,250 @@ +@props([ + 'allowEdit' => true, +]) + +except('options') }} + :allow-edit="{{ $allowEdit ? 'true' : 'false' }}" +> +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/date.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/date.blade.php new file mode 100644 index 0000000..f5f3bd9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/date.blade.php @@ -0,0 +1,245 @@ +@props([ + 'allowEdit' => true, +]) + + +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/datetime.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/datetime.blade.php new file mode 100644 index 0000000..8488c72 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/datetime.blade.php @@ -0,0 +1,245 @@ +@props([ + 'allowEdit' => true, +]) + + +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/email.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/email.blade.php new file mode 100644 index 0000000..507c375 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/email.blade.php @@ -0,0 +1,320 @@ +@props([ + 'allowEdit' => true, +]) + +except('value') }} + :value={{ json_encode($attributes->get('value')) }} + :allow-edit="{{ $allowEdit ? 'true' : 'false' }}" +> +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/file.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/file.blade.php new file mode 100644 index 0000000..0856a1a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/file.blade.php @@ -0,0 +1,245 @@ +@props([ + 'allowEdit' => true, +]) + + +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/image.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/image.blade.php new file mode 100644 index 0000000..7386e18 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/image.blade.php @@ -0,0 +1,242 @@ +@props([ + 'allowEdit' => true, +]) + + +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/lookup.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/lookup.blade.php new file mode 100644 index 0000000..317cde5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/lookup.blade.php @@ -0,0 +1,448 @@ +@props([ + 'allowEdit' => true, + 'attribute' => [], +]) + + +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/multiselect.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/multiselect.blade.php new file mode 100644 index 0000000..9b4fc55 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/multiselect.blade.php @@ -0,0 +1,367 @@ +@props([ + 'allowEdit' => true, + 'data' => [], +]) + +except('data') }} + :data="{{ json_encode($data) }}" + :allow-edit="{{ $allowEdit ? 'true' : 'false' }}" +> +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/phone.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/phone.blade.php new file mode 100644 index 0000000..58af08c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/phone.blade.php @@ -0,0 +1,321 @@ +@props([ + 'allowEdit' => true, +]) + +except('value') }} + :value={{ json_encode($attributes->get('value')) }} + :allow-edit="{{ $allowEdit ? 'true' : 'false' }}" +> +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/select.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/select.blade.php new file mode 100644 index 0000000..307c471 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/select.blade.php @@ -0,0 +1,270 @@ +@props([ + 'allowEdit' => true, + 'options' => [], +]) + +except('options') }} + :options="{{ json_encode($options) }}" + :allow-edit="{{ $allowEdit ? 'true' : 'false' }}" +> +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/text.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/text.blade.php new file mode 100644 index 0000000..68a8bb7 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/inline/text.blade.php @@ -0,0 +1,300 @@ +@props([ + 'allowEdit' => true, +]) + + +
    +
    +
    +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/tags.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/tags.blade.php new file mode 100644 index 0000000..78453cb --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/controls/tags.blade.php @@ -0,0 +1,183 @@ + + +@pushOnce('scripts') + + + +@endpushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/error.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/error.blade.php new file mode 100644 index 0000000..e8883e9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/error.blade.php @@ -0,0 +1,16 @@ +@props([ + 'name' => null, + 'controlName' => '', +]) + + +

    merge(['class' => 'mt-1 text-xs italic text-red-600']) }} + v-text="message" + > +

    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/index.blade.php new file mode 100644 index 0000000..e391d10 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/index.blade.php @@ -0,0 +1,3 @@ +
    merge(['class' => 'mb-4']) }}> + {{ $slot }} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/control-group/label.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/label.blade.php new file mode 100644 index 0000000..8f7b393 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/control-group/label.blade.php @@ -0,0 +1,3 @@ + diff --git a/packages/Webkul/Admin/src/Resources/views/components/form/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/form/index.blade.php new file mode 100644 index 0000000..0faa931 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/form/index.blade.php @@ -0,0 +1,40 @@ + +@if ($attributes->has('as')) + + {{ $slot }} + + + +@else + @props([ + 'method' => 'POST', + ]) + + @php + $method = strtoupper($method); + @endphp + + + @unless(in_array($method, ['HEAD', 'GET', 'OPTIONS'])) + @csrf + @endunless + + @if (! in_array($method, ['GET', 'POST'])) + @method($method) + @endif + + {{ $slot }} + +@endif \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/anonymous.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/anonymous.blade.php new file mode 100644 index 0000000..c1e7e2c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/anonymous.blade.php @@ -0,0 +1,124 @@ + + + + + + {{ $title ?? '' }} + + + + + + + + + + + @stack('meta') + + {{ + vite()->set(['src/Resources/assets/css/app.css', 'src/Resources/assets/js/app.js']) + }} + + + + + + @if ($favicon = core()->getConfigData('general.design.admin_logo.favicon')) + + @else + + @endif + + @php + $brandColor = core()->getConfigData('general.settings.menu_color.brand_color') ?? '#0E90D9'; + @endphp + + @stack('styles') + + + + {!! view_render_event('admin.layout.head') !!} + + + + {!! view_render_event('admin.layout.body.before') !!} + +
    + + + + {!! view_render_event('admin.layout.content.before') !!} + + + {{ $slot }} + + {!! view_render_event('admin.layout.content.after') !!} +
    + + {!! view_render_event('admin.layout.body.after') !!} + + @stack('scripts') + + {!! view_render_event('admin.layout.vue-app-mount.before') !!} + + + + {!! view_render_event('admin.layout.vue-app-mount.after') !!} + + + + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/header/desktop/mega-search.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/header/desktop/mega-search.blade.php new file mode 100644 index 0000000..9efee61 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/header/desktop/mega-search.blade.php @@ -0,0 +1,567 @@ + +
    + + + +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/header/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/header/index.blade.php new file mode 100644 index 0000000..f56484d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/header/index.blade.php @@ -0,0 +1,201 @@ +
    + + + +
    + + @include('admin::components.layouts.header.desktop.mega-search') + + + @include('admin::components.layouts.header.quick-creation') +
    + +
    +
    + + @include('admin::components.layouts.header.mobile.mega-search') +
    + + + +
    + +
    +
    + +
    + + @include('admin::components.layouts.header.quick-creation') +
    + + + + + @php($user = auth()->guard('user')->user()) + + @if ($user->image) + + @else + + @endif + + + + +
    + @if ($logo = core()->getConfigData('general.general.admin_logo.logo_image')) + {{ config('app.name') }} + @else + {{ config('app.name') }} + @endif + + +

    + @lang('admin::app.layouts.app-version', ['version' => core()->version()]) +

    +
    + + + +
    +
    +
    + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/header/mobile/mega-search.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/header/mobile/mega-search.blade.php new file mode 100644 index 0000000..a181e8b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/header/mobile/mega-search.blade.php @@ -0,0 +1,593 @@ + + + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/header/quick-creation.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/header/quick-creation.blade.php new file mode 100644 index 0000000..a330cbe --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/header/quick-creation.blade.php @@ -0,0 +1,443 @@ +@php + $canCreateLead = bouncer()->hasPermission('leads.create.quick-create'); + $canCreateMail = bouncer()->hasPermission('mail.compose.quick-create'); + $canCreatePerson = bouncer()->hasPermission('contacts.persons.create.quick-create'); + $canCreateOrganization = bouncer()->hasPermission('contacts.organizations.create.quick-create'); + $canCreateProduct = bouncer()->hasPermission('products.create.quick-create'); + + $hasAnyQuickAddPermission = $canCreateLead + || $canCreateMail + || $canCreatePerson + || $canCreateOrganization + || $canCreateProduct; + + $defaultQuickAddTab = match (true) { + $canCreateLead => 'lead', + $canCreatePerson => 'person', + $canCreateOrganization => 'organization', + $canCreateProduct => 'product', + $canCreateMail => 'mail', + default => '', + }; +@endphp + +
    + @if ($hasAnyQuickAddPermission) + + @endif + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/index.blade.php new file mode 100644 index 0000000..493d101 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/index.blade.php @@ -0,0 +1,169 @@ + + + + + + + {!! view_render_event('admin.layout.head.before') !!} + + {{ $title }} + + + + + + + + + + + @stack('meta') + + {{ + vite()->set(['src/Resources/assets/css/app.css', 'src/Resources/assets/js/app.js']) + }} + + + + + + @if ($favicon = core()->getConfigData('general.general.admin_logo.favicon_image')) + + @else + + @endif + + @php + $brandColor = core()->getConfigData('general.settings.menu_color.brand_color') ?? '#0E90D9'; + @endphp + + @stack('styles') + + + + {!! view_render_event('admin.layout.head.after') !!} + + + + {!! view_render_event('admin.layout.body.before') !!} + +
    + + + + + + + {!! view_render_event('admin.layout.content.before') !!} + + + + +
    + + + + @php + /** + * The bar is fixed, so the content below reserves space for it. + * When it is switched off that reserve goes away too, otherwise + * every page keeps a strip of dead space at the bottom. + */ + $showPoweredBy = (bool) core()->getConfigData('general.settings.footer.show'); + @endphp + +
    + +
    + {{ $slot }} +
    + + @if ($showPoweredBy) + +
    +
    +

    {!! core()->sanitizeHtml(core()->getConfigData('general.settings.footer.label')) !!}

    +
    +
    + @endif +
    +
    + + {!! view_render_event('admin.layout.content.after') !!} +
    + + {!! view_render_event('admin.layout.body.after') !!} + + @stack('scripts') + + {!! view_render_event('admin.layout.vue-app-mount.before') !!} + + + + {!! view_render_event('admin.layout.vue-app-mount.after') !!} + + + diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/sidebar/desktop/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/sidebar/desktop/index.blade.php new file mode 100644 index 0000000..b7ba0f1 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/sidebar/desktop/index.blade.php @@ -0,0 +1,134 @@ +
    +
    + +
    + + {!! view_render_event('admin.layout.sidebar.toggle.before') !!} + + + + + {!! view_render_event('admin.layout.sidebar.toggle.after') !!} +
    + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/sidebar/mobile/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/sidebar/mobile/index.blade.php new file mode 100644 index 0000000..eb73854 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/sidebar/mobile/index.blade.php @@ -0,0 +1,120 @@ + + + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/layouts/tabs.blade.php b/packages/Webkul/Admin/src/Resources/views/components/layouts/tabs.blade.php new file mode 100755 index 0000000..9a0b47f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/layouts/tabs.blade.php @@ -0,0 +1,20 @@ +@php + $tabs = menu()->getCurrentActiveMenu('admin')?->getChildren(); +@endphp + +@if ( + $tabs + && $tabs->isNotEmpty() +) +
    +
    + @foreach ($tabs as $tab) + +
    + {{ $tab->getName() }} +
    +
    + @endforeach +
    +
    +@endif diff --git a/packages/Webkul/Admin/src/Resources/views/components/lookup/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/lookup/index.blade.php new file mode 100644 index 0000000..fd3a27a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/lookup/index.blade.php @@ -0,0 +1,365 @@ + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/media/images.blade.php b/packages/Webkul/Admin/src/Resources/views/components/media/images.blade.php new file mode 100644 index 0000000..5b97cbc --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/media/images.blade.php @@ -0,0 +1,334 @@ +@props([ + 'name' => 'images', + 'allowMultiple' => false, + 'showPlaceholders' => false, + 'uploadedImages' => [], + 'width' => '120px', + 'height' => '120px' +]) + + + + + +@pushOnce('scripts') + + + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/media/videos.blade.php b/packages/Webkul/Admin/src/Resources/views/components/media/videos.blade.php new file mode 100644 index 0000000..f324887 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/media/videos.blade.php @@ -0,0 +1,307 @@ +@props([ + 'name' => 'images', + 'allowMultiple' => false, + 'uploadedVideos' => [], + 'width' => '210px', + 'height' => '120px' +]) + +get('class') }} +> + + +@pushOnce('scripts') + + + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/modal/confirm.blade.php b/packages/Webkul/Admin/src/Resources/views/components/modal/confirm.blade.php new file mode 100644 index 0000000..ed302b6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/modal/confirm.blade.php @@ -0,0 +1,140 @@ + + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/modal/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/modal/index.blade.php new file mode 100644 index 0000000..d2f4c00 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/modal/index.blade.php @@ -0,0 +1,225 @@ +@props([ + 'isActive' => false, + 'position' => 'center', + 'size' => 'normal', +]) + + + @isset($toggle) + + @endisset + + @isset($header) + + @endisset + + @isset($content) + + @endisset + + @isset($footer) + + @endisset + + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/accordion/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/accordion/index.blade.php new file mode 100644 index 0000000..7c97ce1 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/accordion/index.blade.php @@ -0,0 +1,17 @@ +
    +
    +

    +

    +
    + +
    +
    +

    +

    +

    +

    +

    +

    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/activities/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/activities/index.blade.php new file mode 100644 index 0000000..251e654 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/activities/index.blade.php @@ -0,0 +1,41 @@ +
    + +
    + @for ($i = 0; $i < 5; $i++) +
    +
    +
    + @endfor +
    + + +
    + +
    + @for ($i = 0; $i < 5; $i++) + +
    + +
    + + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    + @endfor +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/charts/bar.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/charts/bar.blade.php new file mode 100644 index 0000000..4762e4b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/charts/bar.blade.php @@ -0,0 +1,28 @@ +@props(['count' => 30]) + +
    +
    + @foreach (range(1, 10) as $i) +
    + @endforeach +
    + +
    +
    +
    + @foreach (range(1, $count) as $i) +
    + @endforeach +
    +
    + +
    + @foreach (range(1, $count) as $i) +
    + @endforeach +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/common/address.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/common/address.blade.php new file mode 100644 index 0000000..0eca400 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/common/address.blade.php @@ -0,0 +1,15 @@ +
    +
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/open-leads-by-states.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/open-leads-by-states.blade.php new file mode 100644 index 0000000..1ed64bf --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/open-leads-by-states.blade.php @@ -0,0 +1,16 @@ +
    +
    +
    +
    + +
    +
    + @for ($i = 0; $i < 4; $i++) +
    +
    +
    +
    + @endfor +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/over-all.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/over-all.blade.php new file mode 100644 index 0000000..c06eafe --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/over-all.blade.php @@ -0,0 +1,13 @@ +
    + @for ($i = 1; $i <= 6; $i++) +
    +
    + +
    +
    + +
    +
    +
    + @endfor +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue-by-sources.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue-by-sources.blade.php new file mode 100644 index 0000000..485c4e5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue-by-sources.blade.php @@ -0,0 +1,21 @@ +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue-by-types.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue-by-types.blade.php new file mode 100644 index 0000000..485c4e5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue-by-types.blade.php @@ -0,0 +1,21 @@ +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue.blade.php new file mode 100644 index 0000000..960fe5a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/revenue.blade.php @@ -0,0 +1,46 @@ +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/top-persons.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/top-persons.blade.php new file mode 100644 index 0000000..306ecde --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/top-persons.blade.php @@ -0,0 +1,25 @@ +
    +
    +
    +
    + +
    + @for ($i = 1; $i <= 5; $i++) +
    + +
    + +
    + +
    + +
    + + +
    +
    +
    +
    + @endfor +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/top-selling-products.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/top-selling-products.blade.php new file mode 100644 index 0000000..adb7b92 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/top-selling-products.blade.php @@ -0,0 +1,25 @@ +
    +
    +
    +
    + +
    + @for ($i = 1; $i <= 5; $i++) +
    + +
    + +
    + +
    + +
    + + +
    +
    +
    +
    + @endfor +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/total-leads.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/total-leads.blade.php new file mode 100644 index 0000000..f83cd45 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/dashboard/index/total-leads.blade.php @@ -0,0 +1,18 @@ +
    +
    + + + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/index.blade.php new file mode 100644 index 0000000..adf2ac1 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/index.blade.php @@ -0,0 +1,15 @@ +@props(['isMultiRow' => false]) + +
    + + +
    +
    +
    + + + +
    +
    +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/table/body.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/table/body.blade.php new file mode 100644 index 0000000..a1689b1 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/table/body.blade.php @@ -0,0 +1,53 @@ +@props(['isMultiRow' => false]) + +@for ($i = 0; $i < 10; $i++) + @if (! $isMultiRow) +
    +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    +
    +
    + @else +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    + +
    +
    + +
    +
    + +
    + +
    +
    +
    + @endif +@endfor diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/table/head.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/table/head.blade.php new file mode 100644 index 0000000..60cf1d8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/table/head.blade.php @@ -0,0 +1,29 @@ +@props(['isMultiRow' => false]) + +@if (! $isMultiRow) +
    +
    + +
    + +
    + +
    + +
    + +
    +
    +@else +
    +
    +
    + +
    +
    + +
    + +
    +
    +@endif diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar.blade.php new file mode 100644 index 0000000..7284c09 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar.blade.php @@ -0,0 +1,27 @@ +
    + +
    +
    + +
    +
    + + +
    +
    +

    +
    + +
    +
    + +

    + +
    +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/filter.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/filter.blade.php new file mode 100644 index 0000000..cc31c5b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/filter.blade.php @@ -0,0 +1,3 @@ +{{--
    +
    +
    --}} diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/pagination.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/pagination.blade.php new file mode 100644 index 0000000..23f8f3c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/pagination.blade.php @@ -0,0 +1,17 @@ +
    +
    +

    +
    + +
    +
    + +

    + +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/search.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/search.blade.php new file mode 100644 index 0000000..7e58b44 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/datagrid/toolbar/search.blade.php @@ -0,0 +1,5 @@ +
    +
    + +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/configurations.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/configurations.blade.php new file mode 100644 index 0000000..5566bcc --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/configurations.blade.php @@ -0,0 +1,12 @@ +@for ($i = 0; $i < 3; $i++) +
    + +
    + +
    +

    +

    +
    +
    +
    +@endfor diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/leads.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/leads.blade.php new file mode 100644 index 0000000..36387e6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/leads.blade.php @@ -0,0 +1,17 @@ +@for ($i = 0; $i < 3; $i++) +
    + +
    + +
    +

    +

    +
    +
    + + +
    +

    +
    +
    +@endfor diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/persons.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/persons.blade.php new file mode 100644 index 0000000..f339b28 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/persons.blade.php @@ -0,0 +1,17 @@ +@for ($i = 0; $i < 3; $i++) +
    + +
    + +
    +

    +

    +
    +
    + + +
    +

    +
    +
    +@endfor \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/products.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/products.blade.php new file mode 100644 index 0000000..98cbed4 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/products.blade.php @@ -0,0 +1,17 @@ +@for ($i = 0; $i < 3; $i++) +
    + +
    + +
    +

    +

    +
    +
    + + +
    +

    +
    +
    +@endfor \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/quotes.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/quotes.blade.php new file mode 100644 index 0000000..fffad36 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/quotes.blade.php @@ -0,0 +1,12 @@ +@for ($i = 0; $i < 3; $i++) +
    + +
    + +
    +

    +

    +
    +
    +
    +@endfor \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/settings.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/settings.blade.php new file mode 100644 index 0000000..5566bcc --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/header/mega-search/settings.blade.php @@ -0,0 +1,12 @@ +@for ($i = 0; $i < 3; $i++) +
    + +
    + +
    +

    +

    +
    +
    +
    +@endfor diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/image/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/image/index.blade.php new file mode 100644 index 0000000..f4ff081 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/image/index.blade.php @@ -0,0 +1,44 @@ + +
    merge(['class' => 'shimmer bg-neutral-100']) }}>
    +
    + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/datagrid.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/datagrid.blade.php new file mode 100644 index 0000000..292de37 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/datagrid.blade.php @@ -0,0 +1,146 @@ +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    + +
    + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    +
    + + +
    +
    + +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/index/kanban.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/index/kanban.blade.php new file mode 100644 index 0000000..3833651 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/index/kanban.blade.php @@ -0,0 +1,60 @@ + + +
    + + @for ($i = 1; $i <= 6; $i++) +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + +
    + @for ($j = 1; $j <= 3; $j++) + +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + @endfor +
    +
    + @endfor +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/index/kanban/toolbar.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/index/kanban/toolbar.blade.php new file mode 100644 index 0000000..3b34210 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/index/kanban/toolbar.blade.php @@ -0,0 +1,17 @@ +
    +
    +
    +
    +
    + +
    + +
    + + +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/view/mail/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/view/mail/index.blade.php new file mode 100644 index 0000000..2547d55 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/view/mail/index.blade.php @@ -0,0 +1,111 @@ +@props([ + 'count' => 1, +]) + +
    +
    + @for ($i = 0; $i < $count; $i++) +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + @endfor +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/view/stages.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/view/stages.blade.php new file mode 100644 index 0000000..8373cc9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/leads/view/stages.blade.php @@ -0,0 +1,9 @@ +@props(['count' => 5]) + +
    + @for ($i = 0; $i < $count; $i++) +
    +
    +
    + @endfor +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/index.blade.php new file mode 100644 index 0000000..1f633aa --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/index.blade.php @@ -0,0 +1,13 @@ +
    + + +
    +
    +
    + + + +
    +
    +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/table/body.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/table/body.blade.php new file mode 100644 index 0000000..11f80cd --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/table/body.blade.php @@ -0,0 +1,31 @@ +@for ($i = 0; $i < 10; $i++) +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +@endfor \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/table/head.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/table/head.blade.php new file mode 100644 index 0000000..caf7f84 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/mail/datagrid/table/head.blade.php @@ -0,0 +1,13 @@ +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/activities/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/activities/index.blade.php new file mode 100644 index 0000000..7c23df9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/activities/index.blade.php @@ -0,0 +1,41 @@ +
    + +
    + @for ($i = 0; $i < 5; $i++) +
    +
    +
    + @endfor +
    + + +
    + +
    + @for ($i = 0; $i < 5; $i++) + +
    + +
    + + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    + @endfor +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/stages.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/stages.blade.php new file mode 100644 index 0000000..3a4d6a0 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/stages.blade.php @@ -0,0 +1,9 @@ +@props(['count' => 5]) + +
    + @for ($i = 0; $i < $count; $i++) +
    +
    +
    + @endfor +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/tags.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/tags.blade.php new file mode 100644 index 0000000..e03ac2d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/person/view/tags.blade.php @@ -0,0 +1,9 @@ +@props(['count' => 5]) + +
    + @for ($i = 0; $i < $count; $i++) +
    + @endfor + +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/pipelines/kanban.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/pipelines/kanban.blade.php new file mode 100644 index 0000000..2135113 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/pipelines/kanban.blade.php @@ -0,0 +1,33 @@ +
    + @for ($i = 1; $i <= 6; $i++) +
    + +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + @endfor +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/quotes/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/quotes/index.blade.php new file mode 100644 index 0000000..d55cc82 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/quotes/index.blade.php @@ -0,0 +1,116 @@ +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/attributes.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/attributes.blade.php new file mode 100644 index 0000000..1cc494c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/attributes.blade.php @@ -0,0 +1,62 @@ + +
    + +
    +
    +
    + + @for ($i = 1; $i < 5; $i++) +
    +
    + +
    +
    + @endfor +
    +
    + + +
    + +
    +
    +

    + +

    +
    + +
    + @for ($i = 1; $i < 4; $i++) +
    +
    + +
    +
    + @endfor +
    +
    + + +
    +
    +

    + +

    +
    + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/body.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/body.blade.php new file mode 100644 index 0000000..fce4f85 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/body.blade.php @@ -0,0 +1,13 @@ +
    +
    + +
    + +
    +
    + +
    + +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/head.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/head.blade.php new file mode 100644 index 0000000..004d22f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/head.blade.php @@ -0,0 +1,7 @@ +
    +
    + +
    + +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/index.blade.php new file mode 100644 index 0000000..8cfb536 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/settings/web-forms/index.blade.php @@ -0,0 +1,13 @@ +
    + + +
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/tabs/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/tabs/index.blade.php new file mode 100644 index 0000000..614e942 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/tabs/index.blade.php @@ -0,0 +1,9 @@ +
    + + + + + + + +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/tags/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/tags/index.blade.php new file mode 100644 index 0000000..e03ac2d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/tags/index.blade.php @@ -0,0 +1,9 @@ +@props(['count' => 5]) + +
    + @for ($i = 0; $i < $count; $i++) +
    + @endfor + +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/tinymce/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/tinymce/index.blade.php new file mode 100644 index 0000000..311bb35 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/tinymce/index.blade.php @@ -0,0 +1,3 @@ +
    merge(['class' => 'shimmer block bg-neutral-100']) }}> + +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/shimmer/tree/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/shimmer/tree/index.blade.php new file mode 100644 index 0000000..fc8e172 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/shimmer/tree/index.blade.php @@ -0,0 +1,31 @@ +@for ($j = 0; $j < 3; $j++) +
    + +
    + +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    + @for ($k = 0; $k < 5; $k++) +
    +
    + +
    + +
    +
    + @endfor +
    +
    +@endfor \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/spinner/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/spinner/index.blade.php new file mode 100644 index 0000000..ed724bd --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/spinner/index.blade.php @@ -0,0 +1,27 @@ + +@props(['color' => 'currentColor']) + + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/table/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/table/index.blade.php new file mode 100644 index 0000000..b156796 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/table/index.blade.php @@ -0,0 +1,3 @@ +merge(['class' => 'table-fixed w-full min-w-[800px] text-left text-sm border border-gray-300 dark:border-gray-800']) }}> + {{ $slot }} +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/table/tbody/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/table/tbody/index.blade.php new file mode 100644 index 0000000..fd45937 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/table/tbody/index.blade.php @@ -0,0 +1,3 @@ +merge(['class' => 'bg-white dark:bg-gray-900 dark:text-gray-300 dark:border-gray-800']) }}> + {{ $slot }} + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/table/tbody/tr.blade.php b/packages/Webkul/Admin/src/Resources/views/components/table/tbody/tr.blade.php new file mode 100644 index 0000000..178e26c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/table/tbody/tr.blade.php @@ -0,0 +1,3 @@ +merge(['scope' => 'row', 'class' => 'border-b border-gray-300 last:border-b-0 dark:border-gray-800']) }}> + {{ $slot }} + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/table/td.blade.php b/packages/Webkul/Admin/src/Resources/views/components/table/td.blade.php new file mode 100644 index 0000000..06148f9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/table/td.blade.php @@ -0,0 +1,4 @@ +merge(['scope' => 'row', 'class' => 'whitespace-nowrap px-6 py-4']) }}> + {{ $slot }} + + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/table/th.blade.php b/packages/Webkul/Admin/src/Resources/views/components/table/th.blade.php new file mode 100644 index 0000000..57bc1d5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/table/th.blade.php @@ -0,0 +1,3 @@ +merge(['scope' => 'col', 'class' => 'whitespace-nowrap px-6 py-4 font-semibold']) }}> + {{ $slot }} + diff --git a/packages/Webkul/Admin/src/Resources/views/components/table/thead/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/table/thead/index.blade.php new file mode 100644 index 0000000..ace6cd8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/table/thead/index.blade.php @@ -0,0 +1,3 @@ +merge(['class' => 'bg-gray-50 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300']) }}> + {{ $slot }} + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/table/thead/tr.blade.php b/packages/Webkul/Admin/src/Resources/views/components/table/thead/tr.blade.php new file mode 100644 index 0000000..003a958 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/table/thead/tr.blade.php @@ -0,0 +1,3 @@ +merge(['scope' => 'row', 'class' => 'border-b border-gray-300 dark:border-gray-800']) }}> + {{ $slot }} + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/tabs/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/tabs/index.blade.php new file mode 100644 index 0000000..a43b233 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/tabs/index.blade.php @@ -0,0 +1,65 @@ +@props(['position' => 'left']) + + + + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/tabs/item.blade.php b/packages/Webkul/Admin/src/Resources/views/components/tabs/item.blade.php new file mode 100644 index 0000000..52c2fea --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/tabs/item.blade.php @@ -0,0 +1,51 @@ +@props([ + 'title' => '', + 'isSelected' => false, +]) + +merge(['class' => 'p-4']) }} +> + + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/tags/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/tags/index.blade.php new file mode 100644 index 0000000..ebb4f6a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/tags/index.blade.php @@ -0,0 +1,398 @@ +@props([ + 'attachEndpoint', + 'detachEndpoint', + 'addedTags' => [], +]) + + + + + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/components/tinymce/index.blade.php b/packages/Webkul/Admin/src/Resources/views/components/tinymce/index.blade.php new file mode 100644 index 0000000..50a9600 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/tinymce/index.blade.php @@ -0,0 +1,219 @@ +@php($placeholders = app('\Webkul\Automation\Helpers\Entity')->getEmailTemplatePlaceholders()) + + + +@pushOnce('scripts') + + + + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/tree/checkbox.blade.php b/packages/Webkul/Admin/src/Resources/views/components/tree/checkbox.blade.php new file mode 100644 index 0000000..8af3905 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/tree/checkbox.blade.php @@ -0,0 +1,55 @@ +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/tree/radio.blade.php b/packages/Webkul/Admin/src/Resources/views/components/tree/radio.blade.php new file mode 100644 index 0000000..769062a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/tree/radio.blade.php @@ -0,0 +1,54 @@ +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/components/tree/view.blade.php b/packages/Webkul/Admin/src/Resources/views/components/tree/view.blade.php new file mode 100644 index 0000000..0ab10a7 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/components/tree/view.blade.php @@ -0,0 +1,393 @@ +@props([ + 'inputType' => 'checkbox', + 'selectionType' => 'hierarchical', +]) + +@if ($inputType == 'checkbox') + + +@else + + +@endif + +except(['input-type', 'selection-type']) }} + input-type="{{ $inputType }}" + selection-type="{{ $selectionType }}" +> + + + +@pushOnce('scripts') + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/configuration/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/configuration/edit.blade.php new file mode 100644 index 0000000..c0e7cdb --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/configuration/edit.blade.php @@ -0,0 +1,85 @@ +@php + $activeConfiguration = system_config()->getActiveConfigurationItem(); + + $name = $activeConfiguration->getName(); +@endphp + + + + {{ strip_tags($name) }} + + + {!! view_render_event('admin.configuration.edit.form_controls.before') !!} + + + + +
    +

    + {{ $name }} +

    + + +
    + {!! view_render_event('admin.configuration.edit.back_button.before') !!} + + + + @lang('admin::app.configuration.index.back') + + + {!! view_render_event('admin.configuration.edit.back_button.after') !!} + + {!! view_render_event('admin.configuration.edit.save_button.before') !!} + + + + {!! view_render_event('admin.configuration.edit.save_button.after') !!} +
    +
    + +
    + @foreach ($activeConfiguration->getChildren() as $child) +
    +

    + {{ $child->getName() }} +

    + +

    + {!! $child->getInfo() !!} +

    +
    + +
    + {!! view_render_event('admin.configuration.edit.form_controls.before') !!} + + @foreach ($child->getFields() as $field) + @if ( + $field->getType() == 'blade' + && view()->exists($path = $field->getPath()) + ) + {!! view($path, compact('field', 'child'))->render() !!} + @else + @include ('admin::configuration.field-type') + @endif + @endforeach + + {!! view_render_event('admin.configuration.edit.form_controls.after') !!} +
    + @endforeach +
    +
    + + {!! view_render_event('admin.configuration.edit.form_controls.after') !!} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/configuration/field-type.blade.php b/packages/Webkul/Admin/src/Resources/views/configuration/field-type.blade.php new file mode 100755 index 0000000..ecadab2 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/configuration/field-type.blade.php @@ -0,0 +1,524 @@ +@php($value = old($field->getNameKey()) ?? system_config()->getConfigData($field->getNameKey())) + + + +
    + +
    + +
    +
    +
    + +@pushOnce('scripts') + + + + + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/configuration/index.blade.php b/packages/Webkul/Admin/src/Resources/views/configuration/index.blade.php new file mode 100644 index 0000000..ece11be --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/configuration/index.blade.php @@ -0,0 +1,196 @@ + + + + {{ menu()->getLabel('configuration', 'admin::app.configuration.index.title') }} + + + + + + {!! view_render_event('admin.configuration.index.header.before') !!} + + +
    +

    + {{ menu()->getLabel('configuration', 'admin::app.configuration.index.title') }} +

    + + {!! view_render_event('admin.configuration.index.header.configuration_search.before') !!} + + + +
    + + + +
    +
    + + {!! view_render_event('admin.configuration.index.header.configuration_search.after') !!} +
    + + {!! view_render_event('admin.configuration.index.header.after') !!} + + {!! view_render_event('admin.configuration.index.content.before') !!} + + +
    + @foreach (system_config()->getItems() as $item) +
    +
    + +

    + {{ $item->getName() }} +

    + + +

    + {{ $item->getInfo() }} +

    +
    + + +
    + @endforeach +
    + + {!! view_render_event('admin.configuration.index.content.after') !!} + + @pushOnce('scripts') + + + + @endpushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/organizations/create.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/organizations/create.blade.php new file mode 100644 index 0000000..e4f95d5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/organizations/create.blade.php @@ -0,0 +1,74 @@ + + + + + @lang('admin::app.contacts.organizations.create.title') + + + {!! view_render_event('admin.organizations.create.form.before') !!} + + + +
    +
    +
    + {!! view_render_event('admin.organizations.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.organizations.create.breadcrumbs.before') !!} + +
    + @lang('admin::app.contacts.organizations.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.organizations.create.save_buttons.before') !!} + + + + + {!! view_render_event('admin.organizations.create.save_buttons.before') !!} +
    +
    +
    + +
    + {!! view_render_event('admin.contacts.organizations.create.form_controls.before') !!} + + + + {!! view_render_event('admin.contacts.organizations.edit.form_controls.after') !!} +
    +
    +
    + + {!! view_render_event('admin.organizations.create.form.after') !!} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/organizations/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/organizations/edit.blade.php new file mode 100644 index 0000000..09e9680 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/organizations/edit.blade.php @@ -0,0 +1,76 @@ + + + + + @lang('admin::app.contacts.organizations.edit.title') + + + {!! view_render_event('admin.organizations.edit.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.organizations.edit.breadcrumbs.before', ['organization' => $organization]) !!} + + + + {!! view_render_event('admin.organizations.edit.breadcrumbs.before', ['organization' => $organization]) !!} + +
    + @lang('admin::app.contacts.organizations.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.organizations.edit.save_button.before', ['organization' => $organization]) !!} + + + + + {!! view_render_event('admin.organizations.edit.save_button.after', ['organization' => $organization]) !!} +
    +
    +
    + +
    + {!! view_render_event('admin.contacts.organizations.edit.form_controls.before') !!} + + + + {!! view_render_event('admin.contacts.organizations.edit.form_controls.after') !!} +
    +
    +
    + + {!! view_render_event('admin.organizations.edit.form.after') !!} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/organizations/index.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/organizations/index.blade.php new file mode 100644 index 0000000..0beb6f7 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/organizations/index.blade.php @@ -0,0 +1,50 @@ + + + + {{ menu()->getLabel('contacts.organizations', 'admin::app.contacts.organizations.index.title') }} + + +
    +
    +
    + {!! view_render_event('admin.organizations.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.organizations.index.breadcrumbs.before') !!} + +
    + {{ menu()->getLabel('contacts.organizations', 'admin::app.contacts.organizations.index.title') }} +
    +
    + +
    +
    + {!! view_render_event('admin.organizations.index.create_button.before') !!} + + @if (bouncer()->hasPermission('contacts.organizations.create')) + + + @lang('admin::app.contacts.organizations.index.create-btn') + + @endif + + {!! view_render_event('admin.organizations.index.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.organizations.datagrid.index.before') !!} + + + + + + + {!! view_render_event('admin.organizations.datagrid.index.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/persons/create.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/persons/create.blade.php new file mode 100644 index 0000000..aad1f47 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/persons/create.blade.php @@ -0,0 +1,117 @@ + + + + @lang('admin::app.contacts.persons.create.title') + + + {!! view_render_event('admin.persons.create.form.before') !!} + + + +
    + +
    +
    + {!! view_render_event('admin.persons.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.persons.create.breadcrumbs.after') !!} + +
    + @lang('admin::app.contacts.persons.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.persons.create.create_button.before') !!} + + + + + {!! view_render_event('admin.persons.create.create_button.after') !!} +
    +
    +
    + + +
    + {!! view_render_event('admin.persons.create.form_controls.before') !!} + + + + + + {!! view_render_event('admin.persons.create.form_controls.after') !!} +
    +
    +
    + + {!! view_render_event('admin.persons.create.form.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/persons/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/persons/edit.blade.php new file mode 100644 index 0000000..52667ec --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/persons/edit.blade.php @@ -0,0 +1,120 @@ + + + + + @lang('admin::app.contacts.persons.edit.title') + + + {!! view_render_event('admin.persons.edit.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.persons.edit.breadcrumbs.before') !!} + + + + {!! view_render_event('admin.persons.edit.breadcrumbs.after') !!} + +
    + @lang('admin::app.contacts.persons.edit.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.persons.edit.save_button.before') !!} + + + + {!! view_render_event('admin.persons.edit.save_button.after') !!} +
    +
    +
    + +
    + {!! view_render_event('admin.contacts.persons.edit.form_controls.before') !!} + + + + + + {!! view_render_event('admin.contacts.persons.edit.form_controls.after') !!} +
    +
    +
    + + {!! view_render_event('admin.persons.edit.form.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/persons/index.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/persons/index.blade.php new file mode 100644 index 0000000..781acf6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/persons/index.blade.php @@ -0,0 +1,344 @@ + + + {{ menu()->getLabel('contacts.persons', 'admin::app.contacts.persons.index.title') }} + + +
    +
    +
    + + +
    + {{ menu()->getLabel('contacts.persons', 'admin::app.contacts.persons.index.title') }} +
    +
    + +
    + + + + +
    + {!! view_render_event('admin.persons.index.create_button.before') !!} + + @if (bouncer()->hasPermission('contacts.persons.create')) + + @lang('admin::app.contacts.persons.index.create-btn') + + @endif + + {!! view_render_event('admin.persons.index.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.persons.index.datagrid.before') !!} + + + + + + + {!! view_render_event('admin.persons.index.datagrid.after') !!} +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/persons/view.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/persons/view.blade.php new file mode 100644 index 0000000..032b948 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/persons/view.blade.php @@ -0,0 +1,100 @@ + + + @lang('admin::app.contacts.persons.view.title', ['name' => strip_tags($person->name)]) + + + +
    + + {!! view_render_event('admin.contact.persons.view.left.before', ['person' => $person]) !!} + +
    + +
    + +
    + +
    + + {!! view_render_event('admin.contact.persons.view.tags.before', ['person' => $person]) !!} + + + + + {!! view_render_event('admin.contact.persons.view.tags.after', ['person' => $person]) !!} + + + +
    + {!! view_render_event('admin.contact.persons.view.title.before', ['person' => $person]) !!} + +

    + {{ $person->name }} +

    + +

    + {{ $person->job_title }} +

    + + {!! view_render_event('admin.contact.persons.view.title.after', ['person' => $person]) !!} +
    + + +
    + {!! view_render_event('admin.contact.persons.view.actions.before', ['person' => $person]) !!} + + + + + + + + + + + + + + {!! view_render_event('admin.contact.persons.view.actions.after', ['person' => $person]) !!} +
    +
    + + + @include ('admin::contacts.persons.view.attributes') + + + @include ('admin::contacts.persons.view.organization') +
    + + {!! view_render_event('admin.contact.persons.view.left.after', ['person' => $person]) !!} + + +
    + {!! view_render_event('admin.contact.persons.view.right.before', ['person' => $person]) !!} + + + + + {!! view_render_event('admin.contact.persons.view.right.after', ['person' => $person]) !!} +
    +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/persons/view/attributes.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/persons/view/attributes.blade.php new file mode 100644 index 0000000..f205ee5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/persons/view/attributes.blade.php @@ -0,0 +1,41 @@ +{!! view_render_event('admin.contacts.persons.view.attributes.before', ['person' => $person]) !!} + +
    + + +

    + @lang('admin::app.contacts.persons.view.about-person') +

    + + + + {!! view_render_event('admin.contacts.persons.view.attributes.form_controls.before', ['person' => $person]) !!} + + +
    + {!! view_render_event('admin.contacts.persons.view.attributes.form_controls.attributes_view.before', ['person' => $person]) !!} + + + + {!! view_render_event('admin.contacts.persons.view.attributes.form_controls.attributes_view.after', ['person' => $person]) !!} + +
    + + {!! view_render_event('admin.contacts.persons.view.attributes.form_controls.after', ['person' => $person]) !!} + +
    +
    + +{!! view_render_event('admin.contacts.persons.view.attributes.before', ['person' => $person]) !!} diff --git a/packages/Webkul/Admin/src/Resources/views/contacts/persons/view/organization.blade.php b/packages/Webkul/Admin/src/Resources/views/contacts/persons/view/organization.blade.php new file mode 100644 index 0000000..5cc2840 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/contacts/persons/view/organization.blade.php @@ -0,0 +1,73 @@ +{!! view_render_event('admin.contacts.persons.view.organization.before', ['person' => $person]) !!} + +@if ($person?->organization) +
    +

    + @lang('admin::app.contacts.persons.view.about-organization') + + +

    + +
    + {!! view_render_event('admin.contacts.persons.view.organization.avatar.before', ['person' => $person]) !!} + + + + + {!! view_render_event('admin.contacts.persons.view.organization.avatar.after', ['person' => $person]) !!} + + +
    + {!! view_render_event('admin.contacts.persons.view.organization.name.before', ['person' => $person]) !!} + + + {{ $person->organization->name }} + + + {!! view_render_event('admin.contacts.persons.view.organization.name.after', ['person' => $person]) !!} + + + {!! view_render_event('admin.contacts.persons.view.organization.address.before', ['person' => $person]) !!} + + @if ($person->organization->address) +
    + @isset($person->organization->address['address']) + + {{ $person->organization->address['address'] }} + + @endisset + + @if( + isset($person->organization->address['postcode']) + && isset($person->organization->address['city']) + ) + + {{ $person->organization->address['postcode'] . ' ' . $person->organization->address['city'] }} + + @endif + + @isset($person->organization->address['state']) + + {{ core()->state_name($person->organization->address['state']) }} + + @endisset + + @isset($person->organization->address['country']) + + {{ core()->country_name($person->organization->address['country']) }} + + @endisset +
    + @endif + + {!! view_render_event('admin.contacts.persons.view.organization.address.after', ['person' => $person]) !!} +
    +
    +
    +@endif + +{!! view_render_event('admin.contacts.persons.view.organization.after', ['person' => $person]) !!} diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index.blade.php new file mode 100644 index 0000000..d5058c2 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index.blade.php @@ -0,0 +1,203 @@ + + + {{ menu()->getLabel('dashboard', 'admin::app.dashboard.index.title') }} + + + + {!! view_render_event('admin.dashboard.index.header.before') !!} + +
    + {!! view_render_event('admin.dashboard.index.header.left.before') !!} + +
    +

    + {{ menu()->getLabel('dashboard', 'admin::app.dashboard.index.title') }} +

    +
    + + {!! view_render_event('admin.dashboard.index.header.left.after') !!} + + + {!! view_render_event('admin.dashboard.index.header.right.before') !!} + + + +
    + @if ($pipelines->count() > 1) +
    + @endif + +
    +
    +
    +
    +
    + + {!! view_render_event('admin.dashboard.index.header.right.after') !!} +
    + + {!! view_render_event('admin.dashboard.index.header.after') !!} + + + {!! view_render_event('admin.dashboard.index.content.before') !!} + +
    + + {!! view_render_event('admin.dashboard.index.content.left.before') !!} + +
    + + @include('admin::dashboard.index.revenue') + + + @include('admin::dashboard.index.over-all') + + + @include('admin::dashboard.index.total-leads') + +
    + + @include('admin::dashboard.index.top-selling-products') + + + @include('admin::dashboard.index.top-persons') +
    +
    + + {!! view_render_event('admin.dashboard.index.content.left.after') !!} + + + {!! view_render_event('admin.dashboard.index.content.right.before') !!} + +
    + + @include('admin::dashboard.index.open-leads-by-states') + + + @include('admin::dashboard.index.revenue-by-sources') + + + @include('admin::dashboard.index.revenue-by-types') +
    + + {!! view_render_event('admin.dashboard.index.content.left.after') !!} +
    + + {!! view_render_event('admin.dashboard.index.content.after') !!} + + @pushOnce('scripts') + + + + + + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index/open-leads-by-states.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index/open-leads-by-states.blade.php new file mode 100644 index 0000000..a25c1b6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index/open-leads-by-states.blade.php @@ -0,0 +1,168 @@ +{!! view_render_event('admin.dashboard.index.open_leads_by_states.before') !!} + + + + + + + +{!! view_render_event('admin.dashboard.index.open_leads_by_states.after') !!} + +@pushOnce('scripts') + + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index/over-all.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index/over-all.blade.php new file mode 100644 index 0000000..fe1f6c6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index/over-all.blade.php @@ -0,0 +1,231 @@ +{!! view_render_event('admin.dashboard.index.over_all.before') !!} + + + + + + + +{!! view_render_event('admin.dashboard.index.over_all.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue-by-sources.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue-by-sources.blade.php new file mode 100644 index 0000000..1178e85 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue-by-sources.blade.php @@ -0,0 +1,154 @@ +{!! view_render_event('admin.dashboard.index.revenue_by_sources.before') !!} + + + + + + + +{!! view_render_event('admin.dashboard.index.revenue_by_sources.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue-by-types.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue-by-types.blade.php new file mode 100644 index 0000000..6b55fd9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue-by-types.blade.php @@ -0,0 +1,154 @@ +{!! view_render_event('admin.dashboard.index.revenue_by_types.before') !!} + + + + + + + +{!! view_render_event('admin.dashboard.index.revenue_by_types.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue.blade.php new file mode 100644 index 0000000..106dfa1 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index/revenue.blade.php @@ -0,0 +1,235 @@ +{!! view_render_event('admin.dashboard.index.revenue.after') !!} + + + + + + + +{!! view_render_event('admin.dashboard.index.revenue.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index/top-persons.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index/top-persons.blade.php new file mode 100644 index 0000000..43b165b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index/top-persons.blade.php @@ -0,0 +1,120 @@ +{!! view_render_event('admin.dashboard.index.top_persons.before') !!} + + + + + + + +{!! view_render_event('admin.dashboard.index.top_persons.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index/top-selling-products.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index/top-selling-products.blade.php new file mode 100644 index 0000000..55a91f6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index/top-selling-products.blade.php @@ -0,0 +1,129 @@ +{!! view_render_event('admin.dashboard.index.top_selling_proudcts.before') !!} + + + + + + + +{!! view_render_event('admin.dashboard.index.top_selling_proudcts.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/index/total-leads.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/index/total-leads.blade.php new file mode 100644 index 0000000..894c6c9 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/index/total-leads.blade.php @@ -0,0 +1,128 @@ +{!! view_render_event('admin.dashboard.index.total_leads.before') !!} + + + + + + + +{!! view_render_event('admin.dashboard.index.total_leads.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/dashboard/pdf.blade.php b/packages/Webkul/Admin/src/Resources/views/dashboard/pdf.blade.php new file mode 100644 index 0000000..16c461f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/dashboard/pdf.blade.php @@ -0,0 +1,468 @@ + + + + + + + + + @php + if ($locale == 'en') { + $fontFamily = [ + 'regular' => 'DejaVu Sans', + 'bold' => 'DejaVu Sans', + ]; + } else { + $fontFamily = [ + 'regular' => 'Arial, sans-serif', + 'bold' => 'Arial, sans-serif', + ]; + } + + if (in_array($locale, ['ar', 'fa', 'tr'])) { + $fontFamily = [ + 'regular' => 'DejaVu Sans', + 'bold' => 'DejaVu Sans', + ]; + } + @endphp + + + + + +
    + +
    + @if ($logo) + + @endif + +
    + @lang('admin::app.dashboard.index.pdf.title') +
    + +
    + @lang('admin::app.dashboard.index.pdf.filtered-date'): {{ $startDate->format('d M Y') }} - {{ $endDate->format('d M Y') }} +
    +
    + +
    + + +
    +
    + @lang('admin::app.dashboard.index.pdf.revenue') +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    @lang('admin::app.dashboard.index.pdf.metric')@lang('admin::app.dashboard.index.pdf.current')@lang('admin::app.dashboard.index.pdf.previous')@lang('admin::app.dashboard.index.pdf.progress')
    @lang('admin::app.dashboard.index.revenue.won-revenue'){{ $revenueStats['total_won_revenue']['formatted_total'] }}{{ core()->formatBasePrice($revenueStats['total_won_revenue']['previous']) }}{{ number_format($revenueStats['total_won_revenue']['progress'], 2) }}%
    @lang('admin::app.dashboard.index.revenue.lost-revenue'){{ $revenueStats['total_lost_revenue']['formatted_total'] }}{{ core()->formatBasePrice($revenueStats['total_lost_revenue']['previous']) }}{{ number_format($revenueStats['total_lost_revenue']['progress'], 2) }}%
    +
    +
    + + +
    +
    + @lang('admin::app.dashboard.index.pdf.overview') +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @lang('admin::app.dashboard.index.pdf.metric')@lang('admin::app.dashboard.index.pdf.current')@lang('admin::app.dashboard.index.pdf.previous')@lang('admin::app.dashboard.index.pdf.progress')
    @lang('admin::app.dashboard.index.over-all.total-leads'){{ $overAllStats['total_leads']['current'] }}{{ $overAllStats['total_leads']['previous'] }}{{ number_format($overAllStats['total_leads']['progress'], 2) }}%
    @lang('admin::app.dashboard.index.over-all.average-lead-value'){{ $overAllStats['average_lead_value']['formatted_total'] }}{{ core()->formatBasePrice($overAllStats['average_lead_value']['previous']) }}{{ number_format($overAllStats['average_lead_value']['progress'], 2) }}%
    @lang('admin::app.dashboard.index.over-all.average-leads-per-day'){{ number_format($overAllStats['average_leads_per_day']['current'], 2) }}{{ number_format($overAllStats['average_leads_per_day']['previous'], 2) }}{{ number_format($overAllStats['average_leads_per_day']['progress'], 2) }}%
    @lang('admin::app.dashboard.index.over-all.total-quotations'){{ $overAllStats['total_quotations']['current'] }}{{ $overAllStats['total_quotations']['previous'] }}{{ number_format($overAllStats['total_quotations']['progress'], 2) }}%
    @lang('admin::app.dashboard.index.over-all.total-persons'){{ $overAllStats['total_persons']['current'] }}{{ $overAllStats['total_persons']['previous'] }}{{ number_format($overAllStats['total_persons']['progress'], 2) }}%
    @lang('admin::app.dashboard.index.over-all.total-organizations'){{ $overAllStats['total_organizations']['current'] }}{{ $overAllStats['total_organizations']['previous'] }}{{ number_format($overAllStats['total_organizations']['progress'], 2) }}%
    +
    +
    + + +
    +
    + @lang('admin::app.dashboard.index.pdf.total-leads') +
    + +
    + + + + + + + + + + + + @php + $allOverTime = $totalLeadsStats['all']['over_time'] ?? []; + + $wonOverTime = $totalLeadsStats['won']['over_time'] ?? []; + + $lostOverTime = $totalLeadsStats['lost']['over_time'] ?? []; + @endphp + + @forelse ($allOverTime as $index => $interval) + + + + + + + @empty + + + + @endforelse + +
    @lang('admin::app.dashboard.index.pdf.date')@lang('admin::app.dashboard.index.pdf.all')@lang('admin::app.dashboard.index.pdf.won')@lang('admin::app.dashboard.index.pdf.lost')
    {{ $interval['label'] }}{{ $interval['count'] }}{{ $wonOverTime[$index]['count'] ?? 0 }}{{ $lostOverTime[$index]['count'] ?? 0 }}
    -
    +
    +
    + + +
    +
    + @lang('admin::app.dashboard.index.pdf.revenue-by-sources') +
    + +
    + + + + + + + + + + @forelse ($revenueBySources as $source) + + + + + @empty + + + + @endforelse + +
    @lang('admin::app.dashboard.index.pdf.source')@lang('admin::app.dashboard.index.pdf.total')
    {{ $source->name ?? '-' }}{{ core()->formatBasePrice($source->total) }}
    -
    +
    +
    + + +
    +
    + @lang('admin::app.dashboard.index.pdf.revenue-by-types') +
    + +
    + + + + + + + + + + @forelse ($revenueByTypes as $type) + + + + + @empty + + + + @endforelse + +
    @lang('admin::app.dashboard.index.pdf.type')@lang('admin::app.dashboard.index.pdf.total')
    {{ $type->name ?? '-' }}{{ core()->formatBasePrice($type->total) }}
    -
    +
    +
    + + +
    +
    + @lang('admin::app.dashboard.index.pdf.open-leads-by-stages') +
    + +
    + + + + + + + + + + @forelse ($openLeadsByStates as $stage) + + + + + @empty + + + + @endforelse + +
    @lang('admin::app.dashboard.index.pdf.stage')@lang('admin::app.dashboard.index.pdf.total')
    {{ $stage->name ?? '-' }}{{ $stage->total }}
    -
    +
    +
    + + +
    +
    + @lang('admin::app.dashboard.index.pdf.top-selling-products') +
    + +
    + + + + + + + + + + + @forelse ($topSellingProducts as $product) + + + + + + @empty + + + + @endforelse + +
    @lang('admin::app.dashboard.index.pdf.product')@lang('admin::app.dashboard.index.pdf.price')@lang('admin::app.dashboard.index.pdf.total')
    {{ $product['name'] ?? '-' }}{{ $product['formatted_price'] }}{{ $product['formatted_revenue'] }}
    -
    +
    +
    + + +
    +
    + @lang('admin::app.dashboard.index.pdf.top-persons') +
    + +
    + + + + + + + + + + @forelse ($topPersons as $person) + + + + + @empty + + + + @endforelse + +
    @lang('admin::app.dashboard.index.pdf.person')@lang('admin::app.dashboard.index.pdf.total')
    {{ $person['name'] ?? '-' }}{{ $person['formatted_revenue'] }}
    -
    +
    +
    +
    + + diff --git a/packages/Webkul/Admin/src/Resources/views/emails/common/index.blade.php b/packages/Webkul/Admin/src/Resources/views/emails/common/index.blade.php new file mode 100644 index 0000000..f8a3edc --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/emails/common/index.blade.php @@ -0,0 +1,3 @@ +@component('admin::emails.layout') + {!! $body !!} +@endcomponent diff --git a/packages/Webkul/Admin/src/Resources/views/emails/layout.blade.php b/packages/Webkul/Admin/src/Resources/views/emails/layout.blade.php new file mode 100644 index 0000000..38e6f6a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/emails/layout.blade.php @@ -0,0 +1,36 @@ + + + + + + + + + + + + +
    +
    + +
    + + {{ config('app.name') }} + +
    + + + {{ $slot }} + + +

    + @lang('admin::app.emails.common.cheers', ['app_name' => config('app.name')]) +

    +
    +
    + + diff --git a/packages/Webkul/Admin/src/Resources/views/emails/users/create.blade.php b/packages/Webkul/Admin/src/Resources/views/emails/users/create.blade.php new file mode 100644 index 0000000..1d83a1d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/emails/users/create.blade.php @@ -0,0 +1,11 @@ +@component('admin::emails.layout') +
    +

    + @lang('admin::app.emails.common.user.dear', ['username' => $user_name]), 👋 +

    + +

    + @lang('admin::app.emails.common.user.create-body') +

    +
    +@endcomponent diff --git a/packages/Webkul/Admin/src/Resources/views/emails/users/forget-password.blade.php b/packages/Webkul/Admin/src/Resources/views/emails/users/forget-password.blade.php new file mode 100644 index 0000000..bad6350 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/emails/users/forget-password.blade.php @@ -0,0 +1,28 @@ +@component('admin::emails.layout') +
    +

    + @lang('admin::app.emails.common.user.forget-password.dear', ['username' => $user_name]), 👋 +

    + +

    + @lang('admin::app.emails.common.user.forget-password.info') +

    + +

    + + @lang('admin::app.emails.common.user.forget-password.reset-password') + +

    + +

    + @lang('admin::app.emails.common.user.forget-password.final-summary') +

    + +

    + @lang('admin::app.emails.common.user.forget-password.thanks') +

    +
    +@endcomponent diff --git a/packages/Webkul/Admin/src/Resources/views/errors/index.blade.php b/packages/Webkul/Admin/src/Resources/views/errors/index.blade.php new file mode 100644 index 0000000..0a95db6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/errors/index.blade.php @@ -0,0 +1,64 @@ + + + + @lang("admin::app.errors.{$errorCode}.title") + + + +
    +
    +
    + + +
    + {{ $errorCode }} +
    + +

    + @lang("admin::app.errors.{$errorCode}.description") +

    + + + +

    + @lang('admin::app.errors.support', [ + 'link' => 'mailto:support@example.com', + 'email' => 'support@example.com', + 'class' => 'font-semibold text-blue-600 transition-all hover:underline', + ]) +

    +
    + +
    + +
    +
    +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/help/card.blade.php b/packages/Webkul/Admin/src/Resources/views/help/card.blade.php new file mode 100644 index 0000000..576b20a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/help/card.blade.php @@ -0,0 +1,29 @@ + +
    + + + {!! $icons[$card['icon']] !!} + + + + + + +
    + +

    + @lang($card['title']) +

    + +

    + @lang($card['description']) +

    + + + {{ $card['url_label'] }} + +
    diff --git a/packages/Webkul/Admin/src/Resources/views/help/index.blade.php b/packages/Webkul/Admin/src/Resources/views/help/index.blade.php new file mode 100644 index 0000000..ae3b3a5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/help/index.blade.php @@ -0,0 +1,124 @@ +@php + $icons = [ + 'cloud' => '', + 'support' => '', + 'services' => '', + 'extensions' => '', + 'docs' => '', + 'api' => '', + 'community' => '', + 'video' => '', + ]; +@endphp + + + + @lang('admin::app.help.index.title') + + + {!! view_render_event('admin.help.index.before') !!} + +
    + + {!! view_render_event('admin.help.index.hero.before') !!} + +
    +
    +

    + @lang('admin::app.help.index.title') +

    + +

    + @lang('admin::app.help.index.description') +

    +
    + + + @lang('admin::app.help.index.contact-us') + +
    + + {!! view_render_event('admin.help.index.hero.after') !!} + + + {!! view_render_event('admin.help.index.services.before') !!} + +
    +

    + @lang('admin::app.help.index.services.title') +

    + +
    + @foreach ($support['services'] as $card) + @include('admin::help.card', ['card' => $card, 'icons' => $icons]) + @endforeach +
    +
    + + {!! view_render_event('admin.help.index.services.after') !!} + + + {!! view_render_event('admin.help.index.resources.before') !!} + +
    +

    + @lang('admin::app.help.index.resources.title') +

    + +
    + @foreach ($support['resources'] as $card) + @include('admin::help.card', ['card' => $card, 'icons' => $icons]) + @endforeach +
    +
    + + {!! view_render_event('admin.help.index.resources.after') !!} + + +
    +
    + + + + + + +
    +

    @lang('admin::app.help.index.still-need-help-title')

    +

    @lang('admin::app.help.index.still-need-help-description')

    +
    +
    + + + @lang('admin::app.help.index.contact-us') + +
    + + +
    + @foreach ($support['community'] as $card) +
    + + + {!! $icons[$card['icon']] !!} + + + +
    +

    @lang($card['title'])

    +

    @lang($card['description'])

    + + @lang($card['link_label']) + +
    +
    + @endforeach +
    +
    + + {!! view_render_event('admin.help.index.after') !!} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/leads/common/contact.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/common/contact.blade.php new file mode 100644 index 0000000..810dbee --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/common/contact.blade.php @@ -0,0 +1,142 @@ +{!! view_render_event('admin.leads.create.contact_person.form_controls.before') !!} + + + +{!! view_render_event('admin.leads.create.contact_person.form_controls.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/leads/common/products.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/common/products.blade.php new file mode 100644 index 0000000..d983dcb --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/common/products.blade.php @@ -0,0 +1,271 @@ + + +@pushOnce('scripts') + + + + + +@endPushOnce \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/leads/create.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/create.blade.php new file mode 100644 index 0000000..d6ea619 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/create.blade.php @@ -0,0 +1,240 @@ + + + @lang('admin::app.leads.create.title') + + + {!! view_render_event('admin.leads.create.form.before') !!} + + + +
    +
    +
    + + +
    + @lang('admin::app.leads.create.title') +
    +
    + + {!! view_render_event('admin.leads.create.save_button.before') !!} + +
    + +
    + {!! view_render_event('admin.leads.create.form_buttons.before') !!} + + + + {!! view_render_event('admin.leads.create.form_buttons.after') !!} +
    +
    + + {!! view_render_event('admin.leads.create.save_button.after') !!} +
    + + @if (request('stage_id')) + + @endif + + @if (request('pipeline_id')) + + @endif + + + + + +
    +
    + + {!! view_render_event('admin.leads.create.form.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce + + @pushOnce('styles') + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/leads/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/edit.blade.php new file mode 100644 index 0000000..e156893 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/edit.blade.php @@ -0,0 +1,229 @@ + + + + @lang('admin::app.leads.edit.title') + + + {!! view_render_event('admin.leads.edit.form_controls.before', ['lead' => $lead]) !!} + + + +
    +
    +
    + + +
    + @lang('admin::app.leads.edit.title') +
    +
    + +
    + {!! view_render_event('admin.leads.edit.save_button.before', ['lead' => $lead]) !!} + + +
    + {!! view_render_event('admin.leads.edit.form_buttons.before') !!} + + + + {!! view_render_event('admin.leads.edit.form_buttons.after') !!} +
    + + {!! view_render_event('admin.leads.edit.save_button.after', ['lead' => $lead]) !!} +
    +
    + + + + + + + +
    +
    + + {!! view_render_event('admin.leads.edit.form_controls.after', ['lead' => $lead]) !!} + + @pushOnce('scripts') + + + + @endPushOnce + + @pushOnce('styles') + + @endPushOnce +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index.blade.php new file mode 100644 index 0000000..c45f8df --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index.blade.php @@ -0,0 +1,66 @@ + + + {{ menu()->getLabel('leads', 'admin::app.leads.index.title') }} + + + + {!! view_render_event('admin.leads.index.header.before') !!} + +
    + {!! view_render_event('admin.leads.index.header.left.before') !!} + +
    + + + +
    + {{ menu()->getLabel('leads', 'admin::app.leads.index.title') }} +
    +
    + + {!! view_render_event('admin.leads.index.header.left.after') !!} + + {!! view_render_event('admin.leads.index.header.right.before') !!} + +
    + + @if(core()->getConfigData('general.magic_ai.doc_generation.enabled')) + @include('admin::leads.index.upload') + @endif + + @if ((request()->view_type ?? "kanban") == "table") + + + @endif + + +
    + @if (bouncer()->hasPermission('leads.create')) + + @lang('admin::app.leads.index.create-btn') + + @endif +
    +
    + + {!! view_render_event('admin.leads.index.header.right.after') !!} +
    + + {!! view_render_event('admin.leads.index.header.after') !!} + + {!! view_render_event('admin.leads.index.content.before') !!} + + +
    + @if ((request()->view_type ?? "kanban") == "table") + @include('admin::leads.index.table') + @else + @include('admin::leads.index.kanban') + @endif +
    + + {!! view_render_event('admin.leads.index.content.after') !!} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index/kanban.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban.blade.php new file mode 100644 index 0000000..a7c917b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban.blade.php @@ -0,0 +1,881 @@ +{!! view_render_event('admin.leads.index.kanban.before') !!} + + + +
    + + +
    +
    + +{!! view_render_event('admin.leads.index.kanban.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/card-settings.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/card-settings.blade.php new file mode 100644 index 0000000..248fd5d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/card-settings.blade.php @@ -0,0 +1,120 @@ +{!! view_render_event('admin.leads.index.kanban.card_settings.before') !!} + + + + +{!! view_render_event('admin.leads.index.kanban.card_settings.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/filter.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/filter.blade.php new file mode 100644 index 0000000..d540df8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/filter.blade.php @@ -0,0 +1,1093 @@ +{!! view_render_event('admin.leads.index.kanban.filter.before') !!} + + + + +{!! view_render_event('admin.leads.index.kanban.filter.after') !!} + +@pushOnce('scripts') + + + + + + + +@endpushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/search.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/search.blade.php new file mode 100644 index 0000000..d04e1b2 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/search.blade.php @@ -0,0 +1,98 @@ +{!! view_render_event('admin.leads.index.kanban.search.before') !!} + + + + +{!! view_render_event('admin.leads.index.kanban.search.after') !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/toolbar.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/toolbar.blade.php new file mode 100644 index 0000000..900d169 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index/kanban/toolbar.blade.php @@ -0,0 +1,37 @@ +{!! view_render_event('admin.leads.index.kanban.toolbar.before') !!} + +
    +
    + {!! view_render_event('admin.leads.index.kanban.toolbar.search.before') !!} + + + @include('admin::leads.index.kanban.search') + + {!! view_render_event('admin.leads.index.kanban.toolbar.search.after') !!} + + {!! view_render_event('admin.leads.index.kanban.toolbar.filter.before') !!} + + + @include('admin::leads.index.kanban.filter') + + {!! view_render_event('admin.leads.index.kanban.toolbar.filter.after') !!} + + {!! view_render_event('admin.leads.index.kanban.toolbar.card_settings.before') !!} + + + @include('admin::leads.index.kanban.card-settings') + + {!! view_render_event('admin.leads.index.kanban.toolbar.card_settings.after') !!} + + +
    + + {!! view_render_event('admin.leads.index.kanban.toolbar.switcher.before') !!} + + + @include('admin::leads.index.view-switcher') + + {!! view_render_event('admin.leads.index.kanban.toolbar.switcher.after') !!} +
    + +{!! view_render_event('admin.leads.index.kanban.toolbar.after') !!} diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index/table.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index/table.blade.php new file mode 100644 index 0000000..7edfc7b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index/table.blade.php @@ -0,0 +1,14 @@ +{!! view_render_event('admin.leads.index.table.before') !!} + + + + + + + @include('admin::leads.index.view-switcher') + + + + + +{!! view_render_event('admin.leads.index.table.after') !!} \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index/upload.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index/upload.blade.php new file mode 100644 index 0000000..e6d598d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index/upload.blade.php @@ -0,0 +1,141 @@ + + + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/leads/index/view-switcher.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/index/view-switcher.blade.php new file mode 100644 index 0000000..24d31f1 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/index/view-switcher.blade.php @@ -0,0 +1,95 @@ +{!! view_render_event('admin.leads.index.view_switcher.before') !!} + +
    + + + {!! view_render_event('admin.leads.index.view_switcher.pipeline.button.before') !!} + + + + {!! view_render_event('admin.leads.index.view_switcher.pipeline.button.after') !!} + + + + {!! view_render_event('admin.leads.index.view_switcher.pipeline.content.header.before') !!} + + +
    + + @lang('admin::app.leads.index.view-switcher.all-pipelines') + +
    + + {!! view_render_event('admin.leads.index.view_switcher.pipeline.content.header.after') !!} + + + @foreach (app('Webkul\Lead\Repositories\PipelineRepository')->all() as $tempPipeline) + {!! view_render_event('admin.leads.index.view_switcher.pipeline.content.before', ['tempPipeline' => $tempPipeline]) !!} + + + {{ $tempPipeline->name }} + + + {!! view_render_event('admin.leads.index.view_switcher.pipeline.content.after', ['tempPipeline' => $tempPipeline]) !!} + @endforeach + + {!! view_render_event('admin.leads.index.view_switcher.pipeline.content.footer.before') !!} + + + + + @lang('admin::app.leads.index.view-switcher.create-new-pipeline') + + + + {!! view_render_event('admin.leads.index.view_switcher.pipeline.content.footer.after') !!} + +
    + +
    + {!! view_render_event('admin.leads.index.view_switcher.pipeline.view_type.before') !!} + + @if (request('view_type')) + + + + + + @else + + + + + + @endif + + {!! view_render_event('admin.leads.index.view_switcher.pipeline.view_type.after') !!} +
    +
    + +{!! view_render_event('admin.leads.index.view_switcher.after') !!} \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/leads/view.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/view.blade.php new file mode 100644 index 0000000..4f7b96b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/view.blade.php @@ -0,0 +1,144 @@ + + + @lang('admin::app.leads.view.title', ['title' => strip_tags($lead->title)]) + + + +
    + + + {!! view_render_event('admin.leads.view.left.before', ['lead' => $lead]) !!} + +
    + +
    + +
    + +
    + +
    + @if (($days = $lead->rotten_days) > 0) + @php + $lead->tags->prepend([ + 'name' => '' . trans('admin::app.leads.view.rotten-days', ['days' => $days]), + 'color' => '#FEE2E2' + ]); + @endphp + @endif + + {!! view_render_event('admin.leads.view.tags.before', ['lead' => $lead]) !!} + + + + + {!! view_render_event('admin.leads.view.tags.after', ['lead' => $lead]) !!} +
    + + + {!! view_render_event('admin.leads.view.title.before', ['lead' => $lead]) !!} + + +

    + {{ $lead->title }} +

    + + {!! view_render_event('admin.leads.view.title.after', ['lead' => $lead]) !!} + + +
    + {!! view_render_event('admin.leads.view.actions.before', ['lead' => $lead]) !!} + + @if (bouncer()->hasPermission('mail.compose')) + + + @endif + + @if (bouncer()->hasPermission('activities.create')) + + + + + + + + + @endif + + {!! view_render_event('admin.leads.view.actions.after', ['lead' => $lead]) !!} +
    +
    + + + @include ('admin::leads.view.attributes') + + + @include ('admin::leads.view.person') +
    + + {!! view_render_event('admin.leads.view.left.after', ['lead' => $lead]) !!} + + {!! view_render_event('admin.leads.view.right.before', ['lead' => $lead]) !!} + + +
    + + @include ('admin::leads.view.stages') + + + {!! view_render_event('admin.leads.view.activities.before', ['lead' => $lead]) !!} + + + + + @include ('admin::leads.view.products') + + + + + @include ('admin::leads.view.quotes') + + + + +
    + {{ $lead->description }} +
    + +
    + + {!! view_render_event('admin.leads.view.activities.after', ['lead' => $lead]) !!} +
    + + {!! view_render_event('admin.leads.view.right.after', ['lead' => $lead]) !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/leads/view/attributes.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/view/attributes.blade.php new file mode 100644 index 0000000..477b826 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/view/attributes.blade.php @@ -0,0 +1,49 @@ +{!! view_render_event('admin.leads.view.attributes.before', ['lead' => $lead]) !!} + +
    + + +
    +

    @lang('admin::app.leads.view.attributes.title')

    + + @if (bouncer()->hasPermission('leads.edit')) + + @endif +
    + + + + {!! view_render_event('admin.leads.view.attributes.form_controls.before', ['lead' => $lead]) !!} + + +
    + {!! view_render_event('admin.leads.view.attributes.form_controls.attributes.view.before', ['lead' => $lead]) !!} + + + + {!! view_render_event('admin.leads.view.attributes.form_controls.attributes.view.after', ['lead' => $lead]) !!} + +
    + + {!! view_render_event('admin.leads.view.attributes.form_controls.after', ['lead' => $lead]) !!} + +
    +
    + +{!! view_render_event('admin.leads.view.attributes.before', ['lead' => $lead]) !!} diff --git a/packages/Webkul/Admin/src/Resources/views/leads/view/person.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/view/person.blade.php new file mode 100644 index 0000000..5f385d8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/view/person.blade.php @@ -0,0 +1,357 @@ +{!! view_render_event('admin.leads.view.person.before', ['lead' => $lead]) !!} + +
    + @if ($lead?->person) + + +
    +

    @lang('admin::app.leads.view.persons.title')

    + +
    + @if (bouncer()->hasPermission('leads.edit') && bouncer()->hasPermission('contacts.persons.edit')) + $lead->person->id, 'name' => $lead->person->name])' + > + @endif +
    +
    + + + +
    + {!! view_render_event('admin.leads.view.person.avatar.before', ['lead' => $lead]) !!} + + + + + {!! view_render_event('admin.leads.view.person.avatar.after', ['lead' => $lead]) !!} + + +
    + {!! view_render_event('admin.leads.view.person.name.before', ['lead' => $lead]) !!} + + + {{ $lead->person->name }} + + + {!! view_render_event('admin.leads.view.person.name.after', ['lead' => $lead]) !!} + + {!! view_render_event('admin.leads.view.person.job_title.before', ['lead' => $lead]) !!} + + @if ($lead->person->job_title) + + @if ($lead->person->organization) + @lang('admin::app.leads.view.persons.job-title', [ + 'job_title' => $lead->person->job_title, + 'organization' => $lead->person->organization->name + ]) + @else + {{ $lead->person->job_title }} + @endif + + @endif + + {!! view_render_event('admin.leads.view.person.job_title.after', ['lead' => $lead]) !!} + + {!! view_render_event('admin.leads.view.person.email.before', ['lead' => $lead]) !!} + + @foreach ($lead->person->emails as $email) +
    + + {{ $email['value'] }} + + + + ({{ $email['label'] }}) + +
    + @endforeach + + {!! view_render_event('admin.leads.view.person.email.after', ['lead' => $lead]) !!} + + {!! view_render_event('admin.leads.view.person.contact_numbers.before', ['lead' => $lead]) !!} + + @foreach ($lead->person->contact_numbers as $contactNumber) +
    + + {{ $contactNumber['value'] }} + + + + ({{ $contactNumber['label'] }}) + +
    + @endforeach + + {!! view_render_event('admin.leads.view.person.contact_numbers.after', ['lead' => $lead]) !!} +
    +
    + +
    + @else +
    +

    @lang('admin::app.leads.view.persons.title')

    +
    + + @if (bouncer()->hasPermission('leads.edit') && bouncer()->hasPermission('contacts.persons.edit')) + + @else +

    + @lang('admin::app.leads.view.persons.no-person') +

    + @endif + @endif +
    + +{!! view_render_event('admin.leads.view.person.after', ['lead' => $lead]) !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/leads/view/products.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/view/products.blade.php new file mode 100644 index 0000000..9c1d346 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/view/products.blade.php @@ -0,0 +1,445 @@ +{!! view_render_event('admin.leads.view.products.before', ['lead' => $lead]) !!} + + + +{!! view_render_event('admin.leads.view.products.after', ['lead' => $lead]) !!} + +@pushOnce('scripts') + + + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/leads/view/quotes.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/view/quotes.blade.php new file mode 100644 index 0000000..b6cb2a1 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/view/quotes.blade.php @@ -0,0 +1,273 @@ +{!! view_render_event('admin.leads.view.quotes.before', ['lead' => $lead]) !!} + + + +{!! view_render_event('admin.leads.view.quotes.after', ['lead' => $lead]) !!} + +@pushOnce('scripts') + + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/leads/view/stages.blade.php b/packages/Webkul/Admin/src/Resources/views/leads/view/stages.blade.php new file mode 100644 index 0000000..ed2cc8b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/leads/view/stages.blade.php @@ -0,0 +1,257 @@ + +{!! view_render_event('admin.leads.view.stages.before', ['lead' => $lead]) !!} + + + + + + +{!! view_render_event('admin.leads.view.stages.after', ['lead' => $lead]) !!} + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/mail/index.blade.php b/packages/Webkul/Admin/src/Resources/views/mail/index.blade.php new file mode 100644 index 0000000..1a19679 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/mail/index.blade.php @@ -0,0 +1,712 @@ + + + {{ menu()->getLabel('mail.' . $route, 'admin::app.mail.index.' . $route) }} + + +
    +
    +
    + {!! view_render_event('admin.mail.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.mail.create.breadcrumbs.after') !!} + +
    + + {{ menu()->getLabel('mail.' . $route, 'admin::app.mail.index.' . $route) }} +
    +
    + +
    +
    + {!! view_render_event('admin.mail.create.compose_mail_btn.before') !!} + + + @if (bouncer()->hasPermission('mail.compose')) + + @endif + + {!! view_render_event('admin.mail.create.compose_mail_btn.after') !!} +
    +
    +
    + + + + + + +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/mail/view.blade.php b/packages/Webkul/Admin/src/Resources/views/mail/view.blade.php new file mode 100644 index 0000000..abb491e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/mail/view.blade.php @@ -0,0 +1,2011 @@ +@php + if (! $email->is_read) { + $email->is_read = true; + + $email->save(); + } +@endphp + + + + @lang('admin::app.mail.view.subject', ['subject' => strip_tags($email->subject)]) + + +
    +
    +
    + {!! view_render_event('admin.mail.view.form.before', ['email' => $email]) !!} + + + + + {!! view_render_event('admin.mail.view.form.after', ['email' => $email]) !!} + + +
    +
    + @lang('admin::app.mail.view.title') +
    + + {{ ucfirst($route) }} + + {!! view_render_event('admin.mail.view.tags.before', ['email' => $email]) !!} + + + + {!! view_render_event('admin.mail.view.tags.after', ['email' => $email]) !!} +
    +
    +
    + + {!! view_render_event('admin.mail.view.email-list.before', ['email' => $email]) !!} + + + + + + + {!! view_render_event('admin.mail.view.email-list.before', ['email' => $email]) !!} +
    + + @pushOnce('scripts') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/partials/breadcrumbs.blade.php b/packages/Webkul/Admin/src/Resources/views/partials/breadcrumbs.blade.php new file mode 100644 index 0000000..05eeb2b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/partials/breadcrumbs.blade.php @@ -0,0 +1,27 @@ +@unless ($breadcrumbs->isEmpty()) + +@endunless diff --git a/packages/Webkul/Admin/src/Resources/views/products/create.blade.php b/packages/Webkul/Admin/src/Resources/views/products/create.blade.php new file mode 100644 index 0000000..01d2166 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/products/create.blade.php @@ -0,0 +1,107 @@ + + + + + @lang('admin::app.products.create.title') + + + {!! view_render_event('admin.products.create.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.products.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.products.create.breadcrumbs.after') !!} + +
    + @lang('admin::app.products.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.products.create.save_button.before') !!} + + + @if (bouncer()->hasPermission('settings.user.groups.create')) + + @endif + + {!! view_render_event('admin.products.create.save_button.after') !!} +
    +
    +
    + +
    + +
    +
    +

    + @lang('admin::app.products.create.general') +

    + + {!! view_render_event('admin.products.create.attributes.before') !!} + + + + {!! view_render_event('admin.products.create.attributes.after') !!} +
    +
    + + +
    + {!! view_render_event('admin.products.create.accordion.before') !!} + + + + {!! view_render_event('admin.products.create.accordion.header.before') !!} + +
    +

    + @lang('admin::app.products.create.price') +

    +
    + + {!! view_render_event('admin.products.create.accordion.header.after') !!} + + + + {!! view_render_event('admin.products.create.accordion.content.attributes.before') !!} + + + + {!! view_render_event('admin.products.create.accordion.content.attributes.after') !!} + +
    + + {!! view_render_event('admin.products.create.accordion.before') !!} +
    +
    +
    +
    + + {!! view_render_event('admin.products.create.form.after') !!} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/products/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/products/edit.blade.php new file mode 100644 index 0000000..5ad475a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/products/edit.blade.php @@ -0,0 +1,107 @@ + + + + + @lang('admin::app.products.edit.title') + + + {!! view_render_event('admin.products.edit.form.before') !!} + + +
    +
    +
    + + + +
    + @lang('admin::app.products.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.products.edit.create_button.before', ['product' => $product]) !!} + + + + + {!! view_render_event('admin.products.edit.create_button.after', ['product' => $product]) !!} +
    +
    +
    + +
    + +
    +
    +

    + @lang('admin::app.products.create.general') +

    + + {!! view_render_event('admin.products.edit.attributes.before', ['product' => $product]) !!} + + + + {!! view_render_event('admin.products.edit.attributes.after', ['product' => $product]) !!} +
    +
    + + +
    + {!! view_render_event('admin.products.edit.accordion.before', ['product' => $product]) !!} + + + + {!! view_render_event('admin.products.edit.accordion.header.before', ['product' => $product]) !!} + +
    +

    + @lang('admin::app.products.create.price') +

    +
    + + {!! view_render_event('admin.products.edit.accordion.header.after', ['product' => $product]) !!} + + + + {!! view_render_event('admin.products.edit.accordion.content.attributes.before', ['product' => $product]) !!} + + + + {!! view_render_event('admin.products.edit.accordion.content.attributes.after', ['product' => $product]) !!} + +
    + + {!! view_render_event('admin.products.edit.accordion.after', ['product' => $product]) !!} +
    +
    +
    +
    + + {!! view_render_event('admin.products.edit.form.after') !!} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/products/index.blade.php b/packages/Webkul/Admin/src/Resources/views/products/index.blade.php new file mode 100644 index 0000000..8aadef8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/products/index.blade.php @@ -0,0 +1,45 @@ + + + {{ menu()->getLabel('products', 'admin::app.products.index.title') }} + + +
    +
    +
    + + + +
    + {{ menu()->getLabel('products', 'admin::app.products.index.title') }} +
    +
    + +
    + {!! view_render_event('admin.products.index.create_button.before') !!} + + + @if (bouncer()->hasPermission('products.create')) + + @endif + + {!! view_render_event('admin.products.index.create_button.after') !!} +
    +
    + + {!! view_render_event('admin.products.index.datagrid.before') !!} + + + + + + + {!! view_render_event('admin.products.index.datagrid.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/products/view.blade.php b/packages/Webkul/Admin/src/Resources/views/products/view.blade.php new file mode 100644 index 0000000..6cf868d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/products/view.blade.php @@ -0,0 +1,112 @@ + + + {{ strip_tags($product->name) }} + + + +
    + + {!! view_render_event('admin.products.view.left.before', ['product' => $product]) !!} + +
    + +
    + +
    + +
    + + {!! view_render_event('admin.products.view.left.tags.before', ['product' => $product]) !!} + + + + + {!! view_render_event('admin.products.view.left.tags.after', ['product' => $product]) !!} + + +
    + {!! view_render_event('admin.products.view.left.title.before', ['product' => $product]) !!} + +

    + {{ $product->name }} +

    + + {!! view_render_event('admin.products.view.left.title.after', ['product' => $product]) !!} + + {!! view_render_event('admin.products.view.left.sku.before', ['product' => $product]) !!} + +

    + @lang('admin::app.products.view.sku') : {{ $product->sku }} +

    + + {!! view_render_event('admin.products.view.left.sku.after', ['product' => $product]) !!} +
    + + {!! view_render_event('admin.products.view.left.activity_actions.before', ['product' => $product]) !!} + + +
    + {!! view_render_event('admin.products.view.left.activity_actions.note.before', ['product' => $product]) !!} + + + + + {!! view_render_event('admin.products.view.left.activity_actions.note.after', ['product' => $product]) !!} + + {!! view_render_event('admin.products.view.left.activity_actions.file.before', ['product' => $product]) !!} + + + + + {!! view_render_event('admin.products.view.left.activity_actions.file.after', ['product' => $product]) !!} +
    + + {!! view_render_event('admin.products.view.left.activity_actions.after', ['product' => $product]) !!} +
    + + + @include ('admin::products.view.attributes') +
    + + {!! view_render_event('admin.products.view.left.after', ['product' => $product]) !!} + + {!! view_render_event('admin.products.view.right.before', ['product' => $product]) !!} + + +
    + {!! view_render_event('admin.products.view.right.activities.before', ['product' => $product]) !!} + + + + + @include('admin::products.view.inventory') + + + + {!! view_render_event('admin.products.view.right.activities.after', ['product' => $product]) !!} +
    + + {!! view_render_event('admin.products.view.right.after', ['product' => $product]) !!} +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/products/view/attributes.blade.php b/packages/Webkul/Admin/src/Resources/views/products/view/attributes.blade.php new file mode 100644 index 0000000..35fbd47 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/products/view/attributes.blade.php @@ -0,0 +1,44 @@ +{!! view_render_event('admin.products.view.attributes.before', ['product' => $product]) !!} + +
    + + +

    + @lang('admin::app.products.view.attributes.about-product') +

    + + + + {!! view_render_event('admin.products.view.attributes.view.before', ['product' => $product]) !!} + + +
    + + + + + +
    + + {!! view_render_event('admin.products.view.attributes.view.after', ['product' => $product]) !!} + +
    +
    + +{!! view_render_event('admin.products.view.attributes.before', ['product' => $product]) !!} \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/products/view/inventory.blade.php b/packages/Webkul/Admin/src/Resources/views/products/view/inventory.blade.php new file mode 100644 index 0000000..eb8cdf3 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/products/view/inventory.blade.php @@ -0,0 +1,505 @@ +{!! view_render_event('admin.products.view.inventory.before', ['product' => $product]) !!} + + + + +{!! view_render_event('admin.products.view.inventory.after', ['product' => $product]) !!} + +@pushOnce('scripts') + + + + + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/quotes/create.blade.php b/packages/Webkul/Admin/src/Resources/views/quotes/create.blade.php new file mode 100644 index 0000000..92415c6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/quotes/create.blade.php @@ -0,0 +1,943 @@ + + + @lang('admin::app.quotes.create.title') + + + {!! view_render_event('admin.contacts.quotes.create.form_controls.before') !!} + + +
    +
    +
    + + +
    + @lang('admin::app.quotes.create.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.contacts.quotes.create.save_button.before') !!} + + + + {!! view_render_event('admin.contacts.quotes.create.save_button.after') !!} +
    +
    +
    + + + + +
    +
    + + {!! view_render_event('admin.contacts.quotes.create.form_controls.after') !!} + + @pushOnce('scripts') + + + + + + + + @endPushOnce + + @pushOnce('styles') + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/quotes/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/quotes/edit.blade.php new file mode 100644 index 0000000..f66e471 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/quotes/edit.blade.php @@ -0,0 +1,940 @@ + + + @lang('admin::app.quotes.edit.title') + + + {!! view_render_event('admin.contacts.quotes.edit.form_controls.before', ['quote' => $quote]) !!} + + +
    +
    +
    + + +
    + @lang('admin::app.quotes.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.contacts.quotes.edit.save_button.before', ['quote' => $quote]) !!} + + + + + {!! view_render_event('admin.contacts.quotes.edit.save_button.after', ['quote' => $quote]) !!} +
    +
    +
    + + + + +
    +
    + + {!! view_render_event('admin.contacts.quotes.edit.form_controls.after', ['quote' => $quote]) !!} + + @pushOnce('scripts') + + + + + + + + @endPushOnce + + @pushOnce('styles') + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/quotes/index.blade.php b/packages/Webkul/Admin/src/Resources/views/quotes/index.blade.php new file mode 100644 index 0000000..66c8525 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/quotes/index.blade.php @@ -0,0 +1,88 @@ + + + {{ menu()->getLabel('quotes', 'admin::app.quotes.index.title') }} + + + +
    +
    +
    + + + +
    + {{ menu()->getLabel('quotes', 'admin::app.quotes.index.title') }} +
    +
    + +
    + +
    + @if (bouncer()->hasPermission('quotes.create')) + + @lang('admin::app.quotes.index.create-btn') + + @endif +
    +
    +
    + + + +
    +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/quotes/pdf.blade.php b/packages/Webkul/Admin/src/Resources/views/quotes/pdf.blade.php new file mode 100755 index 0000000..c82ce17 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/quotes/pdf.blade.php @@ -0,0 +1,430 @@ + + + + + + + + + @php + if ($locale == 'en') { + $fontFamily = [ + 'regular' => 'DejaVu Sans', + 'bold' => 'DejaVu Sans', + ]; + } else { + $fontFamily = [ + 'regular' => 'Arial, sans-serif', + 'bold' => 'Arial, sans-serif', + ]; + } + + if (in_array($locale, ['ar', 'fa', 'tr'])) { + $fontFamily = [ + 'regular' => 'DejaVu Sans', + 'bold' => 'DejaVu Sans', + ]; + } + @endphp + + + + + + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + @lang('admin::app.quotes.index.pdf.quote-id'): + + + + #{{ $quote->id }} + + + + @lang('admin::app.quotes.index.pdf.person'): + + + + {{ $quote->person->name }} + +
    + + @lang('admin::app.quotes.index.pdf.sales-person'): + + + + {{ $quote->user->name }} + + + + @lang('admin::app.quotes.index.pdf.subject'): + + + + {{ $quote->subject }} + +
    + + @lang('admin::app.quotes.index.pdf.date'): + + + + {{ core()->formatDate($quote->created_at, 'd-m-Y') }} + + + + @lang('admin::app.quotes.index.pdf.sales-person'): + + + + {{ $quote->user->name }} + +
    + + @lang('admin::app.quotes.index.pdf.expired-at'): + + + + {{ core()->formatDate($quote->expired_at, 'd-m-Y') }} + +
    + + + + + + @if ($quote->billing_address) + + @endif + + @if ($quote->shipping_address) + + @endif + + + + + + @if ($quote->billing_address) + + @endif + + @if ($quote->shipping_address) + + @endif + + +
    + + @lang('admin::app.quotes.index.pdf.billing-address') + + + + @lang('admin::app.quotes.index.pdf.shipping-address') + +
    +
    {{ $quote->billing_address['address'] ?? '' }}
    + +
    {{ $quote->billing_address['postcode'] ?? '' . ' ' .$quote->billing_address['city'] ?? '' }}
    + +
    {{ $quote->billing_address['state'] ?? '' }}
    + +
    {{ core()->country_name($quote->billing_address['country'] ?? '') }}
    +
    +
    {{ $quote->shipping_address['address'] ?? ''}}
    + +
    {{ $quote->shipping_address['postcode'] ?? '' . ' ' .$quote->shipping_address['city'] ?? '' }}
    + +
    {{ $quote->shipping_address['state'] ?? '' }}
    + +
    {{ core()->country_name($quote->shipping_address['country'] ?? '') }}
    +
    + + + @if ($quote->description) + + + + + + + + + + + +
    + + @lang('admin::app.quotes.index.pdf.description') + +
    + {{ $quote->description }} +
    + @endif + + +
    + + + + + + + + + + + + + + + + + + + + + + + @foreach ($quote->items as $item) + + + + + + + + + + + + + + + + + + @endforeach + +
    + @lang('admin::app.quotes.index.pdf.sku') + + @lang('admin::app.quotes.index.pdf.product-name') + + @lang('admin::app.quotes.index.pdf.price') + + @lang('admin::app.quotes.index.pdf.quantity') + + @lang('admin::app.quotes.index.pdf.amount') + + @lang('admin::app.quotes.index.pdf.discount') + + @lang('admin::app.quotes.index.pdf.tax') + + @lang('admin::app.quotes.index.pdf.grand-total') +
    {{ $item->sku }} + {{ $item->name }} + {!! core()->formatBasePrice($item->price, true) !!}{{ $item->quantity }}{!! core()->formatBasePrice($item->total, true) !!}{!! core()->formatBasePrice($item->discount_amount, true) !!}{!! core()->formatBasePrice($item->tax_amount, true) !!}{!! core()->formatBasePrice($item->total + $item->tax_amount - $item->discount_amount, true) !!}
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @lang('admin::app.quotes.index.pdf.sub-total')-{!! core()->formatBasePrice($quote->sub_total, true) !!}
    @lang('admin::app.quotes.index.pdf.tax')-{!! core()->formatBasePrice($quote->tax_amount, true) !!}
    @lang('admin::app.quotes.index.pdf.discount')-{!! core()->formatBasePrice($quote->discount_amount, true) !!}
    @lang('admin::app.quotes.index.pdf.adjustment')-{!! core()->formatBasePrice($quote->adjustment_amount, true) !!}
    @lang('admin::app.quotes.index.pdf.grand-total')-{!! core()->formatBasePrice($quote->grand_total, true) !!}
    +
    +
    +
    + + diff --git a/packages/Webkul/Admin/src/Resources/views/sessions/forgot-password.blade.php b/packages/Webkul/Admin/src/Resources/views/sessions/forgot-password.blade.php new file mode 100644 index 0000000..5bbfdfc --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/sessions/forgot-password.blade.php @@ -0,0 +1,86 @@ + + + + @lang('admin::app.users.forget-password.create.page-title') + + +
    +
    + + @if ($logo = core()->getConfigData('general.general.admin_logo.logo_image')) + {{ config('app.name') }} + @else + {{ config('app.name') }} + @endif + +
    + {!! view_render_event('admin.sessions.forgor_password.form_controls.before') !!} + + + +
    +

    + @lang('admin::app.users.forget-password.create.title') +

    +
    + +
    + + + + @lang('admin::app.users.forget-password.create.email') + + + + + + +
    + +
    + + + @lang('admin::app.users.forget-password.create.sign-in-link') + + + + +
    +
    + + {!! view_render_event('admin.sessions.forgor_password.form_controls.after') !!} +
    +
    + + +
    + @lang('admin::app.components.layouts.powered-by.description', [ + 'krayin' => 'Krayin', + 'webkul' => 'Webkul', + ]) +
    +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/sessions/login.blade.php b/packages/Webkul/Admin/src/Resources/views/sessions/login.blade.php new file mode 100644 index 0000000..1b2fb3e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/sessions/login.blade.php @@ -0,0 +1,125 @@ + + + + @lang('admin::app.users.login.title') + + +
    +
    + + @if ($logo = core()->getConfigData('general.general.admin_logo.logo_image')) + {{ config('app.name') }} + @else + {{ config('app.name') }} + @endif + +
    + {!! view_render_event('admin.sessions.login.form_controls.before') !!} + + + +

    + @lang('admin::app.users.login.title') +

    + +
    + + + + @lang('admin::app.users.login.email') + + + + + + + + + + + @lang('admin::app.users.login.password') + + + + + + + + + +
    + +
    + + + @lang('admin::app.users.login.forget-password-link') + + + + +
    +
    + + {!! view_render_event('admin.sessions.login.form_controls.after') !!} +
    +
    + + +
    + @lang('admin::app.components.layouts.powered-by.description', [ + 'krayin' => 'Krayin', + 'webkul' => 'Webkul', + ]) +
    +
    + + @push('scripts') + + @endpush +
    diff --git a/packages/Webkul/Admin/src/Resources/views/sessions/reset-password.blade.php b/packages/Webkul/Admin/src/Resources/views/sessions/reset-password.blade.php new file mode 100644 index 0000000..10852c4 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/sessions/reset-password.blade.php @@ -0,0 +1,131 @@ + + + + @lang('admin::app.users.reset-password.title') + + +
    +
    + + @if ($logo = core()->getConfigData('general.general.admin_logo.logo_image')) + {{ config('app.name') }} + @else + {{ config('app.name') }} + @endif + +
    + {!! view_render_event('admin.sessions.reset-password.form_controls.before') !!} + + + +
    +

    + @lang('admin::app.users.reset-password.title') +

    +
    + + + +
    + + + + @lang('admin::app.users.reset-password.email') + + + + + + + + + + + @lang('admin::app.users.reset-password.password') + + + + + + + + + + + @lang('admin::app.users.reset-password.confirm-password') + + + + + + +
    + +
    + + + @lang('admin::app.users.reset-password.back-link-title') + + + + +
    +
    + + {!! view_render_event('admin.sessions.reset-password.form_controls.after') !!} +
    +
    + + +
    + @lang('admin::app.components.layouts.powered-by.description', [ + 'krayin' => 'Krayin', + 'webkul' => 'Webkul', + ]) +
    +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/attributes/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/attributes/create.blade.php new file mode 100644 index 0000000..c7dd679 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/attributes/create.blade.php @@ -0,0 +1,688 @@ + + + @lang('admin::app.settings.attributes.create.title') + + + {!! view_render_event('admin.settings.attributes.create.before') !!} + + + +
    + {!! view_render_event('admin.settings.attributes.create.form_controls.before') !!} + + +
    +
    + {!! view_render_event('admin.settings.attributes.create.breadcrumbs.before') !!} + + + + {!! view_render_event('admin.settings.attributes.create.breadcrumbs.after') !!} + +
    + {!! view_render_event('admin.settings.attributes.create.title.before') !!} + + @lang('admin::app.settings.attributes.create.title') + + {!! view_render_event('admin.settings.attributes.create.title.after') !!} +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.attributes.create.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.automation.attributes.create')) + + @endif + + {!! view_render_event('admin.settings.attributes.create.create_button.after') !!} +
    +
    +
    + + + + + + + + {!! view_render_event('admin.settings.attributes.create.form_controls.after') !!} +
    +
    + + {!! view_render_event('admin.settings.attributes.create.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/attributes/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/attributes/edit.blade.php new file mode 100644 index 0000000..693fc18 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/attributes/edit.blade.php @@ -0,0 +1,765 @@ + + + @lang('admin::app.settings.attributes.edit.title') + + + {!! view_render_event('admin.catalog.attributes.edit.before', ['attribute' => $attribute]) !!} + + + +
    + {!! view_render_event('admin.settings.attributes.edit.form_controls.before', ['attribute' => $attribute]) !!} + + +
    +
    + {!! view_render_event('admin.settings.attributes.edit.breadcrumbs.before', ['attribute' => $attribute]) !!} + + + + {!! view_render_event('admin.settings.attributes.edit.breadcrumbs.after', ['attribute' => $attribute]) !!} + +
    + {!! view_render_event('admin.settings.attributes.edit.title.before', ['attribute' => $attribute]) !!} + + @lang('admin::app.settings.attributes.edit.title') + + {!! view_render_event('admin.settings.attributes.edit.title.after', ['attribute' => $attribute]) !!} +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.attributes.edit.edit_button.before', ['attribute' => $attribute]) !!} + + @if (bouncer()->hasPermission('settings.automation.attributes.edit')) + + @endif + + {!! view_render_event('admin.settings.attributes.edit.edit_button.after', ['attribute' => $attribute]) !!} +
    +
    +
    + + + + + + + + {!! view_render_event('admin.settings.attributes.edit.form_controls.after', ['attribute' => $attribute]) !!} +
    +
    + + {!! view_render_event('admin.catalog.attributes.edit.after', ['attribute' => $attribute]) !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/attributes/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/attributes/index.blade.php new file mode 100644 index 0000000..b58b2b5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/attributes/index.blade.php @@ -0,0 +1,51 @@ + + + @lang('admin::app.settings.attributes.index.title') + + +
    +
    +
    + {!! view_render_event('admin.settings.attributes.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.attributes.index.breadcrumbs.after') !!} + +
    + + @lang('admin::app.settings.attributes.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.attributes.index.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.automation.attributes.create')) + + @lang('admin::app.settings.attributes.index.create-btn') + + @endif + + {!! view_render_event('admin.settings.attributes.index.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.settings.attributes.index.datagrid.before') !!} + + + + + + + + {!! view_render_event('admin.settings.attributes.index.datagrid.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/create.blade.php new file mode 100644 index 0000000..f469ac3 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/create.blade.php @@ -0,0 +1,232 @@ + + + + @lang('admin::app.settings.data-transfer.imports.create.title') + + + {!! view_render_event('admin.settings.data_transfer.imports.create.before') !!} + + + {!! view_render_event('admin.settings.data_transfer.imports.create.create_form_controls.before') !!} + + +
    +
    + {!! view_render_event('admin.settings.data_transfers.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.data_transfers.create.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.data-transfer.imports.create.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.data_transfers.create.save_button.before') !!} + + @if (bouncer()->hasPermission('settings.automation.data_transfer.imports.create')) + + + @endif + + {!! view_render_event('admin.settings.data_transfers.create.save_button.after') !!} +
    +
    +
    + + +
    + +
    + {!! view_render_event('admin.settings.data_transfer.imports.create.card.general.before') !!} + + +
    +

    + @lang('admin::app.settings.data-transfer.imports.create.general') +

    + + + + + @lang('admin::app.settings.data-transfer.imports.create.type') + + + + @foreach (config('importers') as $code => $importer) + + @endforeach + + + + + @lang('admin::app.settings.data-transfer.imports.create.download-sample') + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.create.file') + + + + + + +
    + + {!! view_render_event('admin.settings.data_transfer.imports.create.card.general.after') !!} +
    + + +
    + {!! view_render_event('admin.settings.data_transfer.imports.create.card.accordion.settings.before') !!} + + + + +
    +

    + @lang('admin::app.settings.data-transfer.imports.create.settings') +

    +
    + + + + + + + @lang('admin::app.settings.data-transfer.imports.create.action') + + + + + + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.create.validation-strategy') + + + + + + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.create.allowed-errors') + + + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.create.field-separator') + + + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.create.process-in-queue') + + + + + + + +
    + + {!! view_render_event('admin.settings.data_transfer.imports.create.card.accordion.settings.after') !!} +
    +
    + + {!! view_render_event('admin.settings.data_transfer.imports.create.create_form_controls.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/edit.blade.php new file mode 100644 index 0000000..06590e0 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/edit.blade.php @@ -0,0 +1,251 @@ + + + + @lang('admin::app.settings.data-transfer.imports.edit.title') + + + {!! view_render_event('admin.settings.data_transfer.imports.edit.before', ['import' => $import]) !!} + + + {!! view_render_event('admin.settings.data_transfer.imports.edit.edit_form_controls.before', ['import' => $import]) !!} + + +
    +
    + {!! view_render_event('admin.settings.data_transfers.edit.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.data_transfers.edit.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.data-transfer.imports.edit.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.data_transfers.edit.save_button.before') !!} + + @if (bouncer()->hasPermission('settings.automation.data_transfer.imports.edit')) + + + @endif + + {!! view_render_event('admin.settings.data_transfers.edit.save_button.after') !!} +
    +
    +
    + + +
    + +
    + {!! view_render_event('admin.settings.data_transfer.imports.edit.card.general.before', ['import' => $import]) !!} + + +
    +

    + @lang('admin::app.settings.data-transfer.imports.edit.general') +

    + + + + + @lang('admin::app.settings.data-transfer.imports.edit.type') + + +
    + + @foreach (config('importers') as $code => $importer) + + @endforeach + + + + + @lang('admin::app.settings.data-transfer.imports.edit.download-sample') + +
    + + +
    + + + + + @lang('admin::app.settings.data-transfer.imports.edit.file') + + +
    + + + + + @if ($import?->file_path) + + + {{ $import?->file_name }} + + @endif +
    +
    +
    + + {!! view_render_event('admin.settings.data_transfer.imports.edit.card.general.after', ['import' => $import]) !!} +
    + + +
    + {!! view_render_event('admin.settings.data_transfer.imports.edit.card.accordion.settings.before', ['import' => $import]) !!} + + + + +
    +

    + @lang('admin::app.settings.data-transfer.imports.edit.settings') +

    +
    + + + + + + + @lang('admin::app.settings.data-transfer.imports.edit.action') + + + + + + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.edit.validation-strategy') + + + + + + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.edit.allowed-errors') + + + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.edit.field-separator') + + + + + + + + + + + @lang('admin::app.settings.data-transfer.imports.edit.process-in-queue') + + + + + + + +
    + + {!! view_render_event('admin.settings.data_transfer.imports.edit.card.accordion.settings.after', ['import' => $import]) !!} +
    +
    + + {!! view_render_event('admin.settings.data_transfer.imports.edit.edit_form_controls.after', ['import' => $import]) !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/import.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/import.blade.php new file mode 100644 index 0000000..ef9d784 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/import.blade.php @@ -0,0 +1,561 @@ + + + @lang('admin::app.settings.data-transfer.imports.import.title') + + + +
    +
    + {!! view_render_event('admin.settings.data_transfers.import.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.data_transfers.import.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.data-transfer.imports.import.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.data_transfers.import.edit_button.before') !!} + + + @if (bouncer()->hasPermission('settings.automation.data_transfer.imports.edit')) + + @lang('admin::app.settings.data-transfer.imports.import.edit-btn') + + @endif + + {!! view_render_event('admin.settings.data_transfers.import.edit_button.after') !!} +
    +
    +
    + + + + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/index.blade.php new file mode 100644 index 0000000..b36794f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/data-transfer/imports/index.blade.php @@ -0,0 +1,50 @@ + + + @lang('admin::app.settings.data-transfer.imports.index.title') + + +
    +
    +
    + {!! view_render_event('admin.settings.data_transfers.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.data_transfers.index.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.data-transfer.imports.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.data_transfers.index.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.automation.data_transfer.imports.create')) + + @lang('admin::app.settings.data-transfer.imports.index.button-title') + + @endif + + {!! view_render_event('admin.settings.data_transfers.index.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.settings.data_transfers.index.datagrid.before') !!} + + + + + + + + {!! view_render_event('admin.settings.data_transfers.index.datagrid.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/email-templates/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/email-templates/create.blade.php new file mode 100644 index 0000000..6db98d6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/email-templates/create.blade.php @@ -0,0 +1,234 @@ + + + + + @lang('admin::app.settings.email-template.create.title') + + + {!! view_render_event('admin.settings.email_template.create.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.email_template.create.breadcrumbs.after') !!} + + + + + {!! view_render_event('admin.settings.email_template.create.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.email-template.create.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.email_template.create.save_button.before') !!} + + + + {!! view_render_event('admin.settings.email_template.create.save_button.after') !!} +
    +
    +
    + + +
    +
    + + {!! view_render_event('admin.email_template.create.form.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/email-templates/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/email-templates/edit.blade.php new file mode 100644 index 0000000..798a1c5 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/email-templates/edit.blade.php @@ -0,0 +1,238 @@ + + + + + @lang('admin::app.settings.email-template.edit.title') + + + {!! view_render_event('admin.settings.email_template.edit.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.email_template.edit.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.email_template.edit.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.email-template.edit.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.email_template.edit.save_button.before') !!} + + + + {!! view_render_event('admin.settings.email_template.edit.save_button.before') !!} +
    +
    +
    + + +
    +
    + + {!! view_render_event('admin.settings.email_template.edit.form.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/email-templates/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/email-templates/index.blade.php new file mode 100644 index 0000000..06552f8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/email-templates/index.blade.php @@ -0,0 +1,51 @@ + + + + @lang('admin::app.settings.email-template.index.title') + + +
    +
    +
    + {!! view_render_event('admin.settings.email_template.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.email_template.index.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.email-template.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.email_template.index.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.automation.email_templates.create')) + + @lang('admin::app.settings.email-template.index.create-btn') + + @endif + + {!! view_render_event('admin.settings.email_template.index.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.settings.email_template.index.datagrid.before') !!} + + + + + + + + {!! view_render_event('admin.settings.email_template.index.datagrid.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/google-contacts/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/google-contacts/index.blade.php new file mode 100644 index 0000000..2fffb54 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/google-contacts/index.blade.php @@ -0,0 +1,50 @@ + + + @lang('admin::app.settings.google-contacts.index.title') + + +
    +
    +
    + + +
    + @lang('admin::app.settings.google-contacts.index.title') +
    +
    +
    + +
    + @if ($account) +

    + {{ trans('admin::app.settings.google-contacts.index.connected-info', ['email' => $account->google_email]) }} +

    + +
    + @csrf + + +
    + @else +

    + @lang('admin::app.settings.google-contacts.index.not-connected-info') +

    + + + @lang('admin::app.settings.google-contacts.index.connect-btn') + + @endif +
    +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/groups/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/groups/index.blade.php new file mode 100644 index 0000000..45d112a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/groups/index.blade.php @@ -0,0 +1,352 @@ + + + + @lang('admin::app.settings.groups.index.title') + + +
    + +
    +
    + {!! view_render_event('admin.settings.groups.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.groups.index.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.groups.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.groups.index.breadcrumbs.after') !!} + + @if (bouncer()->hasPermission('settings.user.groups.create')) + + @endif + + {!! view_render_event('admin.settings.groups.index.create_button.after') !!} +
    +
    +
    + + + + + +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/index.blade.php new file mode 100644 index 0000000..9dc4727 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/index.blade.php @@ -0,0 +1,58 @@ + + + + @lang('admin::app.settings.title') + + + + + +
    +

    + @lang('admin::app.settings.title') +

    +
    + + +
    + @foreach (menu()->getAdminMenuByKey('settings')->getChildren() as $setting) +
    +
    + +

    + {{ $setting->getName() }} +

    + + +

    + {{ $setting->getInfo() }} +

    +
    + +
    + + @foreach ($setting->getChildren() as $key => $child) + +
    + +
    + +
    +

    + {{ $child->getName() }} +

    + +

    + {{ $child->getInfo() }} +

    +
    +
    + @endforeach +
    +
    + @endforeach +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/marketing/campaigns/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/marketing/campaigns/index.blade.php new file mode 100644 index 0000000..35897c6 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/marketing/campaigns/index.blade.php @@ -0,0 +1,525 @@ + + + + @lang('admin::app.settings.marketing.campaigns.index.title') + + +
    + +
    +
    +
    + {!! view_render_event('admin.settings.marketing.campaigns.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.marketing.campaigns.index.breadcrumbs.after') !!} +
    + +
    + @lang('admin::app.settings.marketing.campaigns.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.marketing.campaigns.index.breadcrumbs.after') !!} + + @if (bouncer()->hasPermission('settings.automation.campaigns.create')) + + @endif + + {!! view_render_event('admin.settings.marketing.campaigns.index.create_button.after') !!} +
    +
    +
    + + + + +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/marketing/events/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/marketing/events/index.blade.php new file mode 100644 index 0000000..88e57d8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/marketing/events/index.blade.php @@ -0,0 +1,410 @@ + + + + @lang('admin::app.settings.marketing.events.index.title') + + +
    + +
    +
    + {!! view_render_event('admin.settings.marketing.events.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.marketing.events.index.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.marketing.events.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.marketing.events.index.breadcrumbs.after') !!} + + @if (bouncer()->hasPermission('settings.automation.events.create')) + + @endif + + {!! view_render_event('admin.settings.marketing.events.index.create_button.after') !!} +
    +
    +
    + + + + +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/pipelines/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/pipelines/create.blade.php new file mode 100644 index 0000000..980332a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/pipelines/create.blade.php @@ -0,0 +1,427 @@ + + + + @lang('admin::app.settings.pipelines.create.title') + + + {!! view_render_event('admin.settings.pipelines.create.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.pipelines.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.pipelines.create.breadcrumbs.after') !!} + + +
    + @lang('admin::app.settings.pipelines.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.pipelines.create.save_button.before') !!} + + + + + {!! view_render_event('admin.settings.pipelines.create.save_button.after') !!} +
    +
    +
    + +
    + {!! view_render_event('admin.settings.pipelines.create.form.name.before') !!} + + + + + @lang('admin::app.settings.pipelines.create.name') + + + + + + + + {!! view_render_event('admin.settings.pipelines.create.form.name.after') !!} + + {!! view_render_event('admin.settings.pipelines.create.form.rotten_days.before') !!} + + + + + @lang('admin::app.settings.pipelines.create.rotten-days') + + + + + + + + {!! view_render_event('admin.settings.pipelines.create.form.rotten_days.after') !!} + + {!! view_render_event('admin.settings.pipelines.create.form.is_default.before') !!} + + + + + @lang('admin::app.settings.pipelines.create.mark-as-default') + + + + + + + + {!! view_render_event('admin.settings.pipelines.create.form.is_default.after') !!} +
    +
    + + +
    + {!! view_render_event('admin.settings.pipelines.create.form.stages.before') !!} + + + + + + {!! view_render_event('admin.settings.pipelines.create.form.stages.after') !!} +
    +
    + + {!! view_render_event('admin.settings.pipelines.create.form.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/settings/pipelines/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/pipelines/edit.blade.php new file mode 100644 index 0000000..3cef0cf --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/pipelines/edit.blade.php @@ -0,0 +1,389 @@ + + + + @lang('admin::app.settings.pipelines.edit.title') + + + {!! view_render_event('admin.settings.pipelines.edit.form.before', ['pipeline' => $pipeline]) !!} + + + +
    +
    +
    + {!! view_render_event('admin.settings.pipelines.edit.breadcrumbs.before', ['pipeline' => $pipeline]) !!} + + + + + {!! view_render_event('admin.settings.pipelines.edit.breadcrumbs.after', ['pipeline' => $pipeline]) !!} + + +
    + @lang('admin::app.settings.pipelines.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.pipelines.edit.save_button.before', ['pipeline' => $pipeline]) !!} + + + + + {!! view_render_event('admin.settings.pipelines.edit.save_button.after', ['pipeline' => $pipeline]) !!} +
    +
    +
    + +
    + {!! view_render_event('admin.settings.pipelines.edit.form.name.before', ['pipeline' => $pipeline]) !!} + + + + + @lang('admin::app.settings.pipelines.edit.name') + + + + + + + + {!! view_render_event('admin.settings.pipelines.edit.form.name.after', ['pipeline' => $pipeline]) !!} + + {!! view_render_event('admin.settings.pipelines.edit.form.rotten_days.before', ['pipeline' => $pipeline]) !!} + + + + + @lang('admin::app.settings.pipelines.edit.rotten-days') + + + + + + + + {!! view_render_event('admin.settings.pipelines.edit.form.rotten_days.after', ['pipeline' => $pipeline]) !!} + + {!! view_render_event('admin.settings.pipelines.edit.form.is_default.before', ['pipeline' => $pipeline]) !!} + + + + + @lang('admin::app.settings.pipelines.edit.mark-as-default') + + + + + + + + {!! view_render_event('admin.settings.pipelines.edit.form.is_default.after', ['pipeline' => $pipeline]) !!} +
    +
    + +
    + + + + +
    +
    + + {!! view_render_event('admin.settings.pipelines.edit.form.after', ['pipeline' => $pipeline]) !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/settings/pipelines/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/pipelines/index.blade.php new file mode 100644 index 0000000..172707e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/pipelines/index.blade.php @@ -0,0 +1,52 @@ + + + + @lang('admin::app.settings.pipelines.index.title') + + +
    +
    +
    + {!! view_render_event('admin.settings.pipelines.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.pipelines.index.breadcrumbs.after') !!} + +
    + + @lang('admin::app.settings.pipelines.index.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.pipelines.index.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.lead.pipelines.create')) + + + @lang('admin::app.settings.pipelines.index.create-btn') + + @endif + + {!! view_render_event('admin.settings.pipelines.index.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.settings.pipelines.index.datagrid.before') !!} + + + + + + + + {!! view_render_event('admin.settings.pipelines.index.datagrid.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/roles/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/roles/create.blade.php new file mode 100644 index 0000000..c784a4c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/roles/create.blade.php @@ -0,0 +1,214 @@ + + + + @lang('admin::app.settings.roles.create.title') + + + {!! view_render_event('admin.settings.roles.create.form.before') !!} + + + +
    +
    +
    + {!! view_render_event('admin.settings.roles.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.roles.create.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.roles.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.roles.create.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.user.roles.create')) + + + @endif + + {!! view_render_event('admin.settings.roles.create.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.settings.roles.create.content.before') !!} + + +
    + {!! view_render_event('admin.settings.roles.create.content.left.before') !!} + + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    +
    + + {!! view_render_event('admin.settings.roles.create.content.left.after') !!} + + {!! view_render_event('admin.settings.roles.create.content.right.before') !!} + + +
    + {!! view_render_event('admin.settings.roles.create.accordion.general.before') !!} + + + +
    +

    + @lang('admin::app.settings.roles.create.general') +

    +
    + + + + {!! view_render_event('admin.settings.roles.create.form.name.before') !!} + + + + + @lang('admin::app.settings.roles.create.name') + + + + + + + + {!! view_render_event('admin.settings.roles.create.form.name.after') !!} + + {!! view_render_event('admin.settings.roles.create.form.description.before') !!} + + + + + @lang('admin::app.settings.roles.create.description') + + + + + + + + {!! view_render_event('admin.settings.roles.create.form.description.after') !!} + +
    + + {!! view_render_event('admin.settings.roles.create.accordion.general.after') !!} +
    + + {!! view_render_event('admin.settings.roles.create.content.right.after') !!} +
    + + {!! view_render_event('admin.settings.roles.create.content.after') !!} +
    +
    + + {!! view_render_event('admin.settings.roles.create.form.after') !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/roles/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/roles/edit.blade.php new file mode 100644 index 0000000..766184f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/roles/edit.blade.php @@ -0,0 +1,222 @@ + + + + @lang('admin::app.settings.roles.edit.title') + + + {!! view_render_event('admin.settings.roles.edit.form.before', ['role' => $role]) !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.roles.edit.breadcrumbs.before', ['role' => $role]) !!} + + + + + {!! view_render_event('admin.settings.roles.edit.breadcrumbs.after', ['role' => $role]) !!} + +
    + @lang('admin::app.settings.roles.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.roles.edit.save_button.before', ['role' => $role]) !!} + + @if (bouncer()->hasPermission('settings.user.roles.edit')) + + + @endif + + {!! view_render_event('admin.settings.roles.edit.save_button.after', ['role' => $role]) !!} +
    +
    +
    + + {!! view_render_event('admin.settings.roles.edit.content.before', ['role' => $role]) !!} + + +
    + {!! view_render_event('admin.settings.roles.edit.content.left.before', ['role' => $role]) !!} + + +
    +
    +

    + @lang('admin::app.settings.roles.edit.access-control') +

    + + + + +
    +
    + +
    +
    + + + +
    +
    +
    + + {!! view_render_event('admin.settings.roles.edit.content.left.after', ['role' => $role]) !!} + + {!! view_render_event('admin.settings.roles.edit.content.right.before', ['role' => $role]) !!} + + +
    + {!! view_render_event('admin.settings.roles.edit.accordion.general.before', ['role' => $role]) !!} + + + +
    +

    + @lang('admin::app.settings.roles.edit.general') +

    +
    + + + + {!! view_render_event('admin.settings.roles.edit.form.name.before', ['role' => $role]) !!} + + + + + @lang('admin::app.settings.roles.edit.name') + + + + + + + + {!! view_render_event('admin.settings.roles.edit.form.name.after', ['role' => $role]) !!} + + {!! view_render_event('admin.settings.roles.edit.form.description.before', ['role' => $role]) !!} + + + + + @lang('admin::app.settings.roles.edit.description') + + + + + + + + {!! view_render_event('admin.settings.roles.edit.form.description.after', ['role' => $role]) !!} + +
    + + {!! view_render_event('admin.settings.roles.edit.accordion.general.after', ['role' => $role]) !!} +
    +
    + + {!! view_render_event('admin.settings.roles.edit.content.after', ['role' => $role]) !!} +
    +
    + + {!! view_render_event('admin.settings.roles.edit.form.after', ['role' => $role]) !!} + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/roles/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/roles/index.blade.php new file mode 100644 index 0000000..2384094 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/roles/index.blade.php @@ -0,0 +1,47 @@ + + + + @lang('admin::app.settings.roles.index.title') + + +
    +
    +
    + {!! view_render_event('admin.settings.roles.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.roles.index.breadcrumbs.after') !!} + +
    + + @lang('admin::app.settings.roles.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.roles.index.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.user.roles.create')) + + @lang('admin::app.settings.roles.index.create-btn') + + @endif + + {!! view_render_event('admin.settings.roles.index.create_button.after') !!} +
    +
    +
    + + + + + +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/sources/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/sources/index.blade.php new file mode 100644 index 0000000..3e9c732 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/sources/index.blade.php @@ -0,0 +1,317 @@ + + + @lang('admin::app.settings.sources.index.title') + + +
    + +
    +
    + {!! view_render_event('admin.settings.sources.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.sources.index.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.sources.index.title') +
    +
    + +
    + {!! view_render_event('admin.settings.sources.index.create_button.before') !!} + + + @if (bouncer()->hasPermission('settings.lead.sources.create')) +
    + +
    + @endif + + {!! view_render_event('admin.settings.sources.index.create_button.after') !!} +
    +
    + + + + + +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/tags/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/tags/index.blade.php new file mode 100644 index 0000000..4765dca --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/tags/index.blade.php @@ -0,0 +1,392 @@ + + + + @lang('admin::app.settings.tags.index.title') + + +
    + +
    +
    + {!! view_render_event('admin.settings.tags.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.tags.index.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.tags.index.title') +
    +
    + +
    + {!! view_render_event('admin.settings.tags.index.create_button.before') !!} + + + @if (bouncer()->hasPermission('settings.other_settings.tags.create')) +
    + +
    + @endif + + {!! view_render_event('admin.settings.tags.index.create_button.after') !!} +
    +
    + + + + + +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/types/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/types/index.blade.php new file mode 100644 index 0000000..9f094e0 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/types/index.blade.php @@ -0,0 +1,311 @@ + + + @lang('admin::app.settings.types.index.title') + + +
    + +
    +
    + + + +
    + @lang('admin::app.settings.types.index.title') +
    +
    + +
    + {!! view_render_event('admin.settings.types.index.create_button.before') !!} + + +
    + @if (bouncer()->hasPermission('settings.lead.types.create')) + + @endif +
    + + {!! view_render_event('admin.settings.types.index.create_button.after') !!} +
    +
    + + + + + +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/users/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/users/index.blade.php new file mode 100644 index 0000000..6bf7681 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/users/index.blade.php @@ -0,0 +1,605 @@ + + + @lang('admin::app.settings.users.index.title') + + +
    +
    +
    + + + +
    + @lang('admin::app.settings.users.index.title') +
    +
    + +
    + {!! view_render_event('admin.settings.users.index.create_button.before') !!} + + + @if (bouncer()->hasPermission('settings.user.users.create')) +
    + +
    + @endif + + {!! view_render_event('admin.settings.users.index.create_button.after') !!} +
    +
    + + + + + +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/warehouses/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/create.blade.php new file mode 100644 index 0000000..4a380ef --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/create.blade.php @@ -0,0 +1,107 @@ + + + + + @lang('admin::app.settings.warehouses.create.title') + + + {!! view_render_event('admin.settings.warehouses.create.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.warehouses.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.warehouses.create.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.warehouses.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.warehouses.create.save_button.before') !!} + @if (bouncer()->hasPermission('settings.inventory.warehouse.create')) + + + @endif + + {!! view_render_event('admin.settings.warehouses.create.save_button.after') !!} +
    +
    +
    + +
    + +
    +
    +

    + @lang('admin::app.settings.warehouses.create.contact-info') +

    + + {!! view_render_event('admin.settings.warehouses.create.left.form_controls.before') !!} + + + + {!! view_render_event('admin.settings.warehouses.create.left.form_controls.after') !!} +
    +
    + + +
    + + +
    +

    + @lang('admin::app.settings.roles.create.general') +

    +
    + + + + {!! view_render_event('admin.settings.warehouses.create.right.form_controls.before') !!} + + + + {!! view_render_event('admin.settings.warehouses.create.right.form_controls.after') !!} + +
    +
    +
    +
    +
    + + {!! view_render_event('admin.settings.warehouses.create.form.after') !!} + +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/warehouses/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/edit.blade.php new file mode 100644 index 0000000..e46346a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/edit.blade.php @@ -0,0 +1,112 @@ + + + + + @lang('admin::app.settings.warehouses.edit.title') + + + {!! view_render_event('admin.settings.warehouses.edit.form.before', ['warehouse' => $warehouse]) !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.warehouses.edit.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.warehouses.edit.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.warehouses.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.warehouse.edit.save_button.after') !!} + + @if (bouncer()->hasPermission('settings.inventory.warehouse.edit')) + + + @endif + + {!! view_render_event('admin.settings.warehouses.edit.save_button.before') !!} +
    +
    +
    + +
    + +
    +
    +

    + @lang('admin::app.settings.warehouses.edit.contact-info') +

    + + {!! view_render_event('admin.settings.warehouses.edit.left.form_controls.before', ['warehouse' => $warehouse]) !!} + + + + {!! view_render_event('admin.settings.warehouses.edit.left.form_controls.after', ['warehouse' => $warehouse]) !!} +
    +
    + + +
    + + +
    +

    + @lang('admin::app.settings.roles.create.general') +

    +
    + + + + {!! view_render_event('admin.settings.warehouses.edit.right.form_controls.before', ['warehouse' => $warehouse]) !!} + + + + {!! view_render_event('admin.settings.warehouses.edit.right.form_controls.after', ['warehouse' => $warehouse]) !!} + +
    +
    +
    +
    +
    + + {!! view_render_event('admin.settings.warehouses.edit.form.after', ['warehouse' => $warehouse]) !!} +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/warehouses/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/index.blade.php new file mode 100644 index 0000000..75a9b98 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/index.blade.php @@ -0,0 +1,47 @@ + + + @lang('admin::app.settings.warehouses.index.title') + + +
    +
    +
    + + + +
    + + @lang('admin::app.settings.warehouses.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.warehouses.index.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.inventory.warehouse.create')) + + @lang('admin::app.settings.warehouses.index.create-btn') + + @endif + + {!! view_render_event('admin.settings.warehouses.index.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.settings.warehouses.index.datagrid.before') !!} + + + + + + + + {!! view_render_event('admin.settings.warehouses.index.datagrid.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view.blade.php new file mode 100644 index 0000000..03d850b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view.blade.php @@ -0,0 +1,117 @@ + + + {{ strip_tags($warehouse->name) }} + + +
    + + {!! view_render_event('admin.settings.warehouses.view.left.before', ['warehouse' => $warehouse]) !!} + +
    + +
    + +
    + +
    + + {!! view_render_event('admin.settings.warehouses.view.left.tags.before', ['warehouse' => $warehouse]) !!} + + + + + {!! view_render_event('admin.settings.warehouses.view.left.tags.after', ['warehouse' => $warehouse]) !!} + + {!! view_render_event('admin.settings.warehouses.view.left.title.before', ['warehouse' => $warehouse]) !!} + + +

    + {{ $warehouse->name }} +

    + + {!! view_render_event('admin.settings.warehouses.view.left.title.after', ['warehouse' => $warehouse]) !!} + + {!! view_render_event('admin.settings.warehouses.view.left.actions.before', ['warehouse' => $warehouse]) !!} + + +
    + {!! view_render_event('admin.settings.warehouses.view.left.actions.file.before', ['warehouse' => $warehouse]) !!} + + + + + {!! view_render_event('admin.settings.warehouses.view.left.actions.file.after', ['warehouse' => $warehouse]) !!} + + {!! view_render_event('admin.settings.warehouses.view.left.actions.note.before', ['warehouse' => $warehouse]) !!} + + + + + {!! view_render_event('admin.settings.warehouses.view.left.actions.note.after', ['warehouse' => $warehouse]) !!} + + {!! view_render_event('admin.settings.warehouses.view.left.actions.activity.before', ['warehouse' => $warehouse]) !!} + + + + + {!! view_render_event('admin.settings.warehouses.view.left.actions.activity.after', ['warehouse' => $warehouse]) !!} +
    + + {!! view_render_event('admin.settings.warehouses.view.left.actions.after', ['warehouse' => $warehouse]) !!} +
    + + + @include ('admin::settings.warehouses.view.general-information') + + + @include ('admin::settings.warehouses.view.contact-information') +
    + + {!! view_render_event('admin.settings.warehouses.view.left.after', ['warehouse' => $warehouse]) !!} + + {!! view_render_event('admin.settings.warehouses.view.right.before', ['warehouse' => $warehouse]) !!} + + +
    + {!! view_render_event('admin.settings.warehouses.view.right.attributes.before', ['warehouse' => $warehouse]) !!} + + + + + @include ('admin::settings.warehouses.view.locations') + + + + {!! view_render_event('admin.settings.warehouses.view.right.attributes.after', ['warehouse' => $warehouse]) !!} +
    + + {!! view_render_event('admin.warehouse.view.right.after', ['warehouse' => $warehouse]) !!} +
    +
    \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/contact-information.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/contact-information.blade.php new file mode 100644 index 0000000..63f9a6f --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/contact-information.blade.php @@ -0,0 +1,38 @@ +{!! view_render_event('admin.leads.view.person.before', ['warehouse' => $warehouse]) !!} + +
    + + +

    + @lang('admin::app.settings.warehouses.view.contact-information.title') +

    + + + + + +
    + {!! view_render_event('admin.leads.view.person.attributes.view.before', ['warehouse' => $warehouse]) !!} + + + + {!! view_render_event('admin.leads.view.person.attributes.view.after', ['warehouse' => $warehouse]) !!} + +
    + +
    +
    + +{!! view_render_event('admin.leads.view.person.after', ['warehouse' => $warehouse]) !!} \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/general-information.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/general-information.blade.php new file mode 100644 index 0000000..2e877c8 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/general-information.blade.php @@ -0,0 +1,38 @@ +{!! view_render_event('admin.leads.view.person.before', ['warehouse' => $warehouse]) !!} + +
    + + +

    + @lang('admin::app.settings.warehouses.view.general-information.title') +

    + + + + + +
    + {!! view_render_event('admin.leads.view.person.attributes.view.before', ['warehouse' => $warehouse]) !!} + + + + {!! view_render_event('admin.leads.view.person.attributes.view.after', ['warehouse' => $warehouse]) !!} + +
    + +
    +
    + +{!! view_render_event('admin.leads.view.person.after', ['warehouse' => $warehouse]) !!} \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/locations.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/locations.blade.php new file mode 100644 index 0000000..91ae65e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/warehouses/view/locations.blade.php @@ -0,0 +1,208 @@ + + + +@pushOnce('scripts') + + + +@endPushOnce diff --git a/packages/Webkul/Admin/src/Resources/views/settings/web-forms/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/web-forms/create.blade.php new file mode 100644 index 0000000..3519f8d --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/web-forms/create.blade.php @@ -0,0 +1,731 @@ + + + + @lang('admin::app.settings.webforms.create.title') + + + +
    +
    +
    + {!! view_render_event('admin.settings.webform.create.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.webform.create.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.webforms.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.webform.create.save_button.before') !!} + + + + + {!! view_render_event('admin.settings.webform.create.save_button.after') !!} +
    +
    +
    + + +
    +
    + + @pushOnce('scripts') + + + + + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/web-forms/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/web-forms/edit.blade.php new file mode 100644 index 0000000..55f9f8b --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/web-forms/edit.blade.php @@ -0,0 +1,866 @@ + + + + @lang('admin::app.settings.webforms.edit.title') + + + +
    +
    +
    + {!! view_render_event('admin.settings.webform.edit.breadcrumbs.before', ['webform' => $webForm]) !!} + + + + + {!! view_render_event('admin.settings.webform.edit.breadcrumbs.after', ['webform' => $webForm]) !!} + +
    + @lang('admin::app.settings.webforms.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.webform.edit.embed_button.before', ['webform' => $webForm]) !!} + + + + + {!! view_render_event('admin.settings.webform.edit.embed_button.after', ['webform' => $webForm]) !!} + + {!! view_render_event('admin.settings.webform.edit.preview_button.before', ['webform' => $webForm]) !!} + + + @lang('admin::app.settings.webforms.edit.preview') + + + {!! view_render_event('admin.settings.webform.edit.preview_button.after', ['webform' => $webForm]) !!} + + {!! view_render_event('admin.settings.webform.edit.save_button.before', ['webform' => $webForm]) !!} + + + + {!! view_render_event('admin.settings.webform.edit.save_button.after', ['webform' => $webForm]) !!} +
    +
    +
    + + + +
    +
    + + @pushOnce('scripts') + ' }}" + /> + + + @lang('admin::app.settings.webforms.edit.copy') + + + + {!! view_render_event('admin.settings.webform.edit.modal.form_controls.after', ['webform' => $webForm]) !!} + + + + + + + + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/web-forms/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/web-forms/index.blade.php new file mode 100644 index 0000000..0707765 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/web-forms/index.blade.php @@ -0,0 +1,120 @@ + + + @lang('admin::app.settings.webforms.index.title') + + + +
    +
    +
    + + + +
    + @lang('admin::app.settings.webforms.index.title') +
    +
    + +
    + +
    + @if (bouncer()->hasPermission('admin.settings.web_forms.create')) + + @endif +
    +
    +
    + + + +
    +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/webhook/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/webhook/create.blade.php new file mode 100644 index 0000000..2ee5596 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/webhook/create.blade.php @@ -0,0 +1,699 @@ +@php($placeholders = app('\Webkul\Automation\Helpers\Entity')->getEmailTemplatePlaceholders()) + + + + + @lang('admin::app.settings.webhooks.create.title') + + + {!! view_render_event('admin.settings.webhook.edit.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.webhook.edit.breadrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.webhook.edit.breadrumbs.after') !!} + +
    + @lang('admin::app.settings.webhooks.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.webhook.edit.save_button.before') !!} + + + + + {!! view_render_event('admin.settings.webhook.edit.save_button.after') !!} +
    +
    +
    + + +
    +
    + + {!! view_render_event('admin.settings.webhook.edit.form.after') !!} + + @pushOnce('scripts') + + + + + + + + + + + + + @endPushOnce + + @pushOnce('styles') + + + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/webhook/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/webhook/edit.blade.php new file mode 100644 index 0000000..09a3f61 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/webhook/edit.blade.php @@ -0,0 +1,712 @@ +@php($placeholders = app('\Webkul\Automation\Helpers\Entity')->getEmailTemplatePlaceholders()) + + + + + @lang('admin::app.settings.webhooks.edit.title') + + + {!! view_render_event('admin.settings.webhook.edit.form.before', ['webhook' => $webhook]) !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.webhook.edit.breadcrumbs.before', ['webhook' => $webhook]) !!} + + + + + {!! view_render_event('admin.settings.webhook.edit.breadcrumbs.after', ['webhook' => $webhook]) !!} + +
    + @lang('admin::app.settings.webhooks.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.webhook.edit.save_button.before', ['webhook' => $webhook]) !!} + + + + + {!! view_render_event('admin.settings.webhook.edit.save_button.after', ['webhook' => $webhook]) !!} +
    +
    +
    + + +
    +
    + + {!! view_render_event('admin.settings.webhook.edit.form.after', ['webhook' => $webhook]) !!} + + @pushOnce('scripts') + + + + + + + + + + + + @endPushOnce + + @pushOnce('styles') + + + + + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/webhook/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/webhook/index.blade.php new file mode 100644 index 0000000..d073fc4 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/webhook/index.blade.php @@ -0,0 +1,46 @@ + + + @lang('admin::app.settings.webhooks.index.title') + + +
    +
    +
    + {!! view_render_event('admin.settings.webhooks.breadcrumbs.datagrid.before') !!} + + + + {!! view_render_event('admin.settings.webhooks.breadcrumbs.datagrid.after') !!} + +
    + @lang('admin::app.settings.webhooks.index.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.webhooks.create_button.datagrid.before') !!} + + + + @lang('admin::app.settings.webhooks.index.create-btn') + + + {!! view_render_event('admin.settings.webhooks.create_button.datagrid.after') !!} +
    +
    +
    + + {!! view_render_event('admin.settings.webhooks.index.datagrid.before') !!} + + + + + + + {!! view_render_event('admin.settings.webhooks.index.datagrid.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/workflows/create.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/workflows/create.blade.php new file mode 100644 index 0000000..d65a62c --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/workflows/create.blade.php @@ -0,0 +1,1277 @@ + + + @lang('admin::app.settings.workflows.create.title') + + + {!! view_render_event('admin.settings.workflow.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.workflow.breadcrumbs.before') !!} + + + + {!! view_render_event('admin.settings.webhooks.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.workflows.create.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.workflow.save_button.before') !!} + + + + + {!! view_render_event('admin.settings.workflow.save_button.after') !!} +
    +
    +
    + + + + + +
    +
    + + {!! view_render_event('admin.settings.workflow.form.after') !!} + + @pushOnce('scripts') + + + + + + + + + + + + @endPushOnce + + @pushOnce('styles') + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/workflows/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/workflows/edit.blade.php new file mode 100644 index 0000000..5b9305a --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/workflows/edit.blade.php @@ -0,0 +1,1296 @@ + + + @lang('admin::app.settings.workflows.edit.title') + + + {!! view_render_event('admin.activities.edit.form.before') !!} + + +
    +
    +
    + {!! view_render_event('admin.settings.workflows.edit.breadcrumbs.before', ['workflow' => $workflow]) !!} + + + + {!! view_render_event('admin.settings.workflows.edit.breadcrumbs.after', ['workflow' => $workflow]) !!} + +
    + @lang('admin::app.settings.workflows.edit.title') +
    +
    + +
    +
    + {!! view_render_event('admin.settings.workflows.edit.save_button.before', ['workflow' => $workflow]) !!} + + + + + {!! view_render_event('admin.settings.workflows.edit.save_button.after', ['workflow' => $workflow]) !!} +
    +
    +
    + + + + + +
    +
    + + @pushOnce('scripts') + + + + + + + + + + + + @endPushOnce + + @pushOnce('styles') + + @endPushOnce +
    diff --git a/packages/Webkul/Admin/src/Resources/views/settings/workflows/index.blade.php b/packages/Webkul/Admin/src/Resources/views/settings/workflows/index.blade.php new file mode 100644 index 0000000..055859e --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/settings/workflows/index.blade.php @@ -0,0 +1,50 @@ + + + + @lang('admin::app.settings.workflows.index.title') + + +
    +
    +
    + {!! view_render_event('admin.settings.workflows.index.breadcrumbs.before') !!} + + + + + {!! view_render_event('admin.settings.workflows.index.breadcrumbs.after') !!} + +
    + @lang('admin::app.settings.workflows.index.title') +
    +
    + +
    + +
    + {!! view_render_event('admin.settings.workflows.index.create_button.before') !!} + + @if (bouncer()->hasPermission('settings.automation.workflows.create')) + + @lang('admin::app.settings.workflows.index.create-btn') + + @endif + + {!! view_render_event('admin.settings.workflows.index.create_button.after') !!} +
    +
    +
    + + {!! view_render_event('admin.settings.workflows.index.datagrid.before') !!} + + + + + + + {!! view_render_event('admin.settings.workflows.index.datagrid.after') !!} +
    +
    diff --git a/packages/Webkul/Admin/src/Resources/views/user/account/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/user/account/edit.blade.php new file mode 100644 index 0000000..afd1560 --- /dev/null +++ b/packages/Webkul/Admin/src/Resources/views/user/account/edit.blade.php @@ -0,0 +1,201 @@ + + + + @lang('admin::app.account.edit.title') + + + {!! view_render_event('admin.user.account.form.before', ['user' => $user]) !!} + + + +
    +
    + {!! view_render_event('admin.user.account.breadcrumbs.before', ['user' => $user]) !!} + + + + + {!! view_render_event('admin.user.account.breadcrumbs.after', ['user' => $user]) !!} + +
    + {!! view_render_event('admin.user.account.title.before', ['user' => $user]) !!} + + @lang('admin::app.account.edit.title') + + {!! view_render_event('admin.user.account.title.after', ['user' => $user]) !!} +
    +
    + +
    + +
    + {!! view_render_event('admin.user.account.save_btn.before', ['user' => $user]) !!} + + + + {!! view_render_event('admin.user.account.save_btn.after', ['user' => $user]) !!} +
    +
    +
    + + +
    + {!! view_render_event('admin.user.account.left.before', ['user' => $user]) !!} + + +
    + +
    +

    + @lang('admin::app.account.edit.general') +

    + + + + + + +

    + @lang('admin::app.account.edit.upload-image-info') +

    + + + + + @lang('admin::app.account.edit.name') + + + + + + + + + + + @lang('admin::app.account.edit.email') + + + + + + +
    +
    + + {!! view_render_event('admin.user.account.left.after', ['user' => $user]) !!} + + {!! view_render_event('admin.user.account.right.before', ['user' => $user]) !!} + + +
    + + +

    + @lang('admin::app.account.edit.change-password') +

    + + + + + {!! view_render_event('admin.user.current_password.before', ['user' => $user]) !!} + + + + + @lang('admin::app.account.edit.current-password') + + + + + + + + {!! view_render_event('admin.user.current_password.after', ['user' => $user]) !!} + + {!! view_render_event('admin.user.password.before', ['user' => $user]) !!} + + + + + @lang('admin::app.account.edit.password') + + + + + + + + {!! view_render_event('admin.user.password.after', ['user' => $user]) !!} + + {!! view_render_event('admin.user.confirm-password.before', ['user' => $user]) !!} + + + + + @lang('admin::app.account.edit.confirm-password') + + + + + + + + {!! view_render_event('admin.user.confirm-password.after', ['user' => $user]) !!} + +
    +
    + + {!! view_render_event('admin.user.account.right.after', ['user' => $user]) !!} +
    +
    + + {!! view_render_event('admin.user.account.form.after') !!} +
    diff --git a/packages/Webkul/Admin/src/Routes/Admin/activities-routes.php b/packages/Webkul/Admin/src/Routes/Admin/activities-routes.php new file mode 100644 index 0000000..1fe00ec --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/activities-routes.php @@ -0,0 +1,24 @@ +prefix('activities')->group(function () { + Route::get('', 'index')->name('admin.activities.index'); + + Route::get('get', 'get')->name('admin.activities.get'); + + Route::post('create', 'store')->name('admin.activities.store'); + + Route::get('edit/{id}', 'edit')->name('admin.activities.edit'); + + Route::put('edit/{id}', 'update')->name('admin.activities.update'); + + Route::get('download/{id}', 'download')->name('admin.activities.file_download'); + + Route::delete('{id}', 'destroy')->name('admin.activities.delete'); + + Route::post('mass-update', 'massUpdate')->name('admin.activities.mass_update'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.activities.mass_delete'); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/auth-routes.php b/packages/Webkul/Admin/src/Routes/Admin/auth-routes.php new file mode 100644 index 0000000..a2c221e --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/auth-routes.php @@ -0,0 +1,51 @@ +group(function () { + /** + * Redirect route. + */ + Route::get('/', [Controller::class, 'redirectToLogin']); + + /** + * Session routes. + */ + Route::controller(SessionController::class)->group(function () { + Route::prefix('login')->group(function () { + Route::get('', 'create')->name('admin.session.create'); + + /** + * Throttled inside the controller instead of here, counting only failed attempts, so a + * correct sign-in never consumes the budget. + */ + Route::post('', 'store')->name('admin.session.store'); + }); + + Route::middleware(['user'])->group(function () { + Route::delete('logout', 'destroy')->name('admin.session.destroy'); + }); + }); + + /** + * Forgot password routes. + */ + Route::controller(ForgotPasswordController::class)->prefix('forget-password')->group(function () { + Route::get('', 'create')->name('admin.forgot_password.create'); + + Route::middleware(['throttle:5,1'])->post('', 'store')->name('admin.forgot_password.store'); + }); + + /** + * Reset password routes. + */ + Route::controller(ResetPasswordController::class)->prefix('reset-password')->group(function () { + Route::get('{token}', 'create')->name('admin.reset_password.create'); + + Route::middleware(['throttle:5,1'])->post('', 'store')->name('admin.reset_password.store'); + }); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/configuration-routes.php b/packages/Webkul/Admin/src/Routes/Admin/configuration-routes.php new file mode 100644 index 0000000..f6c253e --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/configuration-routes.php @@ -0,0 +1,16 @@ +prefix('configuration')->group(function () { + Route::get('search', 'search')->name('admin.configuration.search'); + + Route::prefix('{slug?}/{slug2?}')->group(function () { + Route::get('', 'index')->name('admin.configuration.index'); + + Route::post('', 'store')->name('admin.configuration.store'); + + Route::get('{path}', 'download')->name('admin.configuration.download'); + }); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/contacts-routes.php b/packages/Webkul/Admin/src/Routes/Admin/contacts-routes.php new file mode 100644 index 0000000..0930899 --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/contacts-routes.php @@ -0,0 +1,77 @@ +group(function () { + /** + * Persons routes. + */ + Route::controller(PersonController::class)->prefix('persons')->group(function () { + Route::get('', 'index')->name('admin.contacts.persons.index'); + + Route::get('create', 'create')->name('admin.contacts.persons.create'); + + Route::post('create', 'store')->name('admin.contacts.persons.store'); + + Route::get('view/{id}', 'show')->name('admin.contacts.persons.view'); + + Route::get('edit/{id}', 'edit')->name('admin.contacts.persons.edit'); + + Route::put('edit/{id}', 'update')->name('admin.contacts.persons.update'); + + Route::get('search', 'search')->name('admin.contacts.persons.search'); + + Route::middleware(['throttle:100,60'])->delete('{id}', 'destroy')->name('admin.contacts.persons.delete'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.contacts.persons.mass_delete'); + + /** + * Google Contacts export routes. + */ + Route::controller(GoogleContactExportController::class)->prefix('google-export')->group(function () { + Route::post('', 'store')->name('admin.contacts.persons.google_export.store'); + + Route::get('{batch}/stats', 'stats')->name('admin.contacts.persons.google_export.stats'); + }); + + /** + * Tag routes. + */ + Route::controller(TagController::class)->prefix('{id}/tags')->group(function () { + Route::post('', 'attach')->name('admin.contacts.persons.tags.attach'); + + Route::delete('', 'detach')->name('admin.contacts.persons.tags.detach'); + }); + + /** + * Activity routes. + */ + Route::controller(ActivityController::class)->prefix('{id}/activities')->group(function () { + Route::get('', 'index')->name('admin.contacts.persons.activities.index'); + }); + }); + + /** + * Organization routes. + */ + Route::controller(OrganizationController::class)->prefix('organizations')->group(function () { + Route::get('', 'index')->name('admin.contacts.organizations.index'); + + Route::get('create', 'create')->name('admin.contacts.organizations.create'); + + Route::post('create', 'store')->name('admin.contacts.organizations.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.contacts.organizations.edit'); + + Route::put('edit/{id}', 'update')->name('admin.contacts.organizations.update'); + + Route::delete('{id}', 'destroy')->name('admin.contacts.organizations.delete'); + + Route::put('mass-destroy', 'massDestroy')->name('admin.contacts.organizations.mass_delete'); + }); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/help-routes.php b/packages/Webkul/Admin/src/Routes/Admin/help-routes.php new file mode 100644 index 0000000..6615131 --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/help-routes.php @@ -0,0 +1,8 @@ +prefix('help')->group(function () { + Route::get('', 'index')->name('admin.help.index'); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/leads-routes.php b/packages/Webkul/Admin/src/Routes/Admin/leads-routes.php new file mode 100644 index 0000000..c92c781 --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/leads-routes.php @@ -0,0 +1,66 @@ +prefix('leads')->group(function () { + Route::get('', 'index')->name('admin.leads.index'); + + Route::get('create', 'create')->name('admin.leads.create'); + + Route::post('create', 'store')->name('admin.leads.store'); + + Route::post('create-by-ai', 'createByAI')->name('admin.leads.create_by_ai'); + + Route::get('view/{id}', 'view')->name('admin.leads.view'); + + Route::get('edit/{id}', 'edit')->name('admin.leads.edit'); + + Route::put('edit/{id}', 'update')->name('admin.leads.update'); + + Route::put('attributes/edit/{id}', 'updateAttributes')->name('admin.leads.attributes.update'); + + Route::put('stage/edit/{id}', 'updateStage')->name('admin.leads.stage.update'); + + Route::get('search', 'search')->name('admin.leads.search'); + + Route::delete('{id}', 'destroy')->name('admin.leads.delete'); + + Route::post('mass-update', 'massUpdate')->name('admin.leads.mass_update'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.leads.mass_delete'); + + Route::get('get/{pipeline_id?}', 'get')->name('admin.leads.get'); + + Route::delete('product/{lead_id}', 'removeProduct')->name('admin.leads.product.remove'); + + Route::put('product/{lead_id}', 'addProduct')->name('admin.leads.product.add'); + + Route::get('kanban/look-up', [LeadController::class, 'kanbanLookup'])->name('admin.leads.kanban.look_up'); + + Route::controller(ActivityController::class)->prefix('{id}/activities')->group(function () { + Route::get('', 'index')->name('admin.leads.activities.index'); + }); + + Route::controller(TagController::class)->prefix('{id}/tags')->group(function () { + Route::post('', 'attach')->name('admin.leads.tags.attach'); + + Route::delete('', 'detach')->name('admin.leads.tags.detach'); + }); + + Route::controller(EmailController::class)->prefix('{id}/emails')->group(function () { + Route::post('', 'store')->name('admin.leads.emails.store'); + + Route::delete('', 'detach')->name('admin.leads.emails.detach'); + }); + + Route::controller(QuoteController::class)->prefix('quotes')->group(function () { + Route::post('{quote_id}/mail', 'mail')->name('admin.leads.quotes.mail'); + + Route::delete('{quote_id?}', 'delete')->name('admin.leads.quotes.delete'); + }); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/mail-routes.php b/packages/Webkul/Admin/src/Routes/Admin/mail-routes.php new file mode 100644 index 0000000..45efde4 --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/mail-routes.php @@ -0,0 +1,33 @@ +middleware('sanitize_url')->group(function () { + Route::controller(EmailController::class)->group(function () { + Route::post('create', 'store')->name('admin.mail.store'); + + Route::put('edit/{id}', 'update')->name('admin.mail.update'); + + Route::get('attachment-download/{id?}', 'download')->name('admin.mail.attachment_download'); + + Route::get('{route?}', 'index')->name('admin.mail.index'); + + Route::get('{route}/{id}', 'view')->name('admin.mail.view'); + + Route::delete('{id}', 'destroy')->name('admin.mail.delete'); + + Route::post('mass-update', 'massUpdate')->name('admin.mail.mass_update'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.mail.mass_delete'); + + Route::post('inbound-parse', 'inboundParse')->name('admin.mail.inbound_parse')->withoutMiddleware('user'); + }); + + Route::controller(TagController::class)->prefix('{id}/tags')->group(function () { + Route::post('', 'attach')->name('admin.mail.tags.attach'); + + Route::delete('', 'detach')->name('admin.mail.tags.detach'); + }); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/products-routes.php b/packages/Webkul/Admin/src/Routes/Admin/products-routes.php new file mode 100644 index 0000000..6a58e12 --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/products-routes.php @@ -0,0 +1,42 @@ + ['user']], function () { + Route::controller(ProductController::class)->prefix('products')->group(function () { + Route::get('', 'index')->name('admin.products.index'); + + Route::get('create', 'create')->name('admin.products.create'); + + Route::post('create', 'store')->name('admin.products.store'); + + Route::get('view/{id}', 'view')->name('admin.products.view'); + + Route::get('edit/{id}', 'edit')->name('admin.products.edit'); + + Route::put('edit/{id}', 'update')->name('admin.products.update'); + + Route::get('search', 'search')->name('admin.products.search'); + + Route::get('{id}/warehouses', 'warehouses')->name('admin.products.warehouses'); + + Route::post('{id}/inventories/{warehouseId?}', 'storeInventories')->name('admin.products.inventories.store'); + + Route::delete('{id}', 'destroy')->name('admin.products.delete'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.products.mass_delete'); + + Route::controller(ActivityController::class)->prefix('{id}/activities')->group(function () { + Route::get('', 'index')->name('admin.products.activities.index'); + }); + + Route::controller(TagController::class)->prefix('{id}/tags')->group(function () { + Route::post('', 'attach')->name('admin.products.tags.attach'); + + Route::delete('', 'detach')->name('admin.products.tags.detach'); + }); + }); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/quote-routes.php b/packages/Webkul/Admin/src/Routes/Admin/quote-routes.php new file mode 100644 index 0000000..05f49c8 --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/quote-routes.php @@ -0,0 +1,26 @@ +prefix('quotes')->group(function () { + Route::get('', 'index')->name('admin.quotes.index'); + + Route::get('create/{lead_id?}', 'create')->name('admin.quotes.create'); + + Route::post('create', 'store')->name('admin.quotes.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.quotes.edit'); + + Route::put('edit/{id}', 'update')->name('admin.quotes.update'); + + Route::get('print/{id?}', 'print')->name('admin.quotes.print'); + + Route::delete('{id}', 'destroy')->name('admin.quotes.delete'); + + Route::get('search', 'search')->name('admin.quotes.search'); + + Route::get('lead-products/{lead_id}', 'leadProducts')->name('admin.quotes.lead_products'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.quotes.mass_delete'); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/rest-routes.php b/packages/Webkul/Admin/src/Routes/Admin/rest-routes.php new file mode 100644 index 0000000..453a989 --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/rest-routes.php @@ -0,0 +1,56 @@ +prefix('dashboard')->group(function () { + Route::get('', 'index')->name('admin.dashboard.index'); + + Route::get('stats', 'stats')->name('admin.dashboard.stats'); + + Route::get('export-pdf', 'exportPdf')->name('admin.dashboard.export_pdf'); +}); + +/** + * DataGrid routes. + */ +Route::prefix('datagrid')->group(function () { + /** + * Saved filter routes. + */ + Route::controller(SavedFilterController::class)->prefix('datagrid/saved-filters')->group(function () { + Route::post('', 'store')->name('admin.datagrid.saved_filters.store'); + + Route::get('', 'get')->name('admin.datagrid.saved_filters.index'); + + Route::put('{id}', 'update')->name('admin.datagrid.saved_filters.update'); + + Route::delete('{id}', 'destroy')->name('admin.datagrid.saved_filters.destroy'); + }); + + /** + * Lookup routes. + */ + Route::get('datagrid/look-up', [DataGridController::class, 'lookUp'])->name('admin.datagrid.look_up'); +}); + +/** + * Tinymce file upload handler. + */ +Route::post('tinymce/upload', [TinyMCEController::class, 'upload'])->name('admin.tinymce.upload'); + +/** + * User profile routes. + */ +Route::controller(AccountController::class)->prefix('account')->group(function () { + Route::get('', 'edit')->name('admin.user.account.edit'); + + Route::put('update', 'update')->name('admin.user.account.update'); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/settings-routes.php b/packages/Webkul/Admin/src/Routes/Admin/settings-routes.php new file mode 100644 index 0000000..90b54ec --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/settings-routes.php @@ -0,0 +1,391 @@ +group(function () { + /** + * Settings routes. + */ + Route::controller(SettingController::class)->prefix('settings')->group(function () { + Route::get('', 'index')->name('admin.settings.index'); + + Route::get('search', 'search')->name('admin.settings.search'); + }); + + /** + * Groups routes. + */ + Route::controller(GroupController::class)->prefix('groups')->group(function () { + Route::get('', 'index')->name('admin.settings.groups.index'); + + Route::post('create', 'store')->name('admin.settings.groups.store'); + + Route::get('edit/{id}', 'edit')->name('admin.settings.groups.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.groups.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.groups.delete'); + }); + + /** + * Type routes. + */ + Route::controller(TypeController::class)->prefix('types')->group(function () { + Route::get('', 'index')->name('admin.settings.types.index'); + + Route::post('create', 'store')->name('admin.settings.types.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.types.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.types.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.types.delete'); + }); + + /** + * Roles routes. + */ + Route::controller(RoleController::class)->prefix('roles')->group(function () { + Route::get('', 'index')->name('admin.settings.roles.index'); + + Route::get('create', 'create')->name('admin.settings.roles.create'); + + Route::post('create', 'store')->name('admin.settings.roles.store'); + + Route::get('edit/{id}', 'edit')->name('admin.settings.roles.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.roles.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.roles.delete'); + }); + + /** + * Google Contacts Routes. + */ + Route::controller(GoogleContactController::class)->prefix('google-contacts')->group(function () { + Route::get('', 'index')->name('admin.settings.google_contacts.index'); + + Route::get('connect', 'connect')->name('admin.settings.google_contacts.connect'); + + Route::get('callback', 'callback')->name('admin.settings.google_contacts.callback'); + + Route::post('disconnect', 'disconnect')->name('admin.settings.google_contacts.disconnect'); + }); + + /** + * WebForms Routes. + */ + Route::controller(WebFormController::class)->prefix('web-forms')->group(function () { + Route::group(['middleware' => ['user']], function () { + Route::get('', 'index')->name('admin.settings.web_forms.index'); + + Route::get('create', 'create')->name('admin.settings.web_forms.create'); + + Route::post('create', 'store')->name('admin.settings.web_forms.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.web_forms.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.web_forms.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.web_forms.delete'); + }); + }); + + /** + * Workflows Routes. + */ + Route::controller(WorkflowController::class)->prefix('workflows')->group(function () { + Route::get('', 'index')->name('admin.settings.workflows.index'); + + Route::get('create', 'create')->name('admin.settings.workflows.create'); + + Route::post('create', 'store')->name('admin.settings.workflows.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.workflows.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.workflows.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.workflows.delete'); + }); + + /** + * Webhook Routes. + */ + Route::controller(WebhookController::class)->prefix('webhooks')->group(function () { + Route::get('', 'index')->name('admin.settings.webhooks.index'); + + Route::get('create', 'create')->name('admin.settings.webhooks.create'); + + Route::post('create', 'store')->name('admin.settings.webhooks.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.webhooks.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.webhooks.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.webhooks.delete'); + }); + + /** + * Tags Routes. + */ + Route::controller(TagController::class)->prefix('tags')->group(function () { + Route::get('', 'index')->name('admin.settings.tags.index'); + + Route::post('create', 'store')->name('admin.settings.tags.store'); + + Route::get('edit/{id}', 'edit')->name('admin.settings.tags.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.tags.update'); + + Route::get('search', 'search')->name('admin.settings.tags.search'); + + Route::delete('{id}', 'destroy')->name('admin.settings.tags.delete'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.settings.tags.mass_delete'); + }); + + /** + * Users Routes. + */ + Route::controller(UserController::class)->prefix('users')->group(function () { + Route::get('', 'index')->name('admin.settings.users.index'); + + Route::post('create', 'store')->name('admin.settings.users.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.users.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.users.update'); + + Route::get('search', 'search')->name('admin.settings.users.search'); + + Route::delete('{id}', 'destroy')->name('admin.settings.users.delete'); + + Route::post('mass-update', 'massUpdate')->name('admin.settings.users.mass_update'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.settings.users.mass_delete'); + }); + + /** + * Pipelines Routes. + */ + Route::controller(PipelineController::class)->prefix('pipelines')->group(function () { + Route::get('', 'index')->name('admin.settings.pipelines.index'); + + Route::get('create', 'create')->name('admin.settings.pipelines.create'); + + Route::post('create', 'store')->name('admin.settings.pipelines.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.pipelines.edit'); + + Route::post('edit/{id}', 'update')->name('admin.settings.pipelines.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.pipelines.delete'); + }); + + /** + * Sources Routes. + */ + Route::controller(SourceController::class)->prefix('sources')->group(function () { + Route::get('', 'index')->name('admin.settings.sources.index'); + + Route::post('create', 'store')->name('admin.settings.sources.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.sources.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.sources.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.sources.delete'); + }); + + /** + * Attributes Routes. + */ + Route::controller(AttributeController::class)->prefix('attributes')->group(function () { + Route::get('', 'index')->name('admin.settings.attributes.index'); + + Route::get('check-unique-validation', 'checkUniqueValidation')->name('admin.settings.attributes.check_unique_validation'); + + Route::get('create', 'create')->name('admin.settings.attributes.create'); + + Route::post('create', 'store')->name('admin.settings.attributes.store'); + + Route::get('edit/{id}', 'edit')->name('admin.settings.attributes.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.attributes.update'); + + Route::get('lookup/{lookup?}', 'lookup')->name('admin.settings.attributes.lookup'); + + Route::get('lookup-entity/{lookup?}', 'lookupEntity')->name('admin.settings.attributes.lookup_entity'); + + Route::delete('{id}', 'destroy')->name('admin.settings.attributes.delete'); + + Route::get('{id}/options', 'getAttributeOptions')->name('admin.settings.attributes.options'); + + Route::post('mass-update', 'massUpdate')->name('admin.settings.attributes.mass_update'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.settings.attributes.mass_delete'); + + Route::get('download', 'download')->name('admin.settings.attributes.download'); + }); + + /** + * Warehouses Routes. + */ + Route::controller(WarehouseController::class)->prefix('warehouses')->group(function () { + Route::put('edit/{id}', 'update')->name('admin.settings.warehouses.update'); + + Route::get('', 'index')->name('admin.settings.warehouses.index'); + + Route::get('search', 'search')->name('admin.settings.warehouses.search'); + + Route::get('{id}/products', 'products')->name('admin.settings.warehouses.products.index'); + + Route::get('create', 'create')->name('admin.settings.warehouses.create'); + + Route::post('create', 'store')->name('admin.settings.warehouses.store'); + + Route::get('view/{id}', 'view')->name('admin.settings.warehouses.view'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.warehouses.edit'); + + Route::delete('{id}', 'destroy')->name('admin.settings.warehouses.delete'); + + Route::controller(WarehouseTagController::class)->prefix('{id}/tags')->group(function () { + Route::post('', 'attach')->name('admin.settings.warehouses.tags.attach'); + + Route::delete('', 'detach')->name('admin.settings.warehouses.tags.detach'); + }); + + Route::controller(ActivityController::class)->prefix('{id}/activities')->group(function () { + Route::get('', 'index')->name('admin.settings.warehouse.activities.index'); + }); + }); + + /** + * Warehouses Location Routes. + */ + Route::controller(LocationController::class)->prefix('locations')->group(function () { + Route::get('search', 'search')->name('admin.settings.locations.search'); + + Route::post('create', 'store')->name('admin.settings.locations.store'); + + Route::put('edit/{id}', 'update')->name('admin.settings.locations.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.locations.delete'); + }); + + /** + * Email Templates Routes. + */ + Route::controller(EmailTemplateController::class)->prefix('email-templates')->group(function () { + Route::get('', 'index')->name('admin.settings.email_templates.index'); + + Route::get('create', 'create')->name('admin.settings.email_templates.create'); + + Route::post('create', 'store')->name('admin.settings.email_templates.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.email_templates.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.email_templates.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.email_templates.delete'); + }); + + /** + * Events Routes. + */ + Route::group(['prefix' => 'marketing'], function () { + Route::controller(EventController::class)->prefix('events')->group(function () { + Route::get('', 'index')->name('admin.settings.marketing.events.index'); + + Route::post('create', 'store')->name('admin.settings.marketing.events.store'); + + Route::get('edit/{id?}', 'edit')->name('admin.settings.marketing.events.edit'); + + Route::put('edit/{id}', 'update')->name('admin.settings.marketing.events.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.marketing.events.delete'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.settings.marketing.events.mass_delete'); + }); + + Route::controller(CampaignsController::class)->prefix('campaigns')->group(function () { + Route::get('', 'index')->name('admin.settings.marketing.campaigns.index'); + + Route::get('events', 'getEvents')->name('admin.settings.marketing.campaigns.events'); + + Route::get('email-templates', 'getEmailTemplates')->name('admin.settings.marketing.campaigns.email-templates'); + + Route::post('', 'store')->name('admin.settings.marketing.campaigns.store'); + + Route::get('{id}', 'show')->name('admin.settings.marketing.campaigns.edit'); + + Route::put('{id}', 'update')->name('admin.settings.marketing.campaigns.update'); + + Route::delete('{id}', 'destroy')->name('admin.settings.marketing.campaigns.delete'); + + Route::post('mass-destroy', 'massDestroy')->name('admin.settings.marketing.campaigns.mass_delete'); + }); + }); + + Route::prefix('data-transfer')->group(function () { + /** + * Import routes. + */ + Route::controller(ImportController::class)->prefix('imports')->group(function () { + Route::get('', 'index')->name('admin.settings.data_transfer.imports.index'); + + Route::get('create', 'create')->name('admin.settings.data_transfer.imports.create'); + + Route::post('create', 'store')->name('admin.settings.data_transfer.imports.store'); + + Route::get('edit/{id}', 'edit')->name('admin.settings.data_transfer.imports.edit'); + + Route::put('update/{id}', 'update')->name('admin.settings.data_transfer.imports.update'); + + Route::delete('destroy/{id}', 'destroy')->name('admin.settings.data_transfer.imports.delete'); + + Route::get('import/{id}', 'import')->name('admin.settings.data_transfer.imports.import'); + + Route::get('validate/{id}', 'validateImport')->name('admin.settings.data_transfer.imports.validate'); + + Route::get('start/{id}', 'start')->name('admin.settings.data_transfer.imports.start'); + + Route::get('link/{id}', 'link')->name('admin.settings.data_transfer.imports.link'); + + Route::get('index/{id}', 'indexData')->name('admin.settings.data_transfer.imports.index_data'); + + Route::get('stats/{id}/{state?}', 'stats')->name('admin.settings.data_transfer.imports.stats'); + + Route::get('download-sample/{sample?}', 'downloadSample')->name('admin.settings.data_transfer.imports.download_sample'); + + Route::get('download/{id}', 'download')->name('admin.settings.data_transfer.imports.download'); + + Route::get('download-error-report/{id}', 'downloadErrorReport')->name('admin.settings.data_transfer.imports.download_error_report'); + }); + }); +}); diff --git a/packages/Webkul/Admin/src/Routes/Admin/web.php b/packages/Webkul/Admin/src/Routes/Admin/web.php new file mode 100644 index 0000000..d0e256f --- /dev/null +++ b/packages/Webkul/Admin/src/Routes/Admin/web.php @@ -0,0 +1,56 @@ +name('krayin.home'); diff --git a/packages/Webkul/Admin/src/Traits/HandlesActivityTimeline.php b/packages/Webkul/Admin/src/Traits/HandlesActivityTimeline.php new file mode 100644 index 0000000..7e39271 --- /dev/null +++ b/packages/Webkul/Admin/src/Traits/HandlesActivityTimeline.php @@ -0,0 +1,190 @@ + 'lead_activities', 'foreign' => 'lead_id']. + */ + abstract protected function timelinePivot(): array; + + /** + * The emails column that links to the record (e.g. 'lead_id'), or null when + * the record type does not carry emails. + */ + protected function timelineEmailColumn(): ?string + { + return null; + } + + /** + * Display a paginated listing of the record's activity timeline. + * + * @param int $id + * @return AnonymousResourceCollection + */ + public function index($id) + { + $type = request()->input('type', 'all'); + + // Clamp so a crafted request can't defeat pagination and pull the whole history. + $perPage = max(1, min(50, (int) request()->input('per_page', $this->timelinePerPage))); + + $page = $this->timelineReferences($id, $type)->paginate($perPage); + + return ActivityResource::collection($this->hydrateTimeline($page->items())) + ->additional([ + 'meta' => [ + 'current_page' => $page->currentPage(), + 'last_page' => $page->lastPage(), + 'per_page' => $page->perPage(), + 'total' => $page->total(), + ], + ]); + } + + /** + * Build the ordered union of activity and email references for the record. + * + * Each row carries only its source, id and timestamp, which is enough to + * order and paginate the whole timeline without loading every record. + */ + protected function timelineReferences($id, string $type) + { + $pivot = $this->timelinePivot(); + + $references = []; + + if ($type !== 'email') { + $references[] = DB::table('activities') + ->join($pivot['table'], 'activities.id', '=', $pivot['table'].'.activity_id') + ->where($pivot['table'].'.'.$pivot['foreign'], $id) + ->when($type === 'planned', fn ($query) => $query->where('activities.is_done', 0)) + ->when( + ! in_array($type, ['all', 'planned'], true), + fn ($query) => $query->where('activities.type', $type) + ) + ->select('activities.id as ref_id', 'activities.created_at', DB::raw("'activity' as source")); + } + + if (($emailColumn = $this->timelineEmailColumn()) && in_array($type, ['all', 'email'], true)) { + $references[] = DB::table('emails') + ->where($emailColumn, $id) + ->orWhereIn('parent_id', fn ($query) => $query + ->select('id') + ->from('emails') + ->where($emailColumn, $id) + ) + ->select('emails.id as ref_id', 'emails.created_at', DB::raw("'email' as source")); + } + + $union = array_shift($references); + + foreach ($references as $reference) { + $union->unionAll($reference); + } + + return DB::query() + ->fromSub($union, 'timeline') + ->orderByDesc('created_at') + ->orderByDesc('ref_id'); + } + + /** + * Hydrate a page of references into activity-shaped items, preserving order. + */ + protected function hydrateTimeline($references) + { + $references = collect($references); + + $activityIds = $references->where('source', 'activity')->pluck('ref_id')->all(); + $emailIds = $references->where('source', 'email')->pluck('ref_id')->all(); + + $activities = empty($activityIds) + ? collect() + : $this->activityRepository + ->with(['files', 'participants.user', 'participants.person.organization']) + ->findWhereIn('id', $activityIds) + ->keyBy('id'); + + $emails = empty($emailIds) + ? collect() + : DB::table('emails')->whereIn('id', $emailIds)->get()->keyBy('id'); + + $attachments = empty($emailIds) + ? collect() + : $this->attachmentRepository->findWhereIn('email_id', $emailIds)->groupBy('email_id'); + + return $references + ->map(fn ($reference) => $reference->source === 'activity' + ? $activities->get($reference->ref_id) + : $this->mapEmailAsActivity($emails->get($reference->ref_id), $attachments->get($reference->ref_id, collect())) + ) + ->filter() + ->values(); + } + + /** + * Transform an email row into an activity-shaped object for the timeline. + */ + protected function mapEmailAsActivity($email, $attachments) + { + if (! $email) { + return null; + } + + return (object) [ + 'id' => $email->id, + 'parent_id' => $email->parent_id, + 'title' => $email->subject, + 'type' => 'email', + 'is_done' => 1, + 'comment' => $email->reply, + 'schedule_from' => null, + 'schedule_to' => null, + 'user' => auth()->guard('user')->user(), + 'participants' => [], + 'location' => null, + 'additional' => [ + 'folders' => json_decode($email->folders), + 'from' => json_decode($email->from), + 'to' => json_decode($email->reply_to), + 'cc' => json_decode($email->cc), + 'bcc' => json_decode($email->bcc), + ], + 'files' => $attachments->map(function ($attachment) { + return (object) [ + 'id' => $attachment->id, + 'name' => $attachment->name, + 'path' => $attachment->path, + 'url' => $attachment->url, + 'created_at' => $attachment->created_at, + 'updated_at' => $attachment->updated_at, + ]; + }), + 'created_at' => $email->created_at, + 'updated_at' => $email->updated_at, + ]; + } +} diff --git a/packages/Webkul/Admin/src/Traits/ProvideDropdownOptions.php b/packages/Webkul/Admin/src/Traits/ProvideDropdownOptions.php new file mode 100644 index 0000000..76fa8c5 --- /dev/null +++ b/packages/Webkul/Admin/src/Traits/ProvideDropdownOptions.php @@ -0,0 +1,284 @@ +booleanDropdownChoices); + } + + /** + * Get boolean dropdown options. + * + * @param string $choice + */ + public function getBooleanDropdownOptions($choice = 'active_inactive'): array + { + return $this->isBooleanDropdownChoiceExists($choice) && $choice == 'active_inactive' + ? $this->getActiveInactiveDropdownOptions() + : $this->getYesNoDropdownOptions(); + } + + /** + * Get active/inactive dropdown options. + */ + public function getActiveInactiveDropdownOptions(): array + { + return [ + [ + 'value' => '', + 'label' => trans('admin::app.common.select-options'), + 'disabled' => true, + 'selected' => true, + ], + [ + 'label' => trans('admin::app.datagrid.active'), + 'value' => 1, + 'disabled' => false, + 'selected' => false, + ], [ + 'label' => trans('admin::app.datagrid.inactive'), + 'value' => 0, + 'disabled' => false, + 'selected' => false, + ], + ]; + } + + /** + * Get yes/no dropdown options. + */ + public function getYesNoDropdownOptions(): array + { + return [ + [ + 'value' => '', + 'label' => trans('admin::app.common.select-options'), + 'disabled' => true, + 'selected' => true, + ], + [ + 'value' => 0, + 'label' => trans('admin::app.common.no'), + 'disabled' => false, + 'selected' => false, + ], [ + 'value' => 1, + 'label' => trans('admin::app.common.yes'), + 'disabled' => false, + 'selected' => false, + ], + ]; + } + + /** + * Get user dropdown options. + */ + public function getUserDropdownOptions(): array + { + $options = app(UserRepository::class) + ->get(['id as value', 'name as label']) + ->map(function ($item, $key) { + $item->disabled = false; + + $item->selected = false; + + return $item; + }) + ->toArray(); + + return [ + [ + 'label' => trans('admin::app.common.select-users'), + 'value' => '', + 'disabled' => true, + 'selected' => true, + ], + ...$options, + ]; + } + + /** + * Get lead source options. + */ + public function getLeadSourcesOptions(): array + { + $options = app(SourceRepository::class) + ->get(['id as value', 'name as label']) + ->map(function ($item, $key) { + $item->disabled = false; + + $item->selected = false; + + return $item; + }) + ->toArray(); + + return [ + [ + 'label' => trans('admin::app.common.select-users'), + 'value' => '', + 'disabled' => true, + 'selected' => true, + ], + ...$options, + ]; + } + + /** + * Get organization dropdown options. + */ + public function getOrganizationDropdownOptions(): array + { + $options = app(OrganizationRepository::class) + ->get(['id as value', 'name as label']) + ->map(function ($item, $key) { + $item->disabled = false; + + $item->selected = false; + + return $item; + }) + ->toArray(); + + return [ + [ + 'label' => trans('admin::app.common.select-organization'), + 'value' => '', + 'disabled' => true, + 'selected' => true, + ], + ...$options, + ]; + } + + /** + * Get role dropdown options. + */ + public function getRoleDropdownOptions(): array + { + return [ + [ + 'label' => trans('admin::app.settings.roles.all'), + 'value' => 'all', + 'disabled' => false, + 'selected' => false, + ], [ + 'label' => trans('admin::app.settings.roles.custom'), + 'value' => 'custom', + 'disabled' => false, + 'selected' => false, + ], + ]; + } + + /** + * Get activity type dropdown options. + */ + public function getActivityTypeDropdownOptions(): array + { + return [ + [ + 'label' => trans('admin::app.common.select-type'), + 'value' => '', + 'disabled' => true, + 'selected' => true, + ], [ + 'label' => trans('admin::app.common.select-call'), + 'value' => 'call', + 'disabled' => false, + 'selected' => false, + ], [ + 'label' => trans('admin::app.common.select-meeting'), + 'value' => 'meeting', + 'disabled' => false, + 'selected' => false, + ], [ + 'label' => trans('admin::app.common.select-lunch'), + 'value' => 'lunch', + 'disabled' => false, + 'selected' => false, + ], + ]; + } + + /** + * Get attribute type dropdown options. + */ + public function getAttributeTypeDropdownOptions(): array + { + return [ + [ + 'label' => trans('admin::app.common.select-options'), + 'value' => '', + 'disabled' => true, + 'selected' => true, + ], + [ + 'label' => trans('admin::app.common.system_attribute'), + 'value' => '0', + 'disabled' => false, + 'selected' => false, + ], + [ + 'label' => trans('admin::app.common.custom_attribute'), + 'value' => '1', + 'disabled' => false, + 'selected' => false, + ], + ]; + } + + /** + * Get organization dropdown options. + */ + public function getWarehouseDropdownOptions(): array + { + $options = app(WarehouseRepository::class) + ->get(['id as value', 'name as label']) + ->map(function ($item, $key) { + $item->disabled = false; + + $item->selected = false; + + return $item; + }) + ->toArray(); + + return [ + [ + 'label' => trans('admin::app.common.select-warehouse'), + 'value' => '', + 'disabled' => true, + 'selected' => true, + ], + ...$options, + ]; + } +} diff --git a/packages/Webkul/Admin/tailwind.config.js b/packages/Webkul/Admin/tailwind.config.js new file mode 100644 index 0000000..7fe663a --- /dev/null +++ b/packages/Webkul/Admin/tailwind.config.js @@ -0,0 +1,49 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/Resources/**/*.blade.php", "./src/Resources/**/*.js"], + + theme: { + container: { + center: true, + + screens: { + "4xl": "1920px", + }, + + padding: { + DEFAULT: "16px", + }, + }, + + screens: { + sm: "525px", + md: "768px", + lg: "1024px", + xl: "1240px", + "2xl": "1440px", + "3xl": "1680px", + "4xl": "1920px", + }, + + extend: { + colors: { + brandColor: "var(--brand-color)", + }, + + fontFamily: { + inter: ['Inter'], + icon: ['icomoon'] + } + }, + }, + + darkMode: 'class', + + plugins: [], + + safelist: [ + { + pattern: /icon-/, + } + ] +}; \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/.gitignore b/packages/Webkul/Admin/tests/e2e-pw/.gitignore new file mode 100644 index 0000000..52e84d7 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/.gitignore @@ -0,0 +1,2 @@ +/playwright-report +/test-results diff --git a/packages/Webkul/Admin/tests/e2e-pw/.state/admin-auth.json b/packages/Webkul/Admin/tests/e2e-pw/.state/admin-auth.json new file mode 100644 index 0000000..862f9cf --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/.state/admin-auth.json @@ -0,0 +1,35 @@ +{ + "cookies": [ + { + "name": "XSRF-TOKEN", + "value": "eyJpdiI6IlAzOTFCLzFpTWxrdEswSzM4QmNOUkE9PSIsInZhbHVlIjoiNzZTUGNXeDYwWjJSVlRCbnd5NkU3dlg3NHAwZGM2ajlpTnBlMWQvRStHTkNQWkZjYml5NnFUUGpoTFFuZ3Jna01BUXRoSENFSVN5Y05RdjFvNU5IWjJYZStOTHJiNTNWVVZmbWpmMWVFaE1xWG5xTjkrZW9tZ2ZOU3J5QnNhRmMiLCJtYWMiOiI3MTJhZDM1MzZhMDU2OTVkYmRjZTEwOTRkMjkzYTIyNjcxZTk4MThiNjkwZjQ2NzIzZmUzYTJhM2Y5ZjkyMGE0IiwidGFnIjoiIn0%3D", + "domain": "192.168.15.131", + "path": "/", + "expires": 1787127131.04971, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "krayin_crm_session", + "value": "eyJpdiI6IktDWkpDVEZRcFRwYkViTzd3bXZVZlE9PSIsInZhbHVlIjoiajRTQlpyQ1BobjArZWo1dlFmMy81eXZyUGJXNW5LR2xKOWFiVGd2c0t5dWZ6V0hmdFBGQ0VpVDFyUkFraFBZa0NHbWpxMjdxdkpaWENnTldZenNFN2htSzQ2ajh3YkdmMUJMeEthK0l5YzU3cFhvcWw1aktPMVpBYjZVM0tXQ1YiLCJtYWMiOiI2NmY4Y2Q5OWQ4Yjc2YjU3ZDAwMDI5ODMyOTE0YjY3ZTM5NGJkNzYwZTdiNGVjMmQ5M2U4NjM5ZGIxNzUwZjk2IiwidGFnIjoiIn0%3D", + "domain": "192.168.15.131", + "path": "/", + "expires": 1787127131.049837, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [ + { + "origin": "http://192.168.15.131", + "localStorage": [ + { + "name": "phpdebugbar-height", + "value": "300" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/leads.csv b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/leads.csv new file mode 100644 index 0000000..575889c --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/leads.csv @@ -0,0 +1,3 @@ +id,title,description,lead_value,status,lost_reason,closed_at,user_id,person_id,lead_source_id,lead_type_id,lead_pipeline_id,product,lead_pipeline_stage_id,expected_close_date +1,Updated Urgent Requirement of Product -> TaskMaster Pro - Advanced Project Management Tool,"Urgent Requirement: TaskMaster Pro – an advanced project management tool designed to enhance team collaboration, streamline task tracking, and optimize time management for small to medium-sized teams. Ideal for businesses looking to improve project efficiency and productivity.",50000,1,,2024-08-30 14:39:58,1,19,1,1,1,"price=45,quantity=2,id=1",5,2024-09-02 +,Need more information About Product -> CyberGuard Pro,A client wants to know about the CyberGuard Pro Product in details and he is interested in the product.,100000,1,,,1,19,1,1,1,"price=45,quantity=2,id=2",3,2024-09-06 \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/leads_updated.csv b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/leads_updated.csv new file mode 100644 index 0000000..68a8004 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/leads_updated.csv @@ -0,0 +1,3 @@ +id,title,description,lead_value,status,lost_reason,closed_at,user_id,person_id,lead_source_id,lead_type_id,lead_pipeline_id,product,lead_pipeline_stage_id,expected_close_date +1,Urgent Requirement of Product -> TaskMaster Pro - Advanced Project Management Tool," Requirement: TaskMaster Pro – an advanced project management tool designed to enhance team collaboration, streamline task tracking, and optimize time management for small to medium-sized teams. Ideal for businesses looking to improve project efficiency and productivity.",50000,1,,2024-08-30 14:39:58,1,19,2,2,1,"price=45,quantity=2,id=1",5,2024-09-02 +2,Need more information About Product -> Pro,A wants to know about the CyberGuard Pro Product in details and he is interested in the product.,100000,1,,,1,19,4,1,1,"price=45,quantity=2,id=2",3,2024-09-06 \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/persons.csv b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/persons.csv new file mode 100644 index 0000000..7a57f13 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/persons.csv @@ -0,0 +1,3 @@ +name,emails,contact_numbers,organization_id,job_title,user_id +Wilson Fisk,"[{""label"": ""work"", ""value"": ""contact@wilson.com""}, {""label"": ""home"", ""value"": ""contact.home@wilson.com""}]","[{""label"": ""work"", ""value"": ""5454445454""}]",7,Sales Executive,1 +Sasha Calle,"[{""label"": ""work"", ""value"": ""contact@sasha.com""}]","[{""label"": ""work"", ""value"": ""15454445454""}]",7,Sales Representatives,1 \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/persons_updated.csv b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/persons_updated.csv new file mode 100644 index 0000000..0fdc950 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/persons_updated.csv @@ -0,0 +1,3 @@ +name,emails,contact_numbers,organization_id,job_title,user_id +Wilson Updated Fisk,"[{""label"": ""work"", ""value"": ""contact@wilson.com""}, {""label"": ""home"", ""value"": ""contact.home@wilson.com""}]","[{""label"": ""work"", ""value"": ""5454445454""}]",7,Sales Executive,1 +Sasha Updated Calle,"[{""label"": ""work"", ""value"": ""contact@sasha.com""}]","[{""label"": ""work"", ""value"": ""15454445454""}]",7,Sales Representatives,1 \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/products.csv b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/products.csv new file mode 100644 index 0000000..b27b317 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/products.csv @@ -0,0 +1,3 @@ +sku,name,description,quantity,price +100,TaskMaster Pro - Advanced Project Management Tool,"A project management tool designed for small to medium-sized teams. It offers task tracking, team collaboration, and time management features. It also integrates with popular communication platforms.",45,500 +101,ProjectPulse - A Simple and Effective Project Management Tool,"A cloud-based project management solution that provides Gantt charts, resource allocation, and project reporting. It is designed for large enterprises and supports multi-project management with real-time analytics.",20,450 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/products_updated.csv b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/products_updated.csv new file mode 100644 index 0000000..e64286e --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/data/data-transfer/products_updated.csv @@ -0,0 +1,3 @@ +sku,name,description,quantity,price +100,TaskMaster updated Pro - Advanced Project Management Tool,"A project management tool designed for small to medium-sized teams. It offers task tracking, team collaboration, and time management features. It also integrates with popular communication platforms.",45,500 +101,ProjectPulse updated - A Simple and Effective Project Management Tool,"A cloud-based project management solution that provides Gantt charts, resource allocation, and project reporting. It is designed for large enterprises and supports multi-project management with real-time analytics.",234,450 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/images/10.webp b/packages/Webkul/Admin/tests/e2e-pw/data/images/10.webp new file mode 100644 index 0000000000000000000000000000000000000000..933f80102fe3f48861bdf67ea6c4099e98ef1094 GIT binary patch literal 29098 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 literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/images/11.webp b/packages/Webkul/Admin/tests/e2e-pw/data/images/11.webp new file mode 100644 index 0000000000000000000000000000000000000000..9a9bdb2c7a82424955d0cc0d892c5d4a3d87b7c2 GIT binary patch literal 21748 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 literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/images/3.webp b/packages/Webkul/Admin/tests/e2e-pw/data/images/3.webp new file mode 100644 index 0000000000000000000000000000000000000000..8fd079916a9ca3a928928d0b8eec0dd921fa0e00 GIT binary patch literal 123028 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 literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/images/4.webp b/packages/Webkul/Admin/tests/e2e-pw/data/images/4.webp new file mode 100644 index 0000000000000000000000000000000000000000..c22001306d06ba1197096909f0a4e1266181d346 GIT binary patch literal 90892 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

    <0WN2A?;{sTYC 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 L(PSM!00000d>&+q literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/images/6.webp b/packages/Webkul/Admin/tests/e2e-pw/data/images/6.webp new file mode 100644 index 0000000000000000000000000000000000000000..fbd3733f3ddab88d65a5604f8e47c287651a8039 GIT binary patch literal 36952 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 ACjbBd literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/images/7.webp b/packages/Webkul/Admin/tests/e2e-pw/data/images/7.webp new file mode 100644 index 0000000000000000000000000000000000000000..aae603cbe1b71528094a47e5d322b17582be7e6a GIT binary patch literal 24766 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 literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/images/8.webp b/packages/Webkul/Admin/tests/e2e-pw/data/images/8.webp new file mode 100644 index 0000000000000000000000000000000000000000..933f80102fe3f48861bdf67ea6c4099e98ef1094 GIT binary patch literal 29098 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 literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/tests/e2e-pw/data/images/9.webp b/packages/Webkul/Admin/tests/e2e-pw/data/images/9.webp new file mode 100644 index 0000000000000000000000000000000000000000..767430eaa781fc800f8422842f169365381ed4f2 GIT binary patch literal 27758 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 F007_8wgUhF literal 0 HcmV?d00001 diff --git a/packages/Webkul/Admin/tests/e2e-pw/fixtures/AdminFixtures.ts b/packages/Webkul/Admin/tests/e2e-pw/fixtures/AdminFixtures.ts new file mode 100644 index 0000000..c1883f8 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/fixtures/AdminFixtures.ts @@ -0,0 +1,80 @@ +import { test as base, expect, type Page } from "@playwright/test"; +import fs from "fs"; +import { ADMIN_AUTH_STATE_PATH } from "../playwright.config"; + +import { AdminPage as Admin } from "../pages/AdminPage"; + +interface AdminPage extends Page { + fillInTinymce: (iframeSelector: string, content: string) => Promise; +} + +type AdminFixtures = { + adminPage: AdminPage; +}; + +export const test = base.extend({ + adminPage: async ({ browser }, use) => { + const authExists = fs.existsSync(ADMIN_AUTH_STATE_PATH); + + const context = await browser.newContext( + authExists ? { storageState: ADMIN_AUTH_STATE_PATH } : {} + ); + + const page = await context.newPage(); + const admin = await new Admin(page) + + + if (!authExists) { + /** + * Authenticate the admin user. + */ + await admin.adminLogin("admin@example.com", "admin123"); + + /** + * Save authentication state to a file. + */ + await context.storageState({ path: ADMIN_AUTH_STATE_PATH }); + } else { + /** + * Navigate to the dashboard. + */ + await page.goto("admin/dashboard"); + } + + if (page.url().includes("admin/login")) { + /** + * Authenticate the admin user. + */ + await admin.adminLogin("admin@example.com", "admin123"); + + /** + * Save authentication state to a file. + */ + await context.storageState({ path: ADMIN_AUTH_STATE_PATH }); + } + + /** + * Extend the page object with custom methods.} + + */ + (page as AdminPage).fillInTinymce = async function ( + iframeSelector: string, + content: string + ) { + await page.waitForSelector(iframeSelector); + const iframe = page.frameLocator(iframeSelector); + const editorBody = iframe.locator("body"); + await editorBody.click(); + await editorBody.press("Control+a"); + await editorBody.press("Backspace"); + await editorBody.pressSequentially(content); + await expect(editorBody).toHaveText(content); + }; + + await use(page as AdminPage); + + await context.close(); + }, +}); + +export { expect }; \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/locator/CoreLocators.ts b/packages/Webkul/Admin/tests/e2e-pw/locator/CoreLocators.ts new file mode 100644 index 0000000..0a86d3a --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/locator/CoreLocators.ts @@ -0,0 +1,597 @@ +import { LegacyCharacterEncoding } from "crypto"; +import { expect, FrameLocator, Locator, Page } from "playwright/test"; + +type ElementType = "button" | "textbox" | "link"; + +export default class CoreLocators { + readonly page: Page; + + // Person Locators + readonly createPersonLink: Locator; + readonly savePersonButton: Locator; + readonly personNameTextbox: Locator; + readonly personEmailTextbox: Locator; + readonly personPhoneTextbox: Locator; + readonly personJobTitleTextbox: Locator; + readonly personOrgSelectDiv: Locator; + readonly personOrgListItem: (text: string) => Locator; + + // Links and buttons + readonly createOrgLink: Locator; + readonly saveOrganizationButton: Locator; + + // Textboxes + readonly orgNameTextbox: Locator; + readonly orgCityTextbox: Locator; + readonly orgPostcodeTextbox: Locator; + + // Textareas + readonly orgAddressTextarea: Locator; + + // Select dropdowns + readonly orgCountryCombobox: Locator; + readonly orgStateSelect: Locator; + + // Other UI elements + readonly orgExtraDetailsDiv: Locator; + readonly orgExampleListItem: (text: string) => Locator; + + // Buttons + readonly saveQuoteButton: Locator; + readonly saveProductsButton: Locator; + readonly confirmDeleteButton: Locator; + readonly saveLeadButton: Locator; + readonly sendEmailButton: Locator; + readonly saveFileButton: Locator; + readonly saveNoteButton: Locator; + readonly saveActivityButton: Locator; + readonly signInButton: Locator; + readonly createEventButton: Locator; + readonly saveEventButton: Locator; + readonly agreeButton: Locator; + readonly createTypeButton: Locator; + readonly saveTypeButton: Locator; + readonly createGroupButton: Locator; + readonly saveGroupButton: Locator; + readonly createUserButton: Locator; + readonly saveUserButton: Locator; + readonly composeMailButton: Locator; + readonly createOrganizationButton: Locator; + readonly deleteButton: Locator; + + // Links + readonly createQuoteLink: Locator; + readonly createProductLink: Locator; + readonly createLeadLink: Locator; + readonly signOutLink: Locator; + readonly createOrganizationLink: Locator; + + // Textboxes - single line text input fields + readonly subjectTextbox: Locator; + readonly descriptionTextbox: Locator; + readonly searchInputField: Locator; + readonly productNameTextbox: Locator; + readonly productPriceInput: Locator; + readonly productQuantityInput: Locator; + readonly productSkuInput: Locator; + readonly leadTitleInput: Locator; + readonly leadValueInput: Locator; + readonly leadProductSelect: Locator; + readonly emailReplyToInput: Locator; + readonly emailSubjectInput: Locator; + readonly fileTitleInput: Locator; + readonly fileNameInput: Locator; + readonly noteCommentTextarea: Locator; + readonly activityTitleInput: Locator; + readonly activityLocationInput: Locator; + readonly eventNameInput: Locator; + readonly typeNameInput: Locator; + readonly groupNameInput: Locator; + readonly userNameInput: Locator; + readonly userEmailInput: Locator; + readonly userPasswordInput: Locator; + readonly userConfirmPasswordInput: Locator; + readonly organizationNameInput: Locator; + readonly organizationCityTextbox: Locator; + readonly organizationAddressTextarea: Locator; + readonly organizationPostcodeTextbox: Locator; + readonly organizationCountryCombobox: Locator; + readonly organizationStateSelect: Locator; + + readonly addPersonButton: Locator; + + // Textboxes - date or specialized text input + readonly expiredAtTextbox: Locator; + readonly leadExpectedCloseDateInput: Locator; + readonly activityScheduleFromInput: Locator; + readonly activityScheduleToInput: Locator; + readonly eventDateInput: Locator; + + // Textareas - multi-line input fields + readonly billingAddressTextarea: Locator; + readonly shippingAddressTextarea: Locator; + readonly productDescriptionTextarea: Locator; + readonly leadDescriptionTextarea: Locator; + readonly emailReplyTextarea: Locator; + readonly fileCommentTextarea: Locator; + readonly noteTextarea: Locator; + readonly activityCommentTextarea: Locator; + readonly eventDescriptionTextarea: Locator; + readonly groupDescriptionTextarea: Locator; + + // Inputs - single line, no explicit role + readonly billingCityInput: Locator; + readonly billingPostcodeInput: Locator; + readonly shippingCityInput: Locator; + readonly shippingPostcodeInput: Locator; + readonly leadEmailInput: Locator; + readonly leadPhoneNumberInput: Locator; + + // Select dropdowns + readonly salesOwnerSelect: Locator; + readonly billingCountrySelect: Locator; + readonly billingStateSelect: Locator; + readonly shippingCountrySelect: Locator; + readonly shippingStateSelect: Locator; + readonly leadSourceSelect: Locator; + readonly leadTypeSelect: Locator; + readonly leadUserSelect: Locator; + readonly leadSourceLabelSelect: Locator; + readonly leadOrganizationSelect: Locator; + readonly userRoleSelect: Locator; + readonly userViewPermissionSelect: Locator; + readonly userGroupListbox: Locator; + readonly leadProductAddMoreButton: Locator; + readonly quoteLinkToLeadButton: Locator; + + // Search related list item selector + readonly personListItem: (name: string) => Locator; + + // Lead form - person search dropdown + readonly leadPersonSearchInput: Locator; + + // Lead form - product search dropdown + readonly leadProductSearchInput: Locator; + + readonly quoteAddPersonButton: Locator; + readonly addAsNewButton: Locator; + readonly quoteSelectListPerson: Locator; + + // Edit and delete icons + readonly firstEditIcon: Locator; + readonly firstDeleteIcon: Locator; + // Multi select checkbox + readonly multiSelectCheckbox: Locator; + + // Activity modal selectors + readonly addActivityButton: Locator; + readonly activityCallOption: Locator; + readonly activityMeetingOption: Locator; + readonly activityLunchOption: Locator; + + // Login fields + readonly emailPlaceholderInput: Locator; + readonly passwordPlaceholderInput: Locator; + readonly megaSearchPlaceholder: Locator; + + // Profile and logout buttons + readonly profileButton: Locator; + // Lead + readonly createLeadButton: Locator; + + // Lead form + readonly titleInput: Locator; + readonly descriptionTextarea: Locator; + readonly sourceDropdown: Locator; + readonly expectedCloseDate: Locator; + readonly typeDropdown: Locator; + readonly userDropdown: Locator; + readonly searchInput: Locator; + readonly listSearchInput: Locator; + + // Add person + readonly personEmailInput: Locator; + readonly personPhoneInput: Locator; + readonly successMessage: Locator; + + // Add organization + readonly addOrganizationButton: Locator; + readonly organizationCreateSuccessMessage: Locator; + + // General + readonly leadSuccessToast: Locator; + readonly editLeadButton: Locator; + readonly listViewButton: Locator; + readonly deleteLeadButton: Locator; + readonly noRecordsAvailable: Locator + readonly searchInputExact:Locator; + readonly listViewLeadSearch: Locator; + // Tabs + readonly mailButton: Locator; + readonly fileButton: Locator; + readonly noteButton: Locator; + readonly activityButton: Locator; + + // Mail + readonly mailToInput: Locator; + readonly mailSubjectInput: Locator; + readonly mailBodyTextarea: Locator; + readonly sendMailButton: Locator; + + // File + + // Note + + // Activity + readonly addActivityTitleInput: Locator; + readonly addActivityCommentTextarea: Locator; + readonly scheduleFromInput: Locator; + readonly scheduleToInput: Locator; + readonly locationInput: Locator; + + // Validation message + readonly expectedCloseDateMustBeDateAfter: Locator; + + + + // Other + readonly appLocator: Locator; + + // Quick Add + readonly quickAddTrigger: Locator; + readonly quickAddModalTitle: Locator; + readonly quickAddTab: (tabLabel: string) => Locator; + readonly quickAddSaveButton: Locator; + + readonly quickAddLeadForm: Locator; + readonly quickAddPersonForm: Locator; + readonly quickAddOrganizationForm: Locator; + readonly quickAddProductForm: Locator; + readonly quickAddMailForm: Locator; + + readonly quickAddLeadTitleInput: Locator; + readonly quickAddLeadDescriptionTextarea: Locator; + + readonly quickAddPersonNameInput: Locator; + readonly quickAddPersonEmailInput: Locator; + readonly quickAddPersonContactInput: Locator; + + readonly quickAddOrgNameInput: Locator; + + readonly quickAddProductNameInput: Locator; + readonly quickAddProductDescriptionTextarea: Locator; + readonly quickAddProductSkuInput: Locator; + readonly quickAddProductQuantityInput: Locator; + readonly quickAddProductPriceInput: Locator; + + readonly quickAddMailReplyToInput: Locator; + readonly quickAddMailCcInput: Locator; + readonly quickAddMailBccInput: Locator; + readonly quickAddMailCcButton: Locator; + readonly quickAddMailBccButton: Locator; + readonly quickAddMailSubjectInput: Locator; + readonly quickAddMailEditorFrame: FrameLocator; + + readonly quickAddLeadSuccessMsg: Locator; + readonly quickAddPersonSuccessMsg: Locator; + readonly quickAddOrgSuccessMsg: Locator; + readonly quickAddProductSuccessMsg: Locator; + readonly quickAddMailSuccessMsg: Locator; + + constructor(page: Page) { + this.page = page; + + // Buttons + this.saveQuoteButton = page.getByRole("button", { name: "Save Quote" }); + this.saveProductsButton = page.getByRole("button", { name: "Save Products" }); + this.confirmDeleteButton = page.locator("button.transparent-button + button.primary-button:visible"); + this.saveLeadButton = page.getByRole("button", { name: "Save" }); + this.sendEmailButton = page.getByRole("button", { name: "Send" }); + this.saveFileButton = page.getByRole("button", { name: "Save File" }); + this.saveNoteButton = page.getByRole("button", { name: "Save Note" }); + this.saveActivityButton = page.getByRole("button", { name: "Save Activity" }); + this.signInButton = page.getByRole("button", { name: "Sign In" }); + this.createEventButton = page.getByRole("button", { name: "Create Event" }); + this.saveEventButton = page.getByRole("button", { name: "Save Event" }); + this.agreeButton = page.getByRole("button", { name: "Agree", exact: true }); + this.createTypeButton = page.getByRole("button", { name: "Create Type" }); + this.saveTypeButton = page.getByRole("button", { name: "Save Type" }); + this.createGroupButton = page.getByRole("button", { name: "Create group" }); + this.saveGroupButton = page.getByRole("button", { name: "Save Group" }); + this.createUserButton = page.getByRole("button", { name: "Create User" }); + this.saveUserButton = page.getByRole("button", { name: "Save User" }); + this.composeMailButton = page.getByRole("button", { name: "Compose Mail" }); + this.createOrganizationButton = page.getByRole("link", { name: "Create Organization" }); + this.saveOrganizationButton = page.getByRole("button", { name: "Save Organization" }); + this.deleteButton = page.getByRole("button", { name: "Delete" }); + this.leadProductSelect = page.locator('.mb-4.\\!mb-0 > div > .relative.inline-block > .relative').first(); + this.leadProductAddMoreButton = page.locator('//button[@class="flex max-w-max items-center gap-2 text-brandColor"]'); + this.leadOrganizationSelect = page.locator("div").filter({ hasText: /^Click to add$/ }).nth(2); + this.addPersonButton = page.locator("div", { hasText: /^Click to Add$/ }).nth(1); + this.quoteAddPersonButton = page.locator(".relative.flex.items-center.justify-between").first(); + + // Links + this.createQuoteLink = page.getByRole("link", { name: "Create Quote" }); + this.createProductLink = page.getByRole("link", { name: "Create Product" }); + this.createLeadLink = page.getByRole("link", { name: "Create Lead" }); + this.signOutLink = page.getByRole("link", { name: "Sign Out" }); + this.createOrganizationLink = page.getByRole("link", { name: "Create Organization" }); + + this.addAsNewButton = page.getByText("Add as New"); + this.quoteSelectListPerson = page.locator(`(//li[@class="flex cursor-pointer gap-2 p-2 transition-colors hover:bg-blue-100 dark:text-gray-300 dark:hover:bg-gray-900"])[1]`); + + // Textboxes - single line text input fields + this.subjectTextbox = page.getByRole("textbox", { name: "Subject *" }); + this.descriptionTextbox = page.getByRole("textbox", { name: "Description" }); + + this.productNameTextbox = page.getByRole("textbox", { name: "Name *" }); + this.productPriceInput = page.locator('input[name="price"]'); + this.productQuantityInput = page.locator('input[name="quantity"]'); + this.productSkuInput = page.locator('input[name="sku"]'); + this.leadTitleInput = page.locator('input[name="title"]'); + this.leadValueInput = page.locator('input[name="lead_value"]'); + this.emailReplyToInput = page.locator('input[name="temp-reply_to"]'); + this.emailSubjectInput = page.locator('input[name="subject"]'); + this.fileTitleInput = page.locator('input[name="title"]'); + this.fileNameInput = page.locator('input[name="name"]'); + this.noteCommentTextarea = page.locator('textarea[name="comment"]'); + this.activityTitleInput = page.locator('input[name="title"]'); + this.activityLocationInput = page.locator('input[name="location"]'); + this.eventNameInput = page.locator('input[name="name"]'); + this.typeNameInput = page.locator('input[name="name"]'); + this.groupNameInput = page.locator('input[name="name"]'); + this.userNameInput = page.locator('input[name="name"]'); + this.userEmailInput = page.locator('input[name="email"]'); + this.userPasswordInput = page.locator('input[name="password"]'); + this.userConfirmPasswordInput = page.locator('input[name="confirm_password"]'); + this.organizationNameInput = page.locator('input[name="name"]'); + this.organizationCityTextbox = page.getByRole('textbox', { name: 'City' }); + this.organizationAddressTextarea = page.locator('textarea[name="address\\[address\\]"]'); + this.organizationPostcodeTextbox = page.getByRole('textbox', { name: 'Postcode' }); + this.organizationCountryCombobox = page.getByRole('combobox'); + this.organizationStateSelect = page.locator('select[name="address\\[state\\]"]'); + + // Textboxes - date or specialized text input + this.expiredAtTextbox = page.getByRole("textbox", { name: "Expired At *" }); + this.leadExpectedCloseDateInput = page.locator('input[name="expected_close_date"]'); + this.activityScheduleFromInput = page.locator('input[name="schedule_from"]'); + this.activityScheduleToInput = page.locator('input[name="schedule_to"]'); + this.eventDateInput = page.locator('input[name="date"]'); + + this.noRecordsAvailable = page.getByText('No Records Available.') + + // Textareas - multi-line input fields + this.billingAddressTextarea = page.locator('textarea[name="billing_address\\[address\\]"]'); + this.shippingAddressTextarea = page.locator('textarea[name="shipping_address\\[address\\]"]'); + this.productDescriptionTextarea = page.locator('textarea[name="description"]'); + this.leadDescriptionTextarea = page.locator('textarea[name="description"]'); + this.emailReplyTextarea = page.locator('textarea[name="reply"]'); + this.fileCommentTextarea = page.locator('textarea[name="comment"]'); + this.noteTextarea = page.locator('textarea[name="comment"]'); + this.activityCommentTextarea = page.locator('textarea[name="comment"]'); + this.eventDescriptionTextarea = page.locator('textarea[name="description"]'); + this.groupDescriptionTextarea = page.locator('textarea[name="description"]'); + + // Inputs - single line, no explicit role + this.billingCityInput = page.locator('input[name="billing_address\\[city\\]"]'); + this.billingPostcodeInput = page.locator('input[name="billing_address\\[postcode\\]"]'); + this.shippingCityInput = page.locator('input[name="shipping_address\\[city\\]"]'); + this.shippingPostcodeInput = page.locator('input[name="shipping_address\\[postcode\\]"]'); + this.leadEmailInput = page.locator('input[name="person[emails][0][value]"]'); + this.leadPhoneNumberInput = page.locator('input[name="person[contact_numbers][0][value]"]'); + + // Select dropdowns + this.salesOwnerSelect = page.getByLabel("Sales Owner"); + this.billingCountrySelect = page.locator('select[name="billing_address\\[country\\]"]'); + this.billingStateSelect = page.locator('select[name="billing_address\\[state\\]"]'); + this.shippingCountrySelect = page.locator('select[name="shipping_address\\[country\\]"]'); + this.shippingStateSelect = page.locator('select[name="shipping_address\\[state\\]"]'); + this.leadSourceSelect = page.locator('select[name="lead_source_id"]'); + this.leadTypeSelect = page.locator('select[name="lead_type_id"]'); + this.leadUserSelect = page.locator('select[name="user_id"]'); + this.leadSourceLabelSelect = page.getByLabel('Source'); + this.userRoleSelect = page.locator('select[name="role_id"]'); + this.userViewPermissionSelect = page.locator('select[name="view_permission"]'); + this.userGroupListbox = page.getByRole('listbox'); + + // Search related list item selector + this.personListItem = (name: string) => page.getByRole("listitem").filter({ hasText: name }); + + // Edit and delete icons + this.firstEditIcon = page.locator("span.cursor-pointer.icon-edit").first(); + this.firstDeleteIcon = page.locator("span.cursor-pointer.icon-delete").first(); + + //mass delte checkbox + this.multiSelectCheckbox = page.locator('.icon-checkbox-outline').first(); + + this.quoteLinkToLeadButton = page.locator('div:nth-child(2) > div > .relative.inline-block > .relative'); + + // Activity modal selectors + this.addActivityButton = page.getByRole("button", { name: " Activity" }); + this.activityCallOption = page.getByText("Call", { exact: true }); + this.activityMeetingOption = page.getByText("Meeting", { exact: true }); + this.activityLunchOption = page.getByText("Lunch", { exact: true }); + + // Login fields + this.emailPlaceholderInput = page.getByPlaceholder("Email Address"); + this.passwordPlaceholderInput = page.getByPlaceholder("Password"); + this.megaSearchPlaceholder = page.getByPlaceholder("Mega Search").first(); + + // Profile and logout buttons + this.profileButton = page.getByRole('button', { name: "E" }); + + // Organization Locators Initialization + this.createOrgLink = page.getByRole('link', { name: 'Create Organization' }); + this.orgNameTextbox = page.getByRole('textbox', { name: 'Name *' }); + this.orgAddressTextarea = page.locator('textarea[name="address\\[address\\]"]'); + this.orgCountryCombobox = page.getByRole('combobox'); + this.orgStateSelect = page.locator('select[name="address\\[state\\]"]'); + this.orgCityTextbox = page.getByRole('textbox', { name: 'City' }); + this.orgPostcodeTextbox = page.getByRole('textbox', { name: 'Postcode' }); + + this.orgExtraDetailsDiv = page.locator('div').filter({ hasText: /^Click to add$/ }); + this.orgExampleListItem = (text: string) => page.getByRole('listitem').filter({ hasText: text }); + + // Person Locators Initialization + this.createPersonLink = page.getByRole('link', { name: 'Create Person' }); + this.savePersonButton = page.getByRole('button', { name: 'Save Person' }); + this.personNameTextbox = page.getByRole('textbox', { name: 'Name *' }); + this.personEmailTextbox = page.getByRole('textbox', { name: 'Emails *' }); + this.personPhoneTextbox = page.getByRole('textbox', { name: 'Contact Numbers' }); + this.personJobTitleTextbox = page.getByRole('textbox', { name: 'Job Title' }); + this.personOrgSelectDiv = page.locator('.relative > div > .relative').first(); + + this.personOrgListItem = (text: string) => page.getByRole('listitem').filter({ hasText: text }); + + + // Lead + this.createLeadButton = page.getByRole('link', { name: 'Create Lead' }); + + // Lead form + this.titleInput = page.getByRole('textbox', { name: 'Title *' }); + this.descriptionTextarea = page.locator('textarea[name="description"]'); + this.sourceDropdown = page.locator('select[name="lead_source_id"]'); + this.expectedCloseDate = page.locator('input[name="expected_close_date"]'); + this.typeDropdown = page.locator('select[name="lead_type_id"]'); + this.userDropdown = page.locator('select[name="user_id"]'); + this.leadValueInput = page.locator('input[name="lead_value"]'); + this.listViewLeadSearch= page.getByRole('textbox', { name: 'Search', exact: true }); + this.searchInput = page.getByRole('textbox', { name: 'Search by Title' }); + this.listSearchInput = page.getByRole('textbox', { name: 'Search' }); + + // Lead form - person search dropdown + this.leadPersonSearchInput = page.getByPlaceholder("Search by name, email and number"); + + // Lead form - product search dropdown + this.leadProductSearchInput = page.getByRole('textbox', { name: 'Product Name' }); + + // Add person + + this.personEmailInput = page.locator('input[name="person[emails][0][value]"]'); + this.personPhoneInput = page.locator('input[name="person[contact_numbers][0][value]"]'); + this.successMessage = page.getByText('Success'); + + // Add organization + this.addOrganizationButton = page.locator('div', { hasText: /^Click to add$/ }).nth(2); + + + this.saveLeadButton = page.getByRole('button', { name: 'Save' }); + this.organizationCreateSuccessMessage = page.getByText('Organization created successfully'); + + // General + + this.leadSuccessToast = page.getByText('Success', { exact: true }); + this.editLeadButton = page.getByRole('link', { name: '' }).first(); + this.listViewButton = page.getByRole('link', { name: '' }); + this.agreeButton = page.getByRole('button', { name: 'Agree', exact: true }); + this.deleteLeadButton = page.locator( + '.cursor-pointer.rounded-md.p-1\\.5.text-2xl.transition-all.hover\\:bg-gray-200.dark\\:hover\\:bg-gray-800.max-sm\\:place-self-center.icon-delete' + ).first(); + this.searchInputField = page.getByRole("textbox", { name: "Search..." }); + this.searchInputExact = page.getByRole("textbox", { name: "Search", exact: true }); + + + // Tabs + this.mailButton = page.getByRole('button', { name: ' Mail' }); + this.fileButton = page.getByRole('button', { name: ' File' }); + this.noteButton = page.getByRole('button', { name: ' Note' }); + this.activityButton = page.getByRole('button', { name: ' Activity' }); + + // Mail + this.mailToInput = page.locator('input[name="temp-reply_to"]'); + this.mailSubjectInput = page.locator('input[name="subject"]'); + this.mailBodyTextarea = page.locator('textarea[name="reply"]'); + this.sendMailButton = page.getByRole('button', { name: 'Send' }); + + // File + this.fileTitleInput = page.locator('input[name="title"]'); + this.fileCommentTextarea = page.locator('textarea[name="comment"]'); + this.fileNameInput = page.locator('input[name="name"]'); + this.saveFileButton = page.getByRole('button', { name: 'Save File' }); + + // Note + this.noteCommentTextarea = page.locator('textarea[name="comment"]'); + this.saveNoteButton = page.getByRole('button', { name: 'Save Note' }); + + // Activity + this.addActivityTitleInput = page.locator('input[name="title"]'); + this.addActivityCommentTextarea = page.locator('textarea[name="comment"]'); + this.scheduleFromInput = page.locator('input[name="schedule_from"]'); + this.scheduleToInput = page.locator('input[name="schedule_to"]'); + this.locationInput = page.locator('input[name="location"]'); + this.saveActivityButton = page.getByRole('button', { name: 'Save Activity' }); + + // Validation message + this.expectedCloseDateMustBeDateAfter = page.getByText('The expected close date must be a date after'); + + + // Other + this.appLocator = page.locator("#app"); + + // Quick Add + this.quickAddTrigger = page.locator("header button.bg-brandColor").first(); + this.quickAddModalTitle = page.locator("p").filter({ hasText: "Quick Add" }); + this.quickAddTab = (tabLabel: string) => page.locator("span.cursor-pointer").filter({ hasText: new RegExp(`^${tabLabel}$`) }).first(); + this.quickAddSaveButton = page.getByRole("button", { name: "Save", exact: true }).last(); + + this.quickAddLeadForm = page.locator('form:has(input[value="lead"])').first(); + this.quickAddPersonForm = page.locator('form:has(input[value="person"])').first(); + this.quickAddOrganizationForm = page.locator('form:has(input[value="organization"])').first(); + this.quickAddProductForm = page.locator('form:has(input[value="product"])').first(); + this.quickAddMailForm = page.locator('form:has(input[name="is_draft"])').first(); + + this.quickAddLeadTitleInput = this.quickAddLeadForm.locator('input[name="title"]'); + this.quickAddLeadDescriptionTextarea = this.quickAddLeadForm.locator('textarea[name="description"]'); + + this.quickAddPersonNameInput = this.quickAddPersonForm.locator('input[name="name"]'); + this.quickAddPersonEmailInput = this.quickAddPersonForm.locator('input[name="emails[0][value]"]'); + this.quickAddPersonContactInput = this.quickAddPersonForm.locator('input[name="contact_numbers[0][value]"]'); + + this.quickAddOrgNameInput = this.quickAddOrganizationForm.locator('input[name="name"]'); + + this.quickAddProductNameInput = this.quickAddProductForm.locator('input[name="name"]'); + this.quickAddProductDescriptionTextarea = this.quickAddProductForm.locator('textarea[name="description"]'); + this.quickAddProductSkuInput = this.quickAddProductForm.locator('input[name="sku"]'); + this.quickAddProductQuantityInput = this.quickAddProductForm.locator('input[name="quantity"]'); + this.quickAddProductPriceInput = this.quickAddProductForm.locator('input[name="price"]'); + + this.quickAddMailReplyToInput = this.quickAddMailForm.locator('input[name="temp-reply_to"]'); + this.quickAddMailCcInput = this.quickAddMailForm.locator('input[name="temp-cc"]'); + this.quickAddMailBccInput = this.quickAddMailForm.locator('input[name="temp-bcc"]'); + this.quickAddMailCcButton = this.quickAddMailForm.getByText('CC', { exact: true }); + this.quickAddMailBccButton = this.quickAddMailForm.getByText('BCC', { exact: true }); + this.quickAddMailSubjectInput = this.quickAddMailForm.locator('input[name="subject"]'); + this.quickAddMailEditorFrame = this.quickAddMailForm.frameLocator('iframe.tox-edit-area__iframe'); + + this.quickAddLeadSuccessMsg = page.getByText("Lead created successfully."); + this.quickAddPersonSuccessMsg = page.getByText("Person created successfully."); + this.quickAddOrgSuccessMsg = page.getByText("Organization created successfully."); + this.quickAddProductSuccessMsg = page.getByText("Product created successfully."); + this.quickAddMailSuccessMsg = page.getByText("Email sent successfully."); + } + async searchByName(name: string) { + (await this.getElementByTypeAndName('textbox', 'Search')).fill(name); + + await this.page.keyboard.press('Enter'); + } + + async getElementByTypeAndName(type: ElementType, name: string) { + await expect(this.page.getByRole(`${type}`, { name: `${name}`, exact: true })).toBeVisible(); + return this.page.getByRole(`${type}`, { name: `${name}`, exact: true }); + } + + async selectListItmeByName(value: string) { + return this.page.getByRole('listitem').filter({ hasText: `${value}` }); + } + async massDelete() { + await this.multiSelectCheckbox.click(); + if(await this.deleteButton.isVisible()) { + await this.deleteButton.click(); + await this.agreeButton.click(); + } + + await expect(this.noRecordsAvailable).toBeVisible(); + + } + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/AclPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/AclPage.ts new file mode 100644 index 0000000..cbe4f52 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/AclPage.ts @@ -0,0 +1,146 @@ +import { expect, Locator, Page } from "@playwright/test"; + +import CoreLocators from "../locator/CoreLocators"; +import { QuickAddPage } from "./QuickAddPage"; + +/** + * Page object for asserting what a signed-in user is allowed to see and reach. + * + * The three enforcement layers are kept as separate methods on purpose, because they answer + * different questions and a suite that conflates them proves less than it looks: + * + * - `expectNavVisible` / `expectNavHidden` — menu filtering (presentation only) + * - `expectControlHidden` — Blade `bouncer()->hasPermission()` (presentation only) + * - the route probes in `utils/acl.ts` — the middleware (the real security boundary) + */ +export class AclPage extends CoreLocators { + readonly page: Page; + + readonly unauthorizedHeading: Locator; + readonly datagridRows: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + + this.unauthorizedHeading = page.getByText("401 Unauthorized"); + this.datagridRows = page.locator("div.row.grid.grid-cols-\\[2fr_1fr\\]"); + } + + /** + * A left-navigation entry, matched by the path it links to. + * + * The label is not a usable anchor: each sidebar link wraps an icon span and a `

    `, so its + * accessible name does not match the menu label cleanly. The href comes straight from the menu + * config's route, which is also what `Menu::getItems()` filters on — so this asserts against + * the same thing the code decides. + */ + navItem(path: string): Locator { + return this.page.locator(`nav a[href$="${path}"], aside a[href$="${path}"]`); + } + + async expectNavVisible(path: string): Promise { + await expect( + this.navItem(path).first(), + `a navigation entry for ${path} should appear for a user holding its permission` + ).toBeVisible(); + } + + async expectNavHidden(path: string): Promise { + await expect( + this.navItem(path), + `a navigation entry for ${path} must not appear without its permission` + ).toHaveCount(0); + } + + /** + * Assert a control is absent from the page. Used for create/edit/delete buttons that Blade + * wraps in a permission check. + */ + async expectControlHidden(name: string): Promise { + await expect( + this.page.getByRole("button", { name }), + `"${name}" must not be rendered without its permission` + ).toHaveCount(0); + } + + async expectControlVisible(name: string): Promise { + await expect( + this.page.getByRole("button", { name }).first(), + `"${name}" should be rendered for a user holding its permission` + ).toBeVisible(); + } + + /** + * Assert the browser landed on the unauthorized page rather than the requested screen. + */ + async expectUnauthorizedPage(): Promise { + await expect(this.unauthorizedHeading.first()).toBeVisible(); + } + + /** + * Visit a listing and return the visible row text, so a scope assertion can check which + * records the grid actually returned. + */ + async visibleRowText(path: string): Promise { + await this.page.goto(path); + await this.page.waitForLoadState("networkidle"); + + return (await this.page.locator("body").innerText()).toLowerCase(); + } + + /** + * Assert a listing contains a record's identifying text. + */ + async expectRecordListed(path: string, title: string): Promise { + const body = await this.visibleRowText(path); + + expect(body, `"${title}" should be visible in ${path} for this data scope`).toContain( + title.toLowerCase() + ); + } + + /** + * Assert a listing does not contain a record's identifying text — the core view-permission + * assertion. + */ + async expectRecordNotListed(path: string, title: string): Promise { + const body = await this.visibleRowText(path); + + expect(body, `"${title}" must be hidden in ${path} for this data scope`).not.toContain( + title.toLowerCase() + ); + } + + /** + * Create a person owned by whoever is signed in. + * + * Quick add is used rather than the full create form for one reason: `PersonController::store()` + * assigns `user_id` from the acting user only on the `quick_add` path. Submitting the standard + * form without picking a Sales Owner saves the person with a null owner, which makes it + * invisible to every non-global user — including its own creator — and would make these scope + * assertions fail for a reason that has nothing to do with `view_permission`. + */ + async createPerson(name: string, email: string, phone?: string): Promise { + const quickAdd = new QuickAddPage(this.page); + + await this.page.goto("admin/dashboard").catch(() => undefined); + + await quickAdd.createPerson({ + name, + email, + /** + * Email and phone are both unique across persons, so a shared literal would fail + * validation with "The value has already been taken." from the second person onward. + */ + contactNumber: phone ?? AclPage.uniquePhone(), + }); + } + + /** + * A phone number unique to this call, within the 10 digits the form accepts. + */ + static uniquePhone(): string { + return `9${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 900 + 100)}`; + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/AdminPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/AdminPage.ts new file mode 100644 index 0000000..aec7918 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/AdminPage.ts @@ -0,0 +1,47 @@ +import { Page, Locator, expect } from "@playwright/test"; +import CoreLocators from "../locator/CoreLocators"; + +export class AdminPage extends CoreLocators { + readonly page: Page; + readonly emailInput: Locator; + readonly passwordInput: Locator; + readonly signInButton: Locator; + readonly megaSearchInput: Locator; + readonly profileButton: Locator; + readonly signOutLink: Locator; + + constructor(page: Page) { + super(page) + this.page = page; + this.emailInput = page.getByPlaceholder("Email Address"); + this.passwordInput = page.getByPlaceholder("Password"); + this.signInButton = page.getByRole("button", { name: "Sign In" }); + this.megaSearchInput = page.getByPlaceholder("Mega Search").first(); + this.profileButton = page.getByRole("button", { name: "E" }); + this.signOutLink = page.getByRole("link", { name: "Sign Out" }); + } + + + + async adminLogin(email: string, password: string) { + await this.page.goto('admin/login'); + await this.emailInput.fill(email); + await this.passwordInput.fill(password); + await this.signInButton.click(); + } + + async verifyLogin() { + await expect(this.megaSearchInput).toBeVisible(); + } + + async adminLogout() { + await this.page.waitForTimeout(1000); + await this.profileButton.click(); + await this.signOutLink.click(); + + } + + async verifyLogout() { + await expect(this.passwordInput.first()).toBeVisible(); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/ConfigurationPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/ConfigurationPage.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/LeadPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/LeadPage.ts new file mode 100644 index 0000000..b599366 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/LeadPage.ts @@ -0,0 +1,168 @@ +import { Page, Locator, expect } from "@playwright/test"; +import CoreLocators from "../locator/CoreLocators"; +import { buildPersonData, PersonData } from "./PersonsPage"; +import { buildProductData, ProductData, ProductPage } from "./ProductPage"; +import { generateDescription, generateName } from "../utils/faker"; + + +export type LeadData = { + title: string; + description: string; + value: string; + expectedCloseDate: string; // ISO date string + person: PersonData; + organizationName: string; + product: ProductData; +}; + +const leadPersonData: PersonData = buildPersonData(); + +const leadProductData: ProductData = buildProductData(); + +export const leadData: LeadData = { + title: generateName(), + description: generateDescription(), + value: (Math.floor(Math.random() * 10000)).toString(), + expectedCloseDate: "2028-12-31", + /** + * The lead flow owns its person outright, so it never collides with the one the person or + * quote specs create. + */ + person: leadPersonData, + /** + * The lead flow owns its product too — the quote and product specs each create one of their own. + */ + product: leadProductData, + organizationName: leadPersonData.organizationName +}; + +export class LeadPage extends CoreLocators { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + async navigateToLeadList() { + await this.page.goto("admin/leads"); + } + async getLeadByTitle(title: string) { + return this.page.getByRole('link', { name: ` ${title}` }); + } + async createLead(leadData: LeadData) { + const productPage = new ProductPage(this.page); + await productPage.navigateToProductPage(); + await productPage.createProductLink.click() + await productPage.productForm(leadData.product); + + await this.navigateToLeadList(); + await this.createLeadButton.first().click();; + + await this.titleInput.fill(leadData.title); + await this.descriptionTextarea.fill(leadData.description); + await this.sourceDropdown.selectOption("1"); + await this.typeDropdown.selectOption("1"); + await this.userDropdown.selectOption("1"); + await this.leadValueInput.fill(leadData.value); + + await this.addPersonButton.click(); + await this.leadPersonSearchInput.fill(leadData.person.name); + await this.page.waitForTimeout(1000); + const personItem = (await this.selectListItmeByName(leadData.person.name)).first(); + await this.page.waitForTimeout(1000); + const isPersonAlreadyPresent = await personItem.isVisible(); + + if (isPersonAlreadyPresent) { + await personItem.click(); + console.log("person allready present"); + } + else { + + await this.addAsNewButton.click(); + await this.personEmailInput.fill(leadData.person.emails); + await this.personPhoneInput.fill(leadData.person.contactNumber); + await this.addOrganizationButton.click(); // this an issue add organiztion is not working if person allready present so that i have added this into else condition. + await this.searchInputField.fill(leadData.title); + await this.addAsNewButton.click(); + + } + // await this.leadProductAddMoreButton.click(); + await this.leadProductSelect.click(); + await this.leadProductSearchInput.fill(leadData.product.name); + await this.page.waitForTimeout(1000); + await this.page.locator(`//li[@class="cursor-pointer px-4 py-2 text-gray-800 transition-colors hover:bg-blue-100 dark:text-white dark:hover:bg-gray-900"]`).first().click(); + (await this.getElementByTypeAndName('button', "Save")).click(); + await this.searchInput.fill(leadData.title); + await this.page.keyboard.press('Enter'); + await expect(this.page.getByText(leadData.title).first()).toBeVisible(); + + + await expect(this.leadSuccessToast).toBeVisible(); + + } + async updateLead(leadData: LeadData) { + + + // Now update the lead with new data + + // Fill updated lead data + + // Use locators from LeadPage via page1 context to fill fields + await this.titleInput.fill(leadData.title); + await this.descriptionTextarea.fill(leadData.description); + await this.sourceDropdown.selectOption("1"); + await this.typeDropdown.selectOption("1"); + await this.userDropdown.selectOption("1"); + await this.leadValueInput.fill("1000"); + // await this.leadProductAddMoreButton.click(); + await this.leadProductSelect.click(); + await this.leadProductSearchInput.fill(leadData.product.name); + await this.page.waitForTimeout(1000); + await this.page.locator(`//li[@class="cursor-pointer px-4 py-2 text-gray-800 transition-colors hover:bg-blue-100 dark:text-white dark:hover:bg-gray-900"]`).first().click(); + (await this.getElementByTypeAndName('button', "Save")).click(); + await this.searchInput.fill(leadData.title); + await this.page.keyboard.press('Enter'); + await expect(((await this.getLeadByTitle(leadData.title)).first())).toBeVisible(); + + await expect(this.leadSuccessToast).toBeVisible(); + + } + + async deleteLead(leadData: LeadData) { + await this.listViewButton.click(); + await this.page.waitForTimeout(1000); + + await this.listViewLeadSearch.fill(leadData.title); + await this.page.keyboard.press('Enter'); + await this.page.waitForTimeout(1000); + + await this.deleteLeadButton.isVisible(); + await this.deleteLeadButton.click(); + await this.agreeButton.click(); + + await this.listViewLeadSearch.fill(leadData.title); + await this.page.keyboard.press('Enter'); + await expect(this.deleteLeadButton).not.toBeVisible(); + } + async searchLead(title: string) { + await this.navigateToLeadList(); + await this.searchInput.fill(title); + await this.page.keyboard.press('Enter'); + await this.page.waitForTimeout(1000); + + await (await this.getLeadByTitle(title)).first().click(); + await this.page.waitForLoadState('domcontentloaded'); + + const page1Promise = this.page.waitForEvent('popup'); + await this.editLeadButton.click(); + const page1 = await page1Promise; + const leadPage = new LeadPage(page1); + return leadPage; + } + async listView() { + await this.page.goto('admin/leads?view_type=table'); + await this.page.waitForLoadState('domcontentloaded'); + } + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/MailPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/MailPage.ts new file mode 100644 index 0000000..4762f43 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/MailPage.ts @@ -0,0 +1,139 @@ + +import { Page, Locator, FrameLocator, expect } from '@playwright/test'; + +import CoreLocators from '../locator/CoreLocators'; +import { generateDescription, generateEmail, generateName } from '../utils/faker'; + +export type MailData = { + replyTo: string; + subject: string; + body: string; +}; +export const mailData: MailData = { + replyTo: generateEmail(), + subject: generateName(), + body: generateDescription().slice(0, 50), +}; +export type emailType = 'bcc' | 'cc' | 'normal'|'both'; +export class MailPage extends CoreLocators { + readonly page: Page; + readonly composeEmailButton: Locator; + readonly replyToInput: Locator; + readonly subjectInput: Locator; + readonly sendButton: Locator; + readonly successMessage: Locator; + readonly editorFrame: FrameLocator; + readonly saveDraftButton: Locator; + readonly moveToTrashButton: Locator; + readonly ccMailInput: Locator; + readonly bccMailInput: Locator; + readonly ccButton: Locator; + readonly bccButton: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + this.composeEmailButton = page.getByRole('button', { name: 'Compose Mail' }); + this.replyToInput = page.getByLabel(/reply to/i).or(page.locator('input[name="temp-reply_to"]')); + this.subjectInput = page.getByLabel(/subject/i).or(page.locator('input[name="subject"]')); + this.sendButton = page.getByRole("button", { name: "Send" }); + this.successMessage = page.getByText("Email sent successfully."); + this.editorFrame = page.frameLocator("iframe.tox-edit-area__iframe"); + this.saveDraftButton = page.getByRole("button", { name: "Draft" }); + this.moveToTrashButton = page.getByRole('button', { name: 'Moved To Trash' }) + this.ccMailInput = page.locator('input[name="temp-cc"]'); + this.bccMailInput = page.locator('input[name="temp-bcc"]'); + this.ccButton = page.getByText('CC', { exact: true }); + this.bccButton = page.getByText('BCC', { exact: true }); + } + + async navigateToMailInboxPage() { + await this.page.goto("admin/mail/inbox"); + } + async navigateToMailDraftPage() { + await this.page.goto("admin/mail/draft"); + } + async navigateToMailTrashedPage() { + await this.page.goto("admin/mail/trash"); + } + async composeEmail(mailData: MailData, type: emailType) { + await this.composeEmailButton.click(); + await this.page.waitForTimeout(500); + + if (type === 'cc') { + await this.ccButton.click(); + await this.ccMailInput.waitFor({ state: 'visible', timeout: 5000 }); + await this.ccMailInput.fill("cc" + mailData.replyTo); + } else if (type === 'bcc') { + await this.bccButton.click(); + await this.bccMailInput.waitFor({ state: 'visible', timeout: 5000 }); + await this.bccMailInput.fill("bcc" + mailData.replyTo); + } else if (type === 'both') { + await this.ccButton.click(); + await this.bccButton.click(); + await this.bccMailInput.waitFor({ state: 'visible', timeout: 5000 }); + await this.ccMailInput.waitFor({ state: 'visible', timeout: 5000 }); + await this.ccMailInput.fill("cc" + mailData.replyTo); + await this.bccMailInput.fill("bcc" + mailData.replyTo); + } + + await this.replyToInput.fill(mailData.replyTo); + await this.subjectInput.fill(mailData.subject); + + await this.editorFrame.locator('body').fill(mailData.body); + await this.sendButton.click(); + } + + async saveDraftEmail(mailData: MailData, type: emailType) { + await this.composeEmailButton.click(); + await this.page.waitForTimeout(500); + + if (type === 'cc') { + await this.ccButton.click(); + await this.ccMailInput.waitFor({ state: 'visible', timeout: 5000 }); + await this.ccMailInput.fill("cc" + mailData.replyTo); + } else if (type === 'bcc') { + await this.bccButton.click(); + await this.bccMailInput.waitFor({ state: 'visible', timeout: 5000 }); + await this.bccMailInput.fill("bcc" + mailData.replyTo); + } else if (type === 'both') { + await this.ccButton.click(); + await this.bccButton.click(); + await this.ccMailInput.waitFor({ state: 'visible', timeout: 5000 }); + await this.ccMailInput.fill("cc" + mailData.replyTo); + + + await this.bccMailInput.waitFor({ state: 'visible', timeout: 5000 }); + await this.bccMailInput.fill("bcc" + mailData.replyTo); + } + + await this.replyToInput.fill(mailData.replyTo); + await this.subjectInput.fill(mailData.subject); + + await this.editorFrame.locator('body').fill(mailData.body); + await this.saveDraftButton.click(); + await this.navigateToMailDraftPage(); + await this.searchEmail(mailData.subject); + await expect(this.page.getByRole('paragraph').filter({ hasText: mailData.subject }).first()).toBeVisible(); + } + async movedEmailToTrash() { + await this.page.waitForTimeout(500); + await this.multiSelectCheckbox.click(); + await this.page.waitForTimeout(500); + await this.moveToTrashButton.click(); + await this.agreeButton.click(); + } + async searchEmail(subject: string) { + await this.searchInputExact.fill(subject); + await this.page.keyboard.press('Enter'); + } + async deleteEmailFromTrash(subject: string) { + await this.searchEmail(subject); + await this.page.waitForTimeout(500); + await this.multiSelectCheckbox.click(); + await this.page.waitForTimeout(500); + await this.deleteButton.click(); + await this.agreeButton.click(); + } + +} \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/OrganizationPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/OrganizationPage.ts new file mode 100644 index 0000000..1411438 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/OrganizationPage.ts @@ -0,0 +1,99 @@ +import { expect, Locator, Page } from "playwright/test"; +import CoreLocators from "../locator/CoreLocators"; +import { generateFirstName, generateLocation, generateName } from "../utils/faker"; +export type OrganizationData = { + name: string; + address: string; + country: string; // country code, e.g. 'IN' + state: string; // state code, e.g. 'DL' + city: string; + postcode: string; +}; + export const organizationData: OrganizationData = { + name: generateName(), + address: generateLocation() , + country: "IN", // India country code + state: "DL", // Delhi state code + city: "New Delhi", + postcode: "110015", +}; +export default class OrganizationPage extends CoreLocators{ + + + readonly page:Page + readonly firstOrganization:Locator; + constructor(page:Page) + { + super(page) + this.page=page + this.firstOrganization= page.locator(`(//p[@class="break-words"])[1]`); + + } + async navigateToOrganization() + { + await this.page.goto("admin/contacts/organizations"); + } + + async createOrganization(orgData:OrganizationData) { + await this.createOrgLink.click(); + + // Fill organization details + await this.orgNameTextbox.fill(orgData.name); + await this.orgAddressTextarea.fill(orgData.address); + + await this.orgCountryCombobox.selectOption(orgData.country); + await this.orgStateSelect.selectOption(orgData.state); + + await this.orgCityTextbox.fill(orgData.city); + await this.orgPostcodeTextbox.fill(orgData.postcode); + + await this.orgExtraDetailsDiv.nth(2).click(); + + await this.searchInputField.fill('exampl'); + await this.orgExampleListItem('Example').click(); + + + + // Save organization + await this.saveOrganizationButton.click(); + await expect(this.organizationCreateSuccessMessage.first()).toBeVisible(); + // await this.searchInputExact.fill(orgData.name); + // await this.page.keyboard.press('Enter'); + console.log("There’s an issue: the organization search isn’t working and needs to be fixed."); + + + await expect(this.page.getByText(orgData.name).first()).toBeVisible(); + + + } + + + + async updateOrganization(orgData:OrganizationData) + { + + + await this.firstEditIcon.click(); + // Fill organization details + await this.orgNameTextbox.fill(orgData.name); + await this.orgAddressTextarea.fill(orgData.address); + + await this.orgCountryCombobox.first().selectOption(orgData.country); + await this.orgStateSelect.selectOption(orgData.state); + + await this.orgCityTextbox.fill(orgData.city); + await this.orgPostcodeTextbox.fill(orgData.postcode); + + // Save organization + await this.saveOrganizationButton.click(); + return orgData; + + } + + async deleteOrganization(){ + + await this.firstDeleteIcon.click(); + await this.agreeButton.click(); + } + +} \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/PersonsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/PersonsPage.ts new file mode 100644 index 0000000..1653b7c --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/PersonsPage.ts @@ -0,0 +1,121 @@ +import { expect, Locator, Page } from "playwright/test"; +import CoreLocators from "../locator/CoreLocators"; +import { generateEmail, generateFullName, generateName, generatePhoneNumber } from "../utils/faker"; +import { organizationData } from "../pages/OrganizationPage"; +export type PersonData = { + name: string; + emails: string; // array to hold multiple emails + contactNumber: string; // array to hold multiple contact numbers + jobTitle: string; + salesOwnerId: string; // id or value representing Sales Owner + organizationName: string; +}; + +/** + * Build a person with an email and contact number unique to this call. + * + * Both fields are unique in Krayin, so two persons built from one shared object cannot both be + * saved — the second submit fails with "The value has already been taken." Each spec that creates + * a person therefore needs its own instance, while keeping that instance stable *within* the spec + * so the create / edit / delete cases all address the same record. + */ +export function buildPersonData(overrides: Partial = {}): PersonData { + return { + name: generateName(), + emails: generateEmail(), + contactNumber: generatePhoneNumber(), + jobTitle: generateName(), + salesOwnerId: "1", // Example sales owner id + organizationName: organizationData.name, + ...overrides, + }; +} + +/** + * Default instance kept for existing importers. Do not create a person from this in more than one + * spec — build a fresh instance instead. + */ +export const personData: PersonData = buildPersonData(); + +export default class PersonsPage extends CoreLocators { + readonly page: Page; + readonly firstPerson:Locator; + + constructor(page: Page) { + super(page), + this.page = page + this.firstPerson= page.locator(`(//div[@class="flex flex-col gap-1.5 dark:text-gray-300"])[1]`); + } + + async navigageToPersonsPage() { + await this.page.goto("admin/contacts/persons"); + } + async createPerson(personData: PersonData) { + + await this.createPersonLink.click(); + + // Fill person details (use first email/phone if multiple provided) + await this.personNameTextbox.fill(personData.name); + if (personData.emails && personData.emails.length > 0) { + await this.personEmailTextbox.fill(personData.emails); + } + if (personData.contactNumber && personData.contactNumber.length > 0) { + await this.personPhoneTextbox.fill(personData.contactNumber); + } + await this.personJobTitleTextbox.fill(personData.jobTitle || ""); + + // Assign organization (if provided) + if (personData.organizationName) { + await this.personOrgSelectDiv.click(); + await this.searchInputField.fill("Examp"); + await this.personOrgListItem("Examp").click(); + } + // Save person + await this.savePersonButton.click(); + await this.searchInputExact.fill(personData.name); + await this.page.keyboard.press('Enter'); + await expect(this.page.getByText(personData.name).first()).toBeVisible(); + + } + async updatePerson(personData: PersonData) { + await this.searchInputExact.fill(personData.name); + await this.page.keyboard.press('Enter'); + await this.firstEditIcon.click(); + // Fill person details (use first email/phone if multiple provided) + await this.personNameTextbox.fill(personData.name); + if (personData.emails && personData.emails.length > 0) { + await this.personEmailTextbox.fill(personData.emails); + } + if (personData.contactNumber && personData.contactNumber.length > 0) { + await this.personPhoneTextbox.fill(personData.contactNumber); + } + await this.personJobTitleTextbox.fill(personData.jobTitle || ""); + + // Assign organization (if provided) + if (personData.organizationName) { + await this.personOrgSelectDiv.click(); + await this.searchInputField.fill("Examp"); + await this.personOrgListItem("Examp").click(); + } + // Save person + await this.savePersonButton.click(); + await this.searchInputExact.fill(personData.name); + await expect(this.page.getByText(personData.name).first()).toBeVisible(); + + } + async personDelete() { + + await this.firstDeleteIcon.click(); + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } + async personMassDelete() { + await this.multiSelectCheckbox.click(); + await this.deleteButton.click(); + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } + + + +} \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/ProductPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/ProductPage.ts new file mode 100644 index 0000000..965effc --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/ProductPage.ts @@ -0,0 +1,86 @@ +import { expect, Page } from "playwright/test"; +import CoreLocators from "../locator/CoreLocators"; +import { generateDescription, generateName, generateSKU } from "../utils/faker"; +import { saveData } from "../utils/savedata"; +export type ProductData = { + name: string, + description: string, + sku: string, + price: string, + priceInDollars?: string, // Optional if price is stored in a different currency + quantity: string, +} +/** + * Build a product with a SKU unique to this call. + * + * SKU is unique in Krayin, so two products built from one shared object cannot both be saved. The + * product, lead and quote specs each create a product, so each needs its own instance — sharing one + * makes whichever spec runs later fail on the product step. + */ +export function buildProductData(overrides: Partial = {}): ProductData { + return { + name: generateName(), + description: generateDescription(), + sku: generateSKU(), + price: Math.floor(Math.random() * 1000).toString(), + quantity: Math.floor(Math.random() * 100).toString(), + ...overrides, + }; +} + +/** + * Default instance kept for existing importers. Do not create a product from this in more than one + * spec — build a fresh instance instead. + */ +export const productData: ProductData = buildProductData(); + + +export class ProductPage extends CoreLocators { + + + page: Page; + + constructor(page: Page) { + super(page); + this.page = page + } + + async navigateToProductPage() { + + await this.page.goto("admin/products"); + + } + async productForm(productdata: ProductData) { + + // Fill product form using this + await this.productNameTextbox.waitFor({ state: "visible" }); + await this.productNameTextbox.click(); + await this.productNameTextbox.fill(productdata.name); + + await this.productDescriptionTextarea.fill(""); + await this.productDescriptionTextarea.type(productdata.description); + + await this.productSkuInput.fill(productdata.sku); + + await this.productPriceInput.fill(productdata.price); + await this.productQuantityInput.fill(productdata.quantity); + + // Save product + await this.saveProductsButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + saveData("productData", productdata); + // await this.searchProduct(productdata.name); /* searching is not working now once the issue is fixed uncomment this line */ + // await expect(this.page.getByText(productdata.name).first()).toBeVisible(); + + } + async searchProduct(productName: string) { + await this.searchInputExact.fill(productName); + await this.page.keyboard.press('Enter'); + } + async deleteProduct() + { + await this.firstDeleteIcon.click(); + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } +} \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/QuickAddPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/QuickAddPage.ts new file mode 100644 index 0000000..1e2069d --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/QuickAddPage.ts @@ -0,0 +1,199 @@ +import { Page, expect } from "@playwright/test"; +import CoreLocators from "../locator/CoreLocators"; +import { + generateDescription, + generateEmail, + generateFullName, + generateName, + generatePhoneNumber, + generateRandomNumericString, + generateSKU, +} from "../utils/faker"; + +export type QuickAddTab = "Lead" | "Person" | "Organization" | "Product" | "Email"; + +export type QuickAddLeadData = { + title: string; + description: string; +}; + +export type QuickAddPersonData = { + name: string; + email: string; + contactNumber: string; +}; + +export type QuickAddOrganizationData = { + name: string; +}; + +export type QuickAddProductData = { + name: string; + description: string; + sku: string; + quantity: string; + price: string; +}; + +export type QuickAddMailData = { + replyTo: string; + subject: string; + body: string; +}; + +export const quickAddLeadData: QuickAddLeadData = { + title: generateName(), + description: generateDescription(), +}; + +export const quickAddPersonData: QuickAddPersonData = { + name: generateFullName(), + email: generateEmail(), + contactNumber: generatePhoneNumber(), +}; + +export const quickAddOrganizationData: QuickAddOrganizationData = { + name: generateName() + " Inc", +}; + +export const quickAddProductData: QuickAddProductData = { + name: "Product " + generateName(), + description: generateDescription(), + sku: generateSKU(), + quantity: generateRandomNumericString(2, 10, 50), + price: generateRandomNumericString(3, 100, 500), +}; + +export const quickAddMailData: QuickAddMailData = { + replyTo: generateEmail(), + subject: generateName(), + body: generateDescription().slice(0, 50), +}; + +export class QuickAddPage extends CoreLocators { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + + async openQuickAddModal() { + await this.page.goto("admin/dashboard"); + await this.quickAddTrigger.waitFor({ state: "visible" }); + await this.quickAddTrigger.click(); + await expect(this.quickAddModalTitle).toBeVisible(); + } + + async selectQuickAddTab(tabLabel: QuickAddTab) { + await this.quickAddTab(tabLabel).click(); + } + + async submitQuickAdd() { + await this.quickAddSaveButton.click(); + } + + /** + * Every quick add tab exposed by the seeded admin role must be reachable. + */ + async verifyQuickAddTabs(tabs: QuickAddTab[]) { + await this.openQuickAddModal(); + + for (const tab of tabs) { + await expect(this.quickAddTab(tab)).toBeVisible(); + } + } + + async createLead(leadData: QuickAddLeadData) { + await this.openQuickAddModal(); + await this.selectQuickAddTab("Lead"); + + await this.quickAddLeadTitleInput.fill(leadData.title); + await this.quickAddLeadDescriptionTextarea.fill(leadData.description); + + await this.submitQuickAdd(); + + await expect(this.quickAddLeadSuccessMsg).toBeVisible(); + + await this.page.goto("admin/leads"); + await this.searchInput.fill(leadData.title); + await this.page.keyboard.press("Enter"); + await expect(this.page.getByText(leadData.title).first()).toBeVisible(); + } + + async createPerson(personData: QuickAddPersonData) { + await this.openQuickAddModal(); + await this.selectQuickAddTab("Person"); + + await this.quickAddPersonNameInput.fill(personData.name); + await this.quickAddPersonEmailInput.fill(personData.email); + await this.quickAddPersonContactInput.fill(personData.contactNumber); + + await this.submitQuickAdd(); + + await expect(this.quickAddPersonSuccessMsg).toBeVisible(); + + await this.page.goto("admin/contacts/persons"); + await this.searchByName(personData.name); + await expect(this.page.getByText(personData.name).first()).toBeVisible(); + } + + async createOrganization(organizationData: QuickAddOrganizationData) { + await this.openQuickAddModal(); + await this.selectQuickAddTab("Organization"); + + await this.quickAddOrgNameInput.fill(organizationData.name); + + await this.submitQuickAdd(); + + await expect(this.quickAddOrgSuccessMsg).toBeVisible(); + + await this.page.goto("admin/contacts/organizations"); + await this.searchByName(organizationData.name); + await expect(this.page.getByText(organizationData.name).first()).toBeVisible(); + } + + async createProduct(productData: QuickAddProductData) { + await this.openQuickAddModal(); + await this.selectQuickAddTab("Product"); + + await this.quickAddProductNameInput.fill(productData.name); + await this.quickAddProductDescriptionTextarea.fill(productData.description); + await this.quickAddProductSkuInput.fill(productData.sku); + await this.quickAddProductQuantityInput.fill(productData.quantity); + await this.quickAddProductPriceInput.fill(productData.price); + + await this.submitQuickAdd(); + + await expect(this.quickAddProductSuccessMsg).toBeVisible(); + + await this.page.goto("admin/products"); + await this.searchByName(productData.name); + await expect(this.page.getByText(productData.name).first()).toBeVisible(); + } + + async createMail(mailData: QuickAddMailData) { + await this.openQuickAddModal(); + await this.selectQuickAddTab("Email"); + + await this.quickAddMailReplyToInput.fill(mailData.replyTo); + await this.quickAddMailSubjectInput.fill(mailData.subject); + await this.quickAddMailEditorFrame.locator("body").fill(mailData.body); + + await this.submitQuickAdd(); + + await expect(this.quickAddMailSuccessMsg).toBeVisible(); + } + + /** + * Validation errors must keep the modal mounted instead of closing it. + */ + async submitEmptyForm(tabLabel: QuickAddTab) { + await this.openQuickAddModal(); + await this.selectQuickAddTab(tabLabel); + + await this.submitQuickAdd(); + + await expect(this.quickAddModalTitle).toBeVisible(); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/QuotesPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/QuotesPage.ts new file mode 100644 index 0000000..7ecc3bf --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/QuotesPage.ts @@ -0,0 +1,204 @@ +import { expect, Locator, Page } from "@playwright/test"; +import CoreLocators from "../locator/CoreLocators"; +import { buildProductData, ProductData } from "./ProductPage"; +import { generateDescription, generateFullName } from "../utils/faker"; +import PersonsPage, { buildPersonData, PersonData } from "./PersonsPage"; +export type QuoteData = { + subject: string; + description: string; + salesOwnerId: string; + expiredAt: string; // expiration date string + person: PersonData; + leadName: string; + address: string; + countryCode: string; // e.g. 'IN' + stateCode: string; // e.g. 'DL' + city: string; + postcode: string; + product: ProductData; + // Add quote items type here if needed +}; + +const quotePersonData: PersonData = buildPersonData(); + +const quoteProductData: ProductData = buildProductData(); + +export const quoteData: QuoteData = { + subject: generateFullName(), + description: generateDescription(), + salesOwnerId: "1", + expiredAt: "2026-02-28", + /** + * A person of its own, for the same reason the lead flow has one. + */ + person: quotePersonData, + leadName: "Corporate Website Lead", + address: "Plot 45, Industrial Area", + countryCode: "IN", + stateCode: "DL", + city: "New Delhi", + postcode: "110020", + product: quoteProductData, + // Optionally add quote items array if needed +}; + + + + +export class QuotesPage extends CoreLocators { + readonly page: Page; + + // Navigation links/buttons + readonly createQuoteLink: Locator; + readonly saveQuoteButton: Locator; + + // Form fields + readonly subjectTextbox: Locator; + readonly descriptionTextbox: Locator; + readonly salesOwnerSelect: Locator; + readonly expiredAtTextbox: Locator; + + // Person search and selection + readonly searchTextbox: Locator; + + // Billing address fields + readonly billingAddressTextarea: Locator; + readonly billingCountrySelect: Locator; + readonly billingStateSelect: Locator; + readonly billingCityInput: Locator; + readonly billingPostcodeInput: Locator; + + // Shipping address fields + readonly shippingAddressTextarea: Locator; + readonly shippingCountrySelect: Locator; + readonly shippingStateSelect: Locator; + readonly shippingCityInput: Locator; + readonly shippingPostcodeInput: Locator; + + // Confirmation/Sucess notification locator + + constructor(page: Page) { + super(page); + this.page = page; + this.subjectTextbox = page.getByRole("textbox", { name: "Subject *" }); + this.descriptionTextbox = page.getByRole("textbox", { name: "Description" }); + this.searchTextbox = page.getByRole("textbox", { name: "Search..." }); + + this.expiredAtTextbox = page.getByRole("textbox", { name: "Expired At *" }); + + this.billingAddressTextarea = page.locator('textarea[name="billing_address\\[address\\]"]'); + this.shippingAddressTextarea = page.locator('textarea[name="shipping_address\\[address\\]"]'); + + this.billingCityInput = page.locator('input[name="billing_address\\[city\\]"]'); + this.billingPostcodeInput = page.locator('input[name="billing_address\\[postcode\\]"]'); + this.shippingCityInput = page.locator('input[name="shipping_address\\[city\\]"]'); + this.shippingPostcodeInput = page.locator('input[name="shipping_address\\[postcode\\]"]'); + + this.salesOwnerSelect = page.getByLabel("Sales Owner"); + this.billingCountrySelect = page.locator('select[name="billing_address\\[country\\]"]'); + this.billingStateSelect = page.locator('select[name="billing_address\\[state\\]"]'); + this.shippingCountrySelect = page.locator('select[name="shipping_address\\[country\\]"]'); + this.shippingStateSelect = page.locator('select[name="shipping_address\\[state\\]"]'); + + this.saveQuoteButton = page.getByRole("button", { name: "Save Quote" }); + + this.createQuoteLink = page.getByRole("link", { name: "Create Quote" }); + + + this.createQuoteLink = page.getByRole("link", { name: "Create Quote" }); + + + } + async navigateToQuotesPage() { + await this.page.goto("admin/quotes"); + } + + async createQuote(quoteData: QuoteData) { + + + await this.navigateToQuotesPage(); + // Fill Quote Basics + await this.createQuoteLink.click(); + await this.subjectTextbox.fill(quoteData.subject); + await this.descriptionTextbox.fill(quoteData.description); + await this.salesOwnerSelect.selectOption(quoteData.salesOwnerId); + await this.expiredAtTextbox.fill(quoteData.expiredAt); + + // Link to Person - Click to add and select person by name + await this.quoteAddPersonButton.click(); + await this.searchTextbox.fill(quoteData.person.name); + await this.page.waitForTimeout(1000); + await this.quoteSelectListPerson.click(); // or select existing if applicable + + // // Link to Lead - Click to add and select lead by name + // await this.quoteLinkToLeadButton.click(); + // await this.searchTextbox.fill(quoteData.leadName); + // await this.addAsNewButton.click(); // or select existing + + // Fill Billing Address + await this.billingAddressTextarea.fill(quoteData.address); + await this.billingCountrySelect.selectOption(quoteData.countryCode); + await this.billingStateSelect.selectOption(quoteData.stateCode); + await this.billingCityInput.fill(quoteData.city); + await this.billingPostcodeInput.fill(quoteData.postcode); + + // Fill Shipping Address + await this.shippingAddressTextarea.fill(quoteData.address); + await this.shippingCountrySelect.selectOption(quoteData.countryCode); + await this.shippingStateSelect.selectOption(quoteData.stateCode); + await this.shippingCityInput.fill(quoteData.city); + await this.shippingPostcodeInput.fill(quoteData.postcode); + + // Quote item filling + await this.page.getByText('Click to Add', { exact: true }).first().click(); + await this.page.getByPlaceholder("Search Products").fill(quoteData.product.name); + await this.page.waitForTimeout(1000); + await this.page.getByText(quoteData.product.name, { exact: true }).first().click(); + + // Save the quote + await this.saveQuoteButton.click(); + + } + async updateQuote(quoteData:QuoteData) + { + await this.firstEditIcon.click(); + await this.subjectTextbox.fill(quoteData.subject); + await this.descriptionTextbox.fill(quoteData.description); + await this.salesOwnerSelect.selectOption(quoteData.salesOwnerId); + await this.expiredAtTextbox.fill(quoteData.expiredAt); + + // Link to Person - Click to add and select person by name + await this.quoteAddPersonButton.click(); + await this.searchTextbox.fill(quoteData.person.name); + await this.page.waitForTimeout(1000); + await this.quoteSelectListPerson.click(); // or select existing if applicable + + // Link to Lead - Click to add and select lead by name + // await this.quoteLinkToLeadButton.click(); + // await this.searchTextbox.fill(quoteData.leadName); + // await this.addAsNewButton.click(); // or select existing + + // Fill Billing Address + await this.billingAddressTextarea.fill(quoteData.address); + await this.billingCountrySelect.selectOption(quoteData.countryCode); + await this.billingStateSelect.selectOption(quoteData.stateCode); + await this.billingCityInput.fill(quoteData.city); + await this.billingPostcodeInput.fill(quoteData.postcode); + + + + + + + // Save the quote + await this.saveQuoteButton.click(); + + + } + async deleteQuote() + { + await this.firstDeleteIcon.click(); + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/SettingsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/SettingsPage.ts new file mode 100644 index 0000000..0c7a29f --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/SettingsPage.ts @@ -0,0 +1,96 @@ +import { Page } from "playwright/test"; +import CoreLocators from "../locator/CoreLocators"; + +export class SettingsPage extends CoreLocators { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + + // Warehouses + async navigateToWarehouses() { + await this.page.goto("admin/settings/warehouses"); + } + + // Automation + async navigateToAutomation() { + await this.page.goto("admin/settings/automation"); + } + + // Attributes + async navigateToAttributes() { + await this.page.goto("admin/settings/attributes"); + } + + // Email Templates + async navigateToEmailTemplates() { + await this.page.goto("admin/settings/email-templates"); + } + + // Events + async navigateToEvents() { + await this.page.goto("admin/settings/marketing/events"); + } + + // Campaigns + async navigateToCampaigns() { + await this.page.goto("admin/settings/marketing/campaigns"); + } + + // Webhooks + async navigateToWebhooks() { + await this.page.goto("admin/settings/webhooks"); + } + + // Workflows + async navigateToWorkflows() { + await this.page.goto("admin/settings/workflows"); + } + + // Data Transfer + async navigateToDataTransfer() { + await this.page.goto("admin/settings/data-transfer/imports"); + } + + // Other Settings + async navigateToOtherSettings() { + await this.page.goto("admin/settings/other-settings"); + } + + // Web Forms + async navigateToWebForms() { + await this.page.goto("admin/settings/web-forms"); + } + + // Tags + async navigateToTags() { + await this.page.goto("admin/settings/tags"); + } + + // Groups + async navigateToGroups() { + await this.page.goto("admin/settings/groups"); + } + + // Roles + async navigateToRoles() { + await this.page.goto("admin/settings/roles"); + } + + // Users / Agents + async navigateToUsers() { + await this.page.goto("admin/settings/users"); + } + + // Pipelines + async navigateToPipelines() { + await this.page.goto("admin/settings/pipelines"); + } + + // Sources + async navigateToSources() { + await this.page.goto("admin/settings/sources"); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/AttributesPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/AttributesPage.ts new file mode 100644 index 0000000..a3b2108 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/AttributesPage.ts @@ -0,0 +1,78 @@ +import { expect, Locator, Page } from "playwright/test"; +import { SettingsPage } from "../SettingsPage"; +import { generateFirstName, generateRandomNumericString } from "../../utils/faker"; + +export type AttributeData = { + name: string, + code: string, +} +export const attributeData: AttributeData = { + name: generateFirstName(), + code: generateRandomNumericString(), + +} +export class AttributesPage extends SettingsPage { + + + readonly page: Page; + readonly createAttributeLink: Locator; + readonly nameTextbox: Locator; + readonly codeTextbox: Locator; + readonly typeDropdown: Locator; + readonly entityTypeDropdown: Locator; + readonly validationDropdown: Locator; + readonly saveAttributeButton: Locator; + readonly successMessage: Locator; + readonly firstEditButton: Locator; + readonly firstDeleteButton: Locator; + readonly deleteConfirmButton: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + this.createAttributeLink = page.getByRole('link', { name: 'Create Attribute' }); + this.nameTextbox = page.getByRole('textbox', { name: 'Name' }); + this.codeTextbox = page.getByRole('textbox', { name: 'Code' }); + this.typeDropdown = page.locator('#type'); + this.entityTypeDropdown = page.locator('#entity_type'); + this.validationDropdown = page.locator('#validation'); + this.saveAttributeButton = page.getByRole('button', { name: 'Save Attribute' }); + this.successMessage = page.getByText('Success', { exact: true }); + this.firstEditButton = page.locator('//span[contains(@class,"icon-edit")]').first(); + this.firstDeleteButton = page.locator('//span[contains(@class,"icon-delete")]').first(); + this.deleteConfirmButton = page.getByRole('button', { name: 'Agree', exact: true }); + } + + async createAttribute(attributeData: AttributeData) { + await this.createAttributeLink.click(); + await this.nameTextbox.fill(attributeData.name); + await this.codeTextbox.fill(attributeData.name.toLowerCase()); + await this.typeDropdown.selectOption({ value: 'textarea' }); + await this.entityTypeDropdown.selectOption({ value: 'leads' }); + await this.saveAttributeButton.click(); + await this.searchAttribute(attributeData.name); + await expect(this.page.getByText(attributeData.name).first()).toBeVisible(); + + } + async updateAttribute(attributeData: AttributeData) { + await this.firstEditButton.click(); + await this.nameTextbox.fill(attributeData.name); + + await this.saveAttributeButton.click(); + await this.searchAttribute(attributeData.name); + await expect(this.page.getByText(attributeData.name).first()).toBeVisible(); + } + async searchAttribute(name: string) { + await this.searchInputExact.fill(name); + await this.page.keyboard.press('Enter'); + + } + async deleteAttribute() + { + await this.firstDeleteButton.click(); + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/AutomationPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/AutomationPage.ts new file mode 100644 index 0000000..b2a33b6 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/AutomationPage.ts @@ -0,0 +1,13 @@ +import { Page } from "playwright/test"; +import { SettingsPage } from "../SettingsPage"; + +export class AutomationPage extends SettingsPage { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/CampaignsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/CampaignsPage.ts new file mode 100644 index 0000000..2173dd4 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/CampaignsPage.ts @@ -0,0 +1,114 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateFullName, generateLastName, generateRandomNumericString } from '../../utils/faker'; +import { SettingsPage } from '../SettingsPage'; +import { eventdata, EventData } from './EventsPage'; + +export type CampaignData = { + name: string; + subject: string; + emailTemplateId: string; + event: EventData; + active: number; +} +export const campaignData: CampaignData = { + name: generateFullName(), + subject: generateLastName(), + emailTemplateId: generateRandomNumericString(), + event: eventdata, + active: 1, +} + +export class CampaignsPage extends SettingsPage { + readonly page: Page; + + // Campaign locators + readonly createCampaignButton: Locator; + readonly campaignNameInput: Locator; + readonly campaignSubjectInput: Locator; + readonly emailTemplateDropdown: Locator; + readonly eventDropdown: Locator; + readonly activeToggle: Locator; + readonly saveCampaignButton: Locator; + readonly firstCampaignEditButton: Locator; + readonly firstCampaignDeleteButton: Locator; + readonly confirmDeleteButton: Locator; + readonly successCreatedMessage: Locator; + readonly successUpdatedMessage: Locator; + readonly successDeletedMessage: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + + this.createCampaignButton = page.getByRole('button', { name: 'Create Campaigns' }) + this.campaignNameInput = page.locator('input[name="name"]'); + this.campaignSubjectInput = page.getByRole('textbox', { name: 'Subject *' }); + this.emailTemplateDropdown = page.locator('#marketing_template_id'); + this.eventDropdown = page.getByLabel('Event'); + this.activeToggle = page.locator('.peer.h-5'); + this.saveCampaignButton = page.getByRole('button', { name: 'Save Activity' }); + + this.firstCampaignEditButton = page.locator('.row > div:nth-child(6) > a').first(); + this.firstCampaignDeleteButton = page.locator('xpath=(//span[contains(@class, "icon-delete")])[1]'); + this.confirmDeleteButton = page.getByRole('button', { name: 'Agree', exact: true }); + + this.successCreatedMessage = page.getByText(/(created) successfully\./).first(); + this.successUpdatedMessage = page.getByText(/(updated) successfully\./).first(); + this.successDeletedMessage = page.getByText(/(deleted) successfully\./).first(); + } + + async gotoCampaigns() { + await this.page.goto("admin/settings/marketing/campaigns"); + } + + async createCampaign(data: CampaignData) { + await this.createCampaignButton.click(); + await this.campaignNameInput.fill(data.name); + await this.campaignSubjectInput.fill(data.subject); + await this.campaignSubjectInput.click(); // For validation if needed + await this.campaignSubjectInput.fill(data.subject); + + await this.eventDropdown.selectOption({ label: data.event.name }); + await this.emailTemplateDropdown.selectOption({ value: "1" }); + + + if (data.active) { + // Only toggle if not already active, this might require check logic per app specifics + await this.activeToggle.click(); + } + + await this.saveCampaignButton.click(); + await expect(this.successCreatedMessage).toBeVisible(); + await this.searchByName(data.name); + await expect(this.page.getByText(data.name).first()).toBeVisible(); + } + + async editCampaign(data: CampaignData) { + await this.firstCampaignEditButton.click(); + await this.campaignNameInput.fill(data.name); + await this.campaignSubjectInput.fill(data.subject); + await this.campaignSubjectInput.click(); // For validation if needed + await this.campaignSubjectInput.fill(data.subject); + + await this.eventDropdown.selectOption({ label: data.event.name }); + await this.emailTemplateDropdown.selectOption({ value: "1" }); + + + if (data.active) { + // Only toggle if not already active, this might require check logic per app specifics + await this.activeToggle.click(); + } + + await this.saveCampaignButton.click(); + await expect(this.successUpdatedMessage).toBeVisible(); + await this.searchInputExact.fill(data.name); + await this.page.keyboard.press('Enter'); + await expect(this.page.getByText(data.name).first()).toBeVisible(); + } + + async deleteCampaign() { + await this.firstCampaignDeleteButton.click(); + await this.confirmDeleteButton.click(); + await expect(this.successDeletedMessage).toBeVisible(); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/DataTransferPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/DataTransferPage.ts new file mode 100644 index 0000000..254c723 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/DataTransferPage.ts @@ -0,0 +1,134 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { SettingsPage } from '../SettingsPage'; +import path from 'path'; +import fs from 'fs'; +import { parse } from 'fast-csv'; +import { format } from '@fast-csv/format'; +export type EntityType = 'leads' | 'products' | 'persons'; +export class DataTransferPage extends SettingsPage { + readonly page: Page; + + // Locators + readonly importPageUrl = 'admin/settings/data-transfer/imports'; + readonly createImportLink: Locator; + readonly fileInput: Locator; + readonly processQueueCheckbox: Locator; + readonly processQueueInput: Locator; + readonly entityTypeSelect: Locator; + readonly saveImportButton: Locator; + readonly validateButton: Locator; + readonly runImportButton: Locator; + readonly importRecordsText: Locator; + readonly editIcon: Locator; + readonly deleteIcon: Locator; + readonly confirmDeleteButton: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + this.createImportLink = page.locator('a.primary-button'); + this.fileInput = page.locator('input[name="file"]'); + this.processQueueCheckbox = page.locator('label[for="process_in_queue"]'); + this.processQueueInput = page.locator('input[name="process_in_queue"]'); + this.entityTypeSelect = page.locator('#import-type'); + this.saveImportButton = page.getByRole('button', { name: 'Save Import' }); + this.validateButton = page.locator('//button[contains(.,"Validate")]'); + this.runImportButton = page.getByRole('button', { name: 'Import' }); + this.importRecordsText = page.getByRole('paragraph').filter({ hasText: /^leads$/ }); + this.editIcon = page.locator('.icon-edit').first(); + this.deleteIcon = page.locator('.icon-delete').first(); + this.confirmDeleteButton = page.getByRole('button', { name: 'Agree', exact: true }); + + } + + + async createImport(csvFilePath: string, entityType: EntityType) { + + await this.createImportLink.click(); + await this.setInputFiles('input[name="file"]', csvFilePath); + await this.uncheckProcessQueue(); + + await this.entityTypeSelect.selectOption(entityType); + await this.saveImportButton.click(); + + await this.page.locator('//button[contains(.,"Validate")]').click(); + await this.runImportButton.click(); + + // Wait for success record appearance + await expect(this.successMessage.first()).toBeVisible(); + } + async updateImport(csvFilePath: string) { + await this.editIcon.click(); + await this.setInputFiles('input[name="file"]', csvFilePath); + await this.uncheckProcessQueue(); + await this.saveImportButton.click(); + + // Validation and import + await this.page.locator('//button[@class="primary-button place-self-start"]').click(); + await expect(this.page.getByText('Your import is valid. Click')).toBeVisible(); + + await this.runImportButton.click(); + + // Wait for success message + await expect(this.page.getByText('Congratulations! Your import')).toBeVisible(); + } + + async deleteImport() { + + await this.deleteIcon.click(); + await this.confirmDeleteButton.click(); + // Confirm deletion and presence of 'No Records Available.' + await expect(this.successMessage.first()).toBeVisible(); + } + + /** + * The import must run synchronously in every test, otherwise the records are + * only queued and the assertions race the queue worker. The switch is toggled + * through its label because the real checkbox is visually hidden, and it is + * clicked only when checked so the state stays deterministic on both the + * create form (off by default) and the edit form (reflects the saved import). + */ + async uncheckProcessQueue() { + await this.processQueueInput.waitFor({ state: 'attached' }); + + if (await this.processQueueInput.isChecked()) { + await this.processQueueCheckbox.click(); + } + + await expect(this.processQueueInput).not.toBeChecked(); + } + + // Utility method for setting files, with fallback to Playwright's method + async setInputFiles(selector: string, filePath: string) { + await this.page.setInputFiles(selector, filePath); + } + +async updateCsv(filePath: string,column:string, newOrgId: string): Promise { + return new Promise((resolve, reject) => { + const rows: any[] = []; + + fs.createReadStream(filePath) + .pipe(parse({ headers: true })) + .on('error', reject) + .on('data', (row) => { + row[column] = newOrgId; // only update this field + rows.push(row); + }) + .on('end', () => { + const writeStream = fs.createWriteStream(filePath); + const csvStream = format({ headers: true }); + + csvStream + .pipe(writeStream) + .on('finish', resolve) + .on('error', reject); + + rows.forEach(row => csvStream.write(row)); + csvStream.end(); + }); + }); +} + + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/EmailTemplatesPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/EmailTemplatesPage.ts new file mode 100644 index 0000000..a5a8a05 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/EmailTemplatesPage.ts @@ -0,0 +1,69 @@ +import { Page, Locator, FrameLocator, expect } from '@playwright/test'; +import { SettingsPage } from '../SettingsPage'; +import { generateDescription, generateName } from '../../utils/faker'; + +export type EmailTemplateData = { + subject: string; + content: string; + name: string; +}; + +export const emailTemplateData: EmailTemplateData = { + subject: generateName(), + content: generateDescription(), + name: generateName(), +}; + export class EmailTemplatePage extends SettingsPage { + readonly page: Page; + readonly createButton: Locator; + readonly editButton: Locator; + readonly deleteButton: Locator; + readonly subjectInput: Locator; + readonly contentInput: FrameLocator; + readonly nameInput: Locator; + readonly placeholderSelect: Locator; + readonly saveButton: Locator; + readonly deleteConfirmButton: Locator; + readonly listingUrl: string; + + constructor(page: Page) { + super(page); + this.page = page; + this.listingUrl = 'admin/settings/email-templates'; + this.createButton = page.getByRole('link', { name: 'Create Email Template' }); + this.editButton = page.locator('//span[@class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center icon-edit"]').first(); + this.deleteButton = page.locator('//span[@class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center icon-delete"]').first(); + this.subjectInput = page.getByRole('textbox', { name: 'Subject' }); + this.contentInput = page.frameLocator(`iframe[id="content_ifr"]`); + this.nameInput = page.getByRole('textbox', { name: 'Name' }); + this.placeholderSelect = page.locator('#placeholder'); + this.saveButton = page.getByRole('button', { name: 'Save Email Template' }); + this.deleteConfirmButton = page.getByRole('button', { name: 'Delete' }); + } + + async createTemplate(emailTemplateData: EmailTemplateData) { + await this.createButton.click(); + await this.subjectInput.fill(emailTemplateData.subject); + + await this.nameInput.fill(emailTemplateData.name); + await this.contentInput.locator('body').fill(emailTemplateData.content); + await this.saveButton.click(); + } + + async updateEmailTemplate(emailTemplateData: EmailTemplateData) { + await this.editButton.click(); + await this.subjectInput.fill(emailTemplateData.subject); + await this.contentInput.locator('body').fill(emailTemplateData.content); + await this.saveButton.click(); + } + + async deleteEmailTemplate() { + await this.firstDeleteIcon.click(); + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } + async searchEmailTemplate(name: string) { + await this.searchInputField.fill(name); + await this.page.keyboard.press('Enter'); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/EventsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/EventsPage.ts new file mode 100644 index 0000000..97036d2 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/EventsPage.ts @@ -0,0 +1,82 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { SettingsPage } from '../SettingsPage'; +import { generateDescription, generateFullName, generateRandomDateTime } from '../../utils/faker'; + +export type EventData = { + name: string, + description: string, + date: string +} +export const eventdata: EventData = { + name: generateFullName(), + description: generateDescription(), + date: "2027-11-18", +} + +export class EventsPage extends SettingsPage { + readonly page: Page; + readonly createEventButton: Locator; + readonly nameInput: Locator; + readonly descriptionTextarea: Locator; + readonly dateInput: Locator; + readonly saveEventButton: Locator; + readonly editFirstEventButton: Locator; + readonly deleteFirstEventButton: Locator; + readonly confirmDeleteButton: Locator; + readonly successMessage: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + this.createEventButton = page.getByRole('button', { name: 'Create Event' }); + this.nameInput = page.locator('input[name="name"]'); + this.descriptionTextarea = page.locator('textarea[name="description"]'); + this.dateInput = page.locator('input[name="date"]'); + this.saveEventButton = page.getByRole('button', { name: 'Save Event' }); + this.editFirstEventButton = page.locator('.row > div:nth-child(6) > a').first(); + this.deleteFirstEventButton = page.locator('div:nth-child(6) > a:nth-child(2)').first(); + this.confirmDeleteButton = page.getByRole('button', { name: 'Agree', exact: true }); + this.successMessage = page.getByText(/Event (created|updated|deleted) successfully./); + } + + async createEvent(eventdata: EventData) { + + await this.createEventButton.click(); + await this.nameInput.fill(eventdata.name); + await this.descriptionTextarea.fill(eventdata.name); + await this.dateInput.fill(eventdata.date); + await this.saveEventButton.click(); + await expect(this.successMessage).toBeVisible(); + + } + async massDeleteEvents() { + await this.multiSelectCheckbox.click() + await this.deleteButton.click(); + await this.agreeButton.click(); + await expect(this.noRecordsAvailable).toBeVisible(); + + } + async editEvents(eventData: EventData) { + + await this.firstEditIcon.click(); + await this.nameInput.fill(eventdata.name); + await this.descriptionTextarea.fill(eventdata.name); + await this.dateInput.fill(eventdata.date); + await this.saveEventButton.click(); + await expect(this.successMessage).toBeVisible(); + } + async deleteEvent() { + await this.deleteFirstEventButton.click(); + await this.agreeButton.click(); + await expect(this.successMessage).toBeVisible(); + + } + async eventSearch(name: string) { + await this.searchInputExact.fill(name); + await this.page.keyboard.press('Enter'); + + } + + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/GroupsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/GroupsPage.ts new file mode 100644 index 0000000..a2d6a8a --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/GroupsPage.ts @@ -0,0 +1,71 @@ +// groupPage.js +import { expect, Locator, Page } from '@playwright/test'; +import { SettingsPage } from '../SettingsPage'; +import { generateDescription, generateFirstName, generateName } from '../../utils/faker'; +export type GroupData={ + name:string, + description:string +} +export const groupData:GroupData={ + name: generateFirstName()+" Group", + description: generateDescription().slice(0, 60), +} + +export class GroupPage extends SettingsPage { + readonly page:Page; + readonly createGroupButton: Locator; + readonly saveGroupButton: Locator; + readonly nameInput: Locator; + readonly descriptionTextarea: Locator; + readonly successMessageGroupCreated: Locator; + readonly successMessageGroupUpdated: Locator; + + constructor(page:Page) { + super(page); + this.page = page; + + // Buttons + this.createGroupButton = page.getByRole("button", { name: "Create group" }); + this.saveGroupButton = page.getByRole("button", { name: "Save Group" }); + + // Form fields + this.nameInput = page.locator('input[name="name"]'); + this.descriptionTextarea = page.locator('textarea[name="description"]'); + + // Notifications + this.successMessageGroupCreated = page.getByText("Group created successfully."); + this.successMessageGroupUpdated = page.getByText("Group updated successfully."); + } + async createGroup(groupData:GroupData): Promise + { + await this.createGroupButton.click(); + await this.nameInput.fill(groupData.name); + await this.descriptionTextarea.fill(groupData.description); + await this.saveGroupButton.click(); + await expect(this.successMessageGroupCreated.first()).toBeVisible(); + await this.searchGroup(groupData.name); + await expect(this.page.getByText(groupData.name).first()).toBeVisible(); + } + async updateGroup(groupData:GroupData): Promise + { + await this.firstEditIcon.click(); + await this.nameInput.fill(groupData.name); + await this.descriptionTextarea.fill(groupData.description); + await this.saveGroupButton.click(); + await expect(this.successMessageGroupUpdated.first()).toBeVisible(); + await this.searchGroup(groupData.name); + await expect(this.page.getByText(groupData.name).first()).toBeVisible(); + } + async deleteGroup(): Promise + { + await this.firstDeleteIcon.click(); + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } + async searchGroup(name:string): Promise + { + await this.searchInputExact.fill(name); + await this.page.keyboard.press('Enter'); + + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/OtherSettingsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/OtherSettingsPage.ts new file mode 100644 index 0000000..9df5488 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/OtherSettingsPage.ts @@ -0,0 +1,13 @@ +import { Page } from "playwright/test"; +import { SettingsPage } from "../SettingsPage"; + +export class OtherSettingsPage extends SettingsPage { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/PipelinesPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/PipelinesPage.ts new file mode 100644 index 0000000..b0b9087 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/PipelinesPage.ts @@ -0,0 +1,13 @@ +import { Page } from "playwright/test"; +import { SettingsPage } from "../SettingsPage"; + +export class PipelinesPage extends SettingsPage { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/RolesPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/RolesPage.ts new file mode 100644 index 0000000..305bedc --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/RolesPage.ts @@ -0,0 +1,212 @@ +import { expect, Locator, Page } from "@playwright/test"; + +import { SettingsPage } from "../SettingsPage"; +import { generateDescription, generateFirstName } from "../../utils/faker"; +import { PermissionType, runSuffix } from "../../utils/acl"; + +export type RoleData = { + name: string; + description: string; + permissionType: PermissionType; + + /** + * ACL keys to tick in the access-control tree. Only read when `permissionType` is `custom`. + */ + permissions?: string[]; +}; + +/** + * Build role data with a unique name, so repeated runs never collide in the listing. + */ +export function buildRoleData(overrides: Partial = {}): RoleData { + return { + name: `${generateFirstName()} Role ${runSuffix()}`, + description: generateDescription().slice(0, 60), + permissionType: "custom", + permissions: [], + ...overrides, + }; +} + +export class RolesPage extends SettingsPage { + readonly page: Page; + + readonly createRoleButton: Locator; + readonly saveRoleButton: Locator; + + readonly nameInput: Locator; + readonly descriptionTextarea: Locator; + readonly permissionTypeSelect: Locator; + + readonly createSuccessMessage: Locator; + readonly updateSuccessMessage: Locator; + readonly deleteSuccessMessage: Locator; + + readonly permissionsExceedOwnError: Locator; + readonly currentRoleEditError: Locator; + readonly currentRoleDeleteError: Locator; + readonly beingUsedError: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + + this.createRoleButton = page.getByRole("link", { name: "Create Roles" }); + this.saveRoleButton = page.getByRole("button", { name: "Save Role" }); + + this.nameInput = page.locator('input[name="name"]'); + this.descriptionTextarea = page.locator('textarea[name="description"]'); + this.permissionTypeSelect = page.locator('select[name="permission_type"]'); + + this.createSuccessMessage = page.getByText("Role created successfully."); + this.updateSuccessMessage = page.getByText("Role updated successfully."); + this.deleteSuccessMessage = page.getByText("Role deleted successfully."); + + this.permissionsExceedOwnError = page.getByText( + "You can only create or edit roles whose permissions are the same as or fewer than your own." + ); + this.currentRoleEditError = page.getByText("Can not edit the role assigned to your own account."); + this.currentRoleDeleteError = page.getByText("Can not delete role assigned to the current user."); + this.beingUsedError = page.getByText("Role can not be deleted, as this is being used in admin user."); + } + + /** + * The access-control tree renders each ACL key as a hidden checkbox carrying the key as its + * `value`. + * + * The `id` is not usable as an anchor: the tree component prefixes it with a token regenerated + * on every render (`mttr4p8q_leads.edit`), so only `value` is stable across page loads. The + * input itself is `hidden`, so the wrapping label is what a user actually clicks. + */ + permissionCheckbox(key: string): Locator { + return this.page.locator(`input[name="permissions[]"][value="${key}"]`); + } + + permissionLabel(key: string): Locator { + return this.page.locator(`label:has(input[name="permissions[]"][value="${key}"])`); + } + + async navigateToCreateRole(): Promise { + await this.page.goto("admin/settings/roles/create"); + } + + /** + * Tick an ACL key in the tree. + * + * Selection is hierarchical in both directions: ticking a key also ticks its ancestors AND its + * whole subtree. Ticking `leads` therefore grants `leads.delete` too. + * + * Pass only the most specific keys a role should hold and let the ancestors follow — listing a + * parent alongside its child silently widens the role far beyond what the test intended. + */ + async selectPermission(key: string): Promise { + const label = this.permissionLabel(key); + + await expect(label, `permission "${key}" should be offered in the tree`).toBeVisible(); + + if (!(await this.permissionCheckbox(key).isChecked())) { + await label.click(); + } + } + + async deselectPermission(key: string): Promise { + if (await this.permissionCheckbox(key).isChecked()) { + await this.permissionLabel(key).click(); + } + } + + async selectPermissions(keys: string[]): Promise { + for (const key of keys) { + await this.selectPermission(key); + } + } + + /** + * Whether an ACL key is offered at all. `Acl::getAuthorizedItems()` prunes the tree to the + * acting user's own permissions, so a key being absent is itself the assertion in the + * escalation suites. + */ + async isPermissionOffered(key: string): Promise { + return (await this.permissionLabel(key).count()) > 0; + } + + async fillRoleForm(role: RoleData): Promise { + await this.nameInput.fill(role.name); + await this.descriptionTextarea.fill(role.description); + await this.permissionTypeSelect.selectOption(role.permissionType); + + if (role.permissionType === "custom") { + await this.selectPermissions(role.permissions ?? []); + } + } + + /** + * Create a role through the form and confirm it lands in the listing. + */ + async createRole(role: RoleData): Promise { + await this.navigateToCreateRole(); + await this.fillRoleForm(role); + await this.saveRoleButton.click(); + + await expect(this.createSuccessMessage.first()).toBeVisible(); + + await this.searchRole(role.name); + + await expect(this.page.getByText(role.name).first()).toBeVisible(); + } + + /** + * Submit the create form without asserting the outcome, for cases where the expected result is + * a refusal rather than a new role. + */ + async submitRoleForm(role: RoleData): Promise { + await this.navigateToCreateRole(); + await this.fillRoleForm(role); + await this.saveRoleButton.click(); + } + + async searchRole(name: string): Promise { + await this.navigateToRoles(); + await this.searchInputExact.fill(name); + await this.page.keyboard.press("Enter"); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Open a named role's edit form by clicking its row's edit icon after searching for it, so the + * test never depends on the role's position in an unfiltered listing. + */ + async openRoleForEdit(name: string): Promise { + await this.searchRole(name); + await this.firstEditIcon.click(); + await this.page.waitForLoadState("networkidle"); + } + + async updateRole(name: string, changes: Partial): Promise { + await this.openRoleForEdit(name); + + if (changes.name) { + await this.nameInput.fill(changes.name); + } + + if (changes.description) { + await this.descriptionTextarea.fill(changes.description); + } + + if (changes.permissionType) { + await this.permissionTypeSelect.selectOption(changes.permissionType); + } + + if (changes.permissions?.length) { + await this.selectPermissions(changes.permissions); + } + + await this.saveRoleButton.click(); + } + + async deleteRole(name: string): Promise { + await this.searchRole(name); + await this.firstDeleteIcon.click(); + await this.agreeButton.click(); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/SourcesPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/SourcesPage.ts new file mode 100644 index 0000000..e29c543 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/SourcesPage.ts @@ -0,0 +1,39 @@ +import { expect, Locator, Page } from "playwright/test"; +import { SettingsPage } from "../SettingsPage"; + +export class SourcesPage extends SettingsPage { + readonly page: Page; + readonly createSourceLink:Locator; + readonly saveSourceButton:Locator; + readonly thisName:Locator; + + constructor(page: Page) { + super(page); + this.page = page; + this.createSourceLink=page.getByRole('button', { name: 'Create Source' }); + this.saveSourceButton=page.getByRole('button', { name: 'Save Source' }) + this.thisName=page.getByRole('textbox', { name: 'Name' }); + } + + async searchSource(thisName:string) + { + await this.searchInputExact.fill(thisName) + await this.page.keyboard.press('Enter'); + + } + async sourceForm(sourceName:string) + { + await this.thisName.fill(sourceName); + await this.saveSourceButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + await this.searchSource(sourceName); + await expect(this.page.getByRole('paragraph').filter({ hasText: sourceName }).first()).toBeVisible(); + } + async deleteSource() + { + await this.firstDeleteIcon.click() + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/TagsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/TagsPage.ts new file mode 100644 index 0000000..aa76966 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/TagsPage.ts @@ -0,0 +1,64 @@ +import { expect, Page, Locator } from '@playwright/test'; +import { generateName } from '../../utils/faker'; +import { SettingsPage } from '../SettingsPage'; + + +export class TagsPage extends SettingsPage { + readonly page: Page; + + // Locators + readonly gotoTagsPageButton: Locator; + readonly createTagButton: Locator; + readonly tagNameInput: Locator; + readonly saveTagButton: Locator; + readonly tagsUpdatedMessage: Locator; + readonly tagsCreatedMessage: Locator; + readonly tagsDeletedMessage: Locator; + readonly firstEditButton: Locator; + readonly firstDeleteButton: Locator; + readonly confirmDeleteButton: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + // Initialize locators + this.gotoTagsPageButton = page.getByRole('link', { name: /Tags/i }); + this.createTagButton = page.getByRole('button', { name: 'Create Tag' }); + this.tagNameInput = page.getByRole('textbox', { name: 'Name' }); + this.saveTagButton = page.getByRole('button', { name: 'Save Tag' }); + this.tagsCreatedMessage = page.getByText(/Tag created successfully\./); + this.tagsUpdatedMessage = page.getByText(/Tag updated successfully\./); + this.tagsDeletedMessage = page.getByText(/Tag deleted successfully\./); + + this.firstEditButton = page.locator('//span[@class="icon-edit cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center"]').first(); + this.firstDeleteButton = page.locator('//span[@class="icon-delete cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center"]').first(); + this.confirmDeleteButton = page.getByRole('button', { name: 'Agree', exact: true }); + } + + async createTag(name:string) { + await this.createTagButton.click(); + await this.tagNameInput.fill(name); + await this.saveTagButton.click(); + await expect(this.tagsCreatedMessage).toBeVisible(); + } + + async udpateTag(newName: string) { + await this.firstEditButton.click(); + await this.tagNameInput.fill(newName); + await this.saveTagButton.click(); + await expect(this.tagsUpdatedMessage).toBeVisible(); + } + + async deleteTag() { + await this.firstDeleteButton.click(); + await this.confirmDeleteButton.click(); + await expect(this.tagsDeletedMessage).toBeVisible(); + } + async seacrhTag(name:string): Promise + { + await this.searchInputExact.fill(name); + await this.page.keyboard.press('Enter'); + } + + // Optional: Any utility methods like search, verify list, etc., can be added here. +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/UsersPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/UsersPage.ts new file mode 100644 index 0000000..e0af861 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/UsersPage.ts @@ -0,0 +1,236 @@ +import { expect, Locator, Page } from "@playwright/test"; + +import { SettingsPage } from "../SettingsPage"; +import { generateEmail, generateFullName } from "../../utils/faker"; +import { TEST_PASSWORD, ViewPermission, runSuffix } from "../../utils/acl"; + +export type UserData = { + name: string; + email: string; + password: string; + roleName: string; + viewPermission: ViewPermission; + + /** + * Group names to associate. Required by the form when `viewPermission` is `group`. + */ + groups?: string[]; + + status?: boolean; +}; + +/** + * Build user data with a unique email, so a re-run never trips the unique constraint. + */ +export function buildUserData(overrides: Partial = {}): UserData { + const suffix = runSuffix(); + + return { + name: `${generateFullName()} ${suffix}`, + email: `acl.${suffix}.${generateEmail()}`, + password: TEST_PASSWORD, + roleName: "Administrator", + viewPermission: "global", + groups: [], + status: true, + ...overrides, + }; +} + +export class UsersPage extends SettingsPage { + readonly page: Page; + + readonly createUserButton: Locator; + readonly saveUserButton: Locator; + + readonly nameInput: Locator; + readonly emailInput: Locator; + readonly passwordInput: Locator; + readonly confirmPasswordInput: Locator; + readonly roleSelect: Locator; + readonly viewPermissionSelect: Locator; + readonly groupSelect: Locator; + readonly statusCheckbox: Locator; + + readonly createSuccessMessage: Locator; + readonly updateSuccessMessage: Locator; + readonly deleteSuccessMessage: Locator; + + readonly unauthorizedMessage: Locator; + readonly roleExceedsOwnMessage: Locator; + readonly scopeExceedsOwnMessage: Locator; + readonly ownPrivilegesMessage: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + + this.createUserButton = page.getByRole("button", { name: "Create User" }); + this.saveUserButton = page.getByRole("button", { name: "Save User" }); + + this.nameInput = page.locator('input[name="name"]'); + this.emailInput = page.locator('input[name="email"]'); + this.passwordInput = page.locator('input[name="password"]'); + this.confirmPasswordInput = page.locator('input[name="confirm_password"]'); + this.roleSelect = page.locator('select[name="role_id"]'); + this.viewPermissionSelect = page.locator('select[name="view_permission"]'); + this.groupSelect = page.locator('select[name="groups[]"]'); + /** + * The form ships a hidden `status` input carrying `0` alongside the real checkbox, so the + * name alone is ambiguous — the id is what identifies the control the user toggles. + */ + this.statusCheckbox = page.locator('input#status[type="checkbox"]'); + + this.createSuccessMessage = page.getByText("User created successfully."); + this.updateSuccessMessage = page.getByText("User updated successfully."); + this.deleteSuccessMessage = page.getByText("User deleted successfully."); + + this.unauthorizedMessage = page.getByText("This action is unauthorized."); + this.roleExceedsOwnMessage = page.getByText( + "You can only assign a role whose permissions are the same as or fewer than your own." + ); + this.scopeExceedsOwnMessage = page.getByText("You cannot grant a data scope wider than your own."); + this.ownPrivilegesMessage = page.getByText("You cannot change your own role or data scope."); + } + + async openCreateModal(): Promise { + await this.navigateToUsers(); + await this.createUserButton.click(); + await expect(this.nameInput).toBeVisible(); + } + + async fillUserForm(user: UserData): Promise { + await this.nameInput.fill(user.name); + await this.emailInput.fill(user.email); + await this.passwordInput.fill(user.password); + await this.confirmPasswordInput.fill(user.password); + + await this.roleSelect.selectOption({ label: user.roleName }); + await this.viewPermissionSelect.selectOption(user.viewPermission); + + /** + * The group field is only rendered for the `group` and `individual` scopes, and is + * mandatory for `group`. + */ + if (user.groups?.length) { + await this.groupSelect.selectOption(user.groups.map((label) => ({ label }))); + } + + /** + * Status is set explicitly rather than only when disabling: the create modal renders the + * toggle unchecked, so a user left at the default is created inactive and cannot sign in + * at all — which would fail every downstream authorization assertion for the wrong reason. + */ + await this.setStatus(user.status ?? true); + } + + /** + * Toggle the status control to a known state rather than blind-clicking, so a form that + * already defaults to the wanted value is not flipped away from it. + */ + async setStatus(active: boolean): Promise { + /** + * The checkbox is `sr-only`, so it is toggled through its own element with `force` rather + * than by a normal click, which Playwright would refuse as targeting a hidden element. + */ + if (active) { + await this.statusCheckbox.check({ force: true }); + } else { + await this.statusCheckbox.uncheck({ force: true }); + } + } + + /** + * Create a user through the modal and confirm the success flash. + */ + async createUser(user: UserData): Promise { + await this.openCreateModal(); + await this.fillUserForm(user); + await this.saveUserButton.click(); + + await expect(this.createSuccessMessage.first()).toBeVisible(); + } + + /** + * Submit the modal without asserting success, for cases where a refusal is the expectation. + */ + async submitUserForm(user: UserData): Promise { + await this.openCreateModal(); + await this.fillUserForm(user); + await this.saveUserButton.click(); + } + + async searchUser(term: string): Promise { + await this.navigateToUsers(); + await this.searchInputExact.fill(term); + await this.page.keyboard.press("Enter"); + await this.page.waitForLoadState("networkidle"); + } + + async openUserForEdit(term: string): Promise { + await this.searchUser(term); + await this.firstEditIcon.click(); + await expect(this.nameInput).toBeVisible(); + } + + /** + * Change an existing user's role and/or scope through the edit modal. + */ + async updateUser(term: string, changes: Partial): Promise { + await this.openUserForEdit(term); + + if (changes.name) { + await this.nameInput.fill(changes.name); + } + + if (changes.roleName) { + await this.roleSelect.selectOption({ label: changes.roleName }); + } + + if (changes.viewPermission) { + await this.viewPermissionSelect.selectOption(changes.viewPermission); + } + + if (changes.groups?.length) { + await this.groupSelect.selectOption(changes.groups.map((label) => ({ label }))); + } + + if (changes.status !== undefined) { + await this.setStatus(changes.status); + } + + await this.saveUserButton.click(); + } + + /** + * Deactivate a user, which the middleware treats as a session-level revocation on that user's + * next request. + */ + async deactivateUser(term: string): Promise { + await this.updateUser(term, { status: false }); + await expect(this.updateSuccessMessage.first()).toBeVisible(); + } + + async deleteUser(term: string): Promise { + await this.searchUser(term); + await this.firstDeleteIcon.click(); + await this.agreeButton.click(); + } + + /** + * Role names offered in the assignment dropdown. A non-administrator must never be offered a + * role broader than their own. + */ + async offeredRoleNames(): Promise { + return this.roleSelect.locator("option").allInnerTexts(); + } + + /** + * Scope values offered in the view-permission dropdown. + */ + async offeredScopes(): Promise { + return this.viewPermissionSelect.locator("option").evaluateAll((options) => + options.map((option) => (option as HTMLOptionElement).value) + ); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WarehousesPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WarehousesPage.ts new file mode 100644 index 0000000..7c77dff --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WarehousesPage.ts @@ -0,0 +1,104 @@ +// pages/WarehousePage.ts +import { Page, Locator, expect } from '@playwright/test'; +import { SettingsPage } from '../SettingsPage'; +import { generateDescription, generateEmail, generateLastName, generateLocation, generateName, generatePhoneNumber, generateRandomNumericString } from '../../utils/faker'; + +export type WarehouseData = { + name: string; + description: string; + contactName: string; + contactEmail: string; + contactAddress: string; + country: string; + state: string; + city: string; + zipcode: string; + phone: string; +}; + +export const warehouseData: WarehouseData = { + name: generateLastName(), + description: generateDescription().slice(0,20), + contactName: generateLastName(), + contactEmail:generateEmail(), + contactAddress: 'Plot 123, Industrial Area, Meerut, UP 250001', + country: 'India', + state: 'Uttar Pradesh', + city: 'Meerut', + zipcode: '250001', + phone: generatePhoneNumber(), +}; + +export class WarehousePage extends SettingsPage { + readonly page: Page; + readonly warehouseCreateLink: Locator; + readonly warehouseNameInput: Locator; + readonly warehouseAddressInput: Locator; + readonly saveButton: Locator; + readonly firstEditButton: Locator; + readonly firstDeleteButton: Locator; + readonly successMessage: Locator; + readonly warehousesTable: Locator; + readonly contactNameInput: Locator; + readonly contactEmailInput: Locator; + readonly contactPhoneInput: Locator; + readonly contactAddressTextarea: Locator; + readonly countrySelect: Locator; + readonly stateSelect: Locator; + readonly cityInput: Locator; + readonly zipcodeInput: Locator; + readonly warehouseDescriptionInput: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + this.warehouseCreateLink = page.getByRole('link', { name: 'Create Warehouse' }) + this.warehouseNameInput = page.getByRole('textbox', { name: 'Name *', exact: true }); + this.warehouseAddressInput = page.locator('textarea[name="contact_address[address]"]'); + this.saveButton = page.getByRole('button', { name: 'Save Warehouse' }) + this.firstEditButton = page.locator('span.cursor-pointer.icon-edit').first(); + this.firstDeleteButton = page.locator('span.cursor-pointer.icon-delete').first(); + this.successMessage = page.getByText(/Warehouse.*successfully/); + this.warehousesTable = page.getByRole('table'); + this.contactNameInput = page.getByRole('textbox', { name: 'Contact Name *' }); + this.contactEmailInput = page.getByRole('textbox', { name: 'Contact Emails *' }) + this.contactPhoneInput = page.getByRole('textbox', { name: 'Contact Numbers' }) + this.contactAddressTextarea = page.locator('textarea[name="contact_address[address]"]'); + this.countrySelect = page.locator('select[name="contact_address[country]"]'); + this.stateSelect = page.locator('select[name="contact_address[state]"]'); + this.cityInput = page.getByRole('textbox', { name: 'City' }); + this.zipcodeInput = page.getByRole('textbox', { name: 'Postcode' }); + this.warehouseDescriptionInput = page.getByRole('textbox', { name: 'Description' }); + } + + async warehouseForm(warehouseData: WarehouseData) { + await this.warehouseNameInput.fill(warehouseData.name); + await this.warehouseDescriptionInput.fill(warehouseData.description); + await this.warehouseAddressInput.fill(warehouseData.contactAddress); + await this.contactNameInput.fill(warehouseData.contactName); + await this.contactEmailInput.fill(warehouseData.contactEmail); + await this.contactPhoneInput.fill(warehouseData.phone); + await this.contactAddressTextarea.fill(warehouseData.contactAddress); + await this.countrySelect.selectOption({ label: warehouseData.country }); + await this.stateSelect.selectOption({ label: warehouseData.state }); + await this.cityInput.fill(warehouseData.city); + await this.zipcodeInput.fill(warehouseData.zipcode); + await this.saveButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + await this.searchWarehouse(warehouseData.contactName); + await expect(this.page.getByRole('paragraph').filter({ hasText: warehouseData.contactName }).first()).toBeVisible(); + + } + async searchWarehouse(warehouseName:string) + { + await this.searchInputExact.fill(warehouseName); + await this.page.keyboard.press('Enter'); + + } + async deleteWarehouse() + { + await this.firstDeleteButton.click() + await this.agreeButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WebFormsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WebFormsPage.ts new file mode 100644 index 0000000..41d05f7 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WebFormsPage.ts @@ -0,0 +1,13 @@ +import { Page } from "playwright/test"; +import { SettingsPage } from "../SettingsPage"; + +export class WebFormsPage extends SettingsPage { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WebhooksPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WebhooksPage.ts new file mode 100644 index 0000000..e136336 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WebhooksPage.ts @@ -0,0 +1,100 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { SettingsPage } from '../SettingsPage'; +import { generateDescription, generateFirstName } from '../../utils/faker'; + +export type WebhookData = { + url: string; + name: string; + description: string; + entity: string; +}; + +export const webhookData: WebhookData = { + url: 'https://example.com/webhook', + name: generateFirstName(), + description: generateDescription().slice(0, 50), + entity: 'persons', +}; +export class WebhookPage extends SettingsPage { + readonly page: Page; + readonly createButton: Locator; + readonly editButton: Locator; + readonly deleteButton: Locator; + readonly urlEndpointInput: Locator; + readonly nameInput: Locator; + readonly descriptionInput: Locator; + readonly entityTypeSelect: Locator; + readonly queryParamKeyInput: Locator; + readonly queryParamValueInput: Locator; + readonly contentTypeRadio: Locator; + readonly payloadKeyInput: Locator; + readonly payloadValueInput: Locator; + readonly saveButton: Locator; + readonly deleteConfirmButton: Locator; + readonly listingUrl: string; + + constructor(page: Page) { + super(page); + this.page = page; + this.listingUrl = 'admin/settings/webhooks'; + + // Main actions + this.createButton = page.getByRole('link', { name: 'Create Webhook' }); + this.editButton = page.locator('(//span[@class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center icon-edit"])[1]'); + this.deleteButton = page.locator('(//span[@class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center icon-delete"])[1]'); + + // Form fields + this.urlEndpointInput = page.getByRole('textbox', { name: 'Url Endpoint' }); + this.nameInput = page.getByRole('textbox', { name: 'Name' }); + this.descriptionInput = page.getByRole('textbox', { name: 'Description' }); + this.entityTypeSelect = page.locator('#entity_type'); + this.queryParamKeyInput = page.locator('input[name="query_params[0][key]"]'); + this.queryParamValueInput = page.locator('[id="query_params[0][value]"]'); + this.contentTypeRadio = page.getByRole('radio', { name: 'x-www-form-urlencoded' }); + this.payloadKeyInput = page.locator('input[name="payload[0][key]"]'); + this.payloadValueInput = page.locator('input[name="payload[0][value]"]'); + this.saveButton = page.getByRole('button', { name: 'Save Webhook' }); + this.deleteConfirmButton = page.getByRole('button', { name: 'Agree', exact: true }); + } + + + async createWebhook(webhookData: WebhookData) { + await this.createButton.click(); + await this.urlEndpointInput.fill(webhookData.url); + await this.nameInput.fill(webhookData.name); + await this.entityTypeSelect.selectOption(webhookData.entity); + await this.descriptionInput.fill(webhookData.description); + await this.queryParamKeyInput.fill('key'); + await this.queryParamValueInput.fill('value'); + await this.contentTypeRadio.check(); + await this.payloadKeyInput.fill('key'); + await this.payloadValueInput.fill('value'); + await this.saveButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } + + async updateFirstWebhook(webhookData: WebhookData) { + await this.editButton.click(); + await this.urlEndpointInput.fill(webhookData.url); + await this.nameInput.fill(webhookData.name); + await this.descriptionInput.fill(webhookData.description); + await this.entityTypeSelect.selectOption(webhookData.entity); + await this.queryParamKeyInput.fill('updatedKey'); + await this.queryParamValueInput.fill('updatedValue'); + await this.payloadKeyInput.fill('updatedPayloadKey'); + await this.payloadValueInput.fill('updatedPayloadValue'); + await this.saveButton.click(); + await this.searchWebhook(webhookData.name); + await expect(this.page.locator('p.break-words', { hasText: webhookData.url }).first()).toBeVisible(); + } + + async deleteWebhook() { + await this.deleteButton.click(); + await this.deleteConfirmButton.click(); + await expect(this.successMessage.first()).toBeVisible(); + } + async searchWebhook(name: string) { + await this.searchInputExact.fill(name); + await this.page.keyboard.press('Enter'); + } +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WorkflowsPage.ts b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WorkflowsPage.ts new file mode 100644 index 0000000..2c36719 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/pages/settings/WorkflowsPage.ts @@ -0,0 +1,13 @@ +import { Page } from "playwright/test"; +import { SettingsPage } from "../SettingsPage"; + +export class WorkflowsPage extends SettingsPage { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + + +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/playwright.config.ts b/packages/Webkul/Admin/tests/e2e-pw/playwright.config.ts new file mode 100644 index 0000000..63b5554 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const TESTS_ROOT_PATH = __dirname; +export const STATE_DIR_PATH = `${ TESTS_ROOT_PATH }/.state/`; +export const ADMIN_AUTH_STATE_PATH = `${ STATE_DIR_PATH }/admin-auth.json`; + +dotenv.config({ path: path.resolve(__dirname, "../../../../../.env") }); + +export default defineConfig({ + testDir: "./tests", + + timeout: 120 * 1000, + + expect: { timeout: 20 * 1000 }, + + outputDir: "./test-results", + + fullyParallel: false, + + workers: 1, + + forbidOnly: !!process.env.CI, + + retries: 0, + + reportSlowTests: null, + + reporter: [ + [ + "html", + { + outputFolder: "./playwright-report", + }, + ], + ], + + use: { + baseURL: `${process.env.APP_URL}/`.replace(/\/+$/, "/"), + screenshot: { mode: "only-on-failure", fullPage: true }, + video: "retain-on-failure", + trace: "retain-on-failure", + }, + + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/admin.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/admin.spec.ts new file mode 100644 index 0000000..fef8e2e --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/admin.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from "../fixtures/AdminFixtures"; +import { AdminPage } from "../pages/AdminPage"; + + +test.describe("admin mangement", async () => { + + test("verify admin login", async ({ adminPage }) => { + + expect(adminPage.waitForURL("admin/dashboard")); + + }) + test("verify admin logout", async ({ adminPage }) => { + const admin = await new AdminPage(adminPage); + + await admin.adminLogout(); + await admin.verifyLogout(); + + + + }) + + +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/lang/lang.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/lang/lang.spec.ts new file mode 100644 index 0000000..b227507 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/lang/lang.spec.ts @@ -0,0 +1,82 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { test, expect } from '@playwright/test'; + +// Resolve file paths relative to this test file +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Absolute path to /lang directory +const LANG_DIR = path.resolve( + __dirname, + '../../../../src/Resources/lang' +); + +const BASE_LANG = 'en'; + +// Helper to extract just the keys from app.php +function getNormalizedKeys(filePath: string): string[] { + const raw = fs.readFileSync(filePath, 'utf-8'); + + const clean = raw + .replace(/<\?php\s*/g, '') + .replace(/return\s*\[/, '') + .replace(/\];/, '') + .trim(); + + return clean + .split('\n') + .map(line => line.trim()) + .filter(line => line.includes('=>') && !line.startsWith('//')) + .map(line => { + // Extract the key before => + const match = line.match(/^['"](.+?)['"]\s*=>/); + return match ? match[1] : null; + }) + .filter(Boolean) as string[]; +} +test('All language files must match number of keys and key names with English app.php', () => { + const baseFile = path.join(LANG_DIR, BASE_LANG, 'app.php'); + const baseKeys = getNormalizedKeys(baseFile); + // All locales except the base one + const locales = fs + .readdirSync(LANG_DIR) + .filter(locale => locale !== BASE_LANG && fs.existsSync(path.join(LANG_DIR, locale, 'app.php'))); + + // Array to collect any locales that have issues + let failedLocales: { locale: string; missingKeys: string[]; extraKeys: string[] }[] = []; + + for (const locale of locales) { + const filePath = path.join(LANG_DIR, locale, 'app.php'); + const keys = getNormalizedKeys(filePath); + + const missingKeys = baseKeys.filter(key => !keys.includes(key)); + const extraKeys = keys.filter(key => !baseKeys.includes(key)); + + if (missingKeys.length > 0 || extraKeys.length > 0) { + failedLocales.push({ locale, missingKeys, extraKeys }); + } + } + + if (failedLocales.length > 0) { + for (const { locale, missingKeys, extraKeys } of failedLocales) { + console.error(` ${locale}/app.php has issues:`); + + if (missingKeys.length) { + console.error(` Missing keys (${missingKeys.length}):`); + for (const key of missingKeys) console.error(` - ${key}`); + } + + if (extraKeys.length) { + console.error(` Extra keys (${extraKeys.length}):`); + for (const key of extraKeys) console.error(` + ${key}`); + } + } + + // Fail the test + expect(failedLocales).toEqual([]); + } else { + console.log('All language files have matching keys and counts with en/app.php'); + } +}); \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/lead.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/lead.spec.ts new file mode 100644 index 0000000..7935dee --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/lead.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from "../fixtures/AdminFixtures"; +import { LeadData, leadData, LeadPage } from "../pages/LeadPage"; +import PersonsPage from "../pages/PersonsPage"; +import { generateDescription, generateEmail, generateName, generatePhoneNumber, generateSKU } from "../utils/faker"; + + +test.describe("lead management", async () => { + + const updatedLeadData={ + title: generateName(), + description: generateDescription(), + value: (Math.floor(Math.random() * 10000)).toString(), + expectedCloseDate: "2028-12-31", + person: leadData.person, + product: leadData.product, + organizationName: leadData.person.organizationName + + } + + test("should create a new lead", async ({ adminPage }) => { + + const leadPage = new LeadPage(adminPage); + const personPage = new PersonsPage(adminPage); + await personPage.navigageToPersonsPage(); + /** + * Create the person the lead form will then select. It has to be the lead flow's own + * person, not the shared one, or this collides with the person spec's record. + */ + await personPage.createPerson(leadData.person); + + await leadPage.createLead(leadData); + + + + }); + + test("should update an existing lead", async ({ adminPage }) => { + + const lead=new LeadPage(adminPage); + + await lead.navigateToLeadList(); + const updatelead = await lead.searchLead(leadData.title); + await updatelead.updateLead(updatedLeadData); + + }); + test("user should able to delete the lead", async ({ adminPage }) => { + const leadPage = new LeadPage(adminPage); + + await leadPage.navigateToLeadList(); + await leadPage.deleteLead(updatedLeadData); + + + }) + + +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/mail.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/mail.spec.ts new file mode 100644 index 0000000..f2a6822 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/mail.spec.ts @@ -0,0 +1,89 @@ +import { expect, test } from "../fixtures/AdminFixtures"; +import { mailData, MailPage } from "../pages/MailPage"; + +test.describe("mail management", async () => { + + test("should compose and send an email", async ({ adminPage }) => { + + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.composeEmail(mailData,'normal'); + }); + test("should compose and send an email with cc", async ({ adminPage }) => { + + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.composeEmail(mailData,'cc'); + }); + test("should compose and send an email with bcc", async ({ adminPage }) => { + + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.composeEmail(mailData,'bcc'); + }); + test("should compose and send an email with cc and bcc", async ({ adminPage }) => { + + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.composeEmail(mailData,'both'); + }); + test("should able to save normal draft email", async ({ adminPage }) => { + + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.searchEmail(mailData.subject); + await mailPage.saveDraftEmail(mailData,'normal'); + + }); + test("should able to save cc draft email", async ({ adminPage }) => { + + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.searchEmail(mailData.subject); + await mailPage.saveDraftEmail(mailData,'cc'); + + }); + test("should able to save bcc draft email", async ({ adminPage }) => { + + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.searchEmail(mailData.subject); + await mailPage.saveDraftEmail(mailData,'bcc'); + + }); + test("should able to save cc and bcc draft email", async ({ adminPage }) => { + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.searchEmail(mailData.subject); + await mailPage.saveDraftEmail(mailData,'both'); + + }); + test("should able to move email to trash", async ({ adminPage }) => { + + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.saveDraftEmail(mailData,'normal'); + + await mailPage.navigateToMailDraftPage(); + await mailPage.searchEmail(mailData.subject); + await mailPage.movedEmailToTrash(); + await mailPage.navigateToMailTrashedPage(); + await expect(adminPage.getByRole('paragraph').filter({ hasText: mailData.subject }).first()).toBeVisible(); + + }); + test("should able to delete email from trash", async ({ adminPage }) => { + const mailPage = new MailPage(adminPage); + await mailPage.navigateToMailInboxPage(); + await mailPage.saveDraftEmail(mailData,'normal'); + + await mailPage.navigateToMailDraftPage(); + await mailPage.searchEmail(mailData.subject); + await mailPage.movedEmailToTrash(); + + await mailPage.navigateToMailTrashedPage(); + await mailPage.deleteEmailFromTrash(mailData.subject); + await expect(adminPage.getByRole('paragraph').filter({ hasText: mailData.subject }).first()).not.toBeVisible(); + + }); + +}); \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/organization.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/organization.spec.ts new file mode 100644 index 0000000..758d5f7 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/organization.spec.ts @@ -0,0 +1,51 @@ +import { test } from "../fixtures/AdminFixtures"; +import OrganizationPage, { organizationData } from "../pages/OrganizationPage"; + + +test.describe("organization mangement",async()=>{ + + + + + test("verify organization create",async({adminPage})=>{ + + const organizationPage=new OrganizationPage(adminPage); + await organizationPage.navigateToOrganization(); + await organizationPage.createOrganization(organizationData); + + + }) + + test('verify update organization',async({adminPage})=>{ + const organizationPage=new OrganizationPage(adminPage); + await organizationPage.navigateToOrganization(); + await organizationPage.updateOrganization(organizationData); + + }) + + + test("verify delete organization",async({adminPage})=>{ + const organizationPage=new OrganizationPage(adminPage); + await organizationPage.navigateToOrganization(); + await organizationPage.searchByName(organizationData.name); + await organizationPage.deleteOrganization(); + + + }) + test("verify mass delete organization",async({adminPage})=>{ + + const organizationPage=new OrganizationPage(adminPage); + await organizationPage.navigateToOrganization(); + + /* create organization before delete*/ + + await organizationPage.createOrganization(organizationData); + + /* than mass delete organization*/ + await organizationPage.massDelete(); + + }) + + + +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/persons.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/persons.spec.ts new file mode 100644 index 0000000..c4ec5f6 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/persons.spec.ts @@ -0,0 +1,51 @@ +import { expect, test } from "../fixtures/AdminFixtures"; +import PersonsPage, { buildPersonData } from "../pages/PersonsPage"; + +test.describe("person managment", async () => { + /** + * This spec builds its own person rather than importing the shared `personData`. Email and + * contact number are unique in Krayin, and the lead and quote specs create a person too — with + * one shared object all three submit the same email and the later ones fail validation with + * "The value has already been taken." + * + * The instance is created once for the whole describe so that create, edit and delete all + * address the same record. + */ + const personData = buildPersonData(); + + test("verify create person", async ({ adminPage }) => { + const personPage = new PersonsPage(adminPage); + + await personPage.navigageToPersonsPage(); + await personPage.createPerson(personData); + }) + + test('verify edit person', async ({ adminPage }) => { + const person = new PersonsPage(adminPage); + + await person.navigageToPersonsPage(); + await person.updatePerson(personData); + }) + + test('verify person delete', async ({ adminPage }) => { + const person = new PersonsPage(adminPage); + + await person.navigageToPersonsPage(); + await person.searchByName(personData.name); + await person.personDelete(); + }) + + test('verify mass delete person', async ({ adminPage }) => { + const person = new PersonsPage(adminPage); + + /** + * A person of its own: the record created above has just been deleted, but reusing its + * email would still collide if the delete case is skipped or run out of order. + */ + const massDeletePersonData = buildPersonData(); + + await person.navigageToPersonsPage(); + await person.createPerson(massDeletePersonData); + await person.personMassDelete(); + }) +}) diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/product.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/product.spec.ts new file mode 100644 index 0000000..8aa590c --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/product.spec.ts @@ -0,0 +1,31 @@ +import { test } from "../fixtures/AdminFixtures"; +import { productData, ProductPage } from "../pages/ProductPage"; + +test.describe("Product mangement", async () => { + test("verify create product", async ({ adminPage }) => { + const product = new ProductPage(adminPage); + + await product.navigateToProductPage(); + await product.createProductLink.click(); + await product.productForm(productData); + + }); + test("verify edit product", async ({ adminPage }) => { + const product = new ProductPage(adminPage); + + await product.navigateToProductPage(); + await product.firstEditIcon.click(); + const updatedProductData = { + ...productData, + name: productData.name + " - Updated", + price: (parseInt(productData.price) + 100).toString(), + } + await product.productForm(updatedProductData); + + }) + test("verify delete product", async ({ adminPage }) => { + const product = new ProductPage(adminPage); + await product.navigateToProductPage(); + await product.deleteProduct(); + }) +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/quick-add.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/quick-add.spec.ts new file mode 100644 index 0000000..6d12580 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/quick-add.spec.ts @@ -0,0 +1,72 @@ +import { test } from "../fixtures/AdminFixtures"; +import { + QuickAddPage, + quickAddLeadData, + quickAddMailData, + quickAddOrganizationData, + quickAddPersonData, + quickAddProductData, +} from "../pages/QuickAddPage"; + +test.describe("quick add modal", async () => { + + test("should open the quick add modal from the header plus button", async ({ adminPage }) => { + + const quickAddPage = new QuickAddPage(adminPage); + + /** + * The seeded admin role has all five quick create permissions. + */ + await quickAddPage.verifyQuickAddTabs(["Lead", "Person", "Organization", "Product", "Email"]); + + }); + + test("should create a lead via quick add", async ({ adminPage }) => { + + const quickAddPage = new QuickAddPage(adminPage); + + await quickAddPage.createLead(quickAddLeadData); + + }); + + test("should create a person via quick add", async ({ adminPage }) => { + + const quickAddPage = new QuickAddPage(adminPage); + + await quickAddPage.createPerson(quickAddPersonData); + + }); + + test("should create an organization via quick add", async ({ adminPage }) => { + + const quickAddPage = new QuickAddPage(adminPage); + + await quickAddPage.createOrganization(quickAddOrganizationData); + + }); + + test("should create a product via quick add", async ({ adminPage }) => { + + const quickAddPage = new QuickAddPage(adminPage); + + await quickAddPage.createProduct(quickAddProductData); + + }); + + test("should send an email via quick add", async ({ adminPage }) => { + + const quickAddPage = new QuickAddPage(adminPage); + + await quickAddPage.createMail(quickAddMailData); + + }); + + test("should keep modal open and surface validation errors when fields are empty", async ({ adminPage }) => { + + const quickAddPage = new QuickAddPage(adminPage); + + await quickAddPage.submitEmptyForm("Lead"); + + }); + +}); diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/quotes.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/quotes.spec.ts new file mode 100644 index 0000000..14955c1 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/quotes.spec.ts @@ -0,0 +1,55 @@ +import { title } from "process"; +import { test, expect } from "../fixtures/AdminFixtures"; +import { AdminPage } from "../pages/AdminPage"; +import { LeadData, LeadPage } from "../pages/LeadPage"; +import OrganizationPage, { OrganizationData } from "../pages/OrganizationPage"; +import PersonsPage, { PersonData } from "../pages/PersonsPage"; +import { ProductData, productData, ProductPage } from "../pages/ProductPage"; +import { quoteData, QuotesPage, } from "../pages/QuotesPage"; +import { generateDescription, generateEmail, generateLocation, generateName, generatePhoneNumber, generateSKU } from "../utils/faker"; + +test.describe("quotes mangement", async () => { + + test("verify create quote", async ({ adminPage }) => { + const person = new PersonsPage(adminPage); + const product = new ProductPage(adminPage); + await person.navigageToPersonsPage(); + + await person.createPerson(quoteData.person); + await product.navigateToProductPage(); + await product.createProductLink.click(); + /** + * The quote flow's own product, so this does not collide with the product the lead and product + * specs create earlier in the same worker. + */ + await product.productForm(quoteData.product); + const quote = new QuotesPage(adminPage); + + await quote.navigateToQuotesPage(); + await quote.createQuote(quoteData); + + await expect(quote.successMessage.first()).toBeVisible({ timeout: 10000 }); + }) + test('verify updated quote', async ({ adminPage }) => { + const updateQuoteData = { + ...quoteData, + subject: quoteData.subject + " - Updated", + title:"updated title", + quoteNumber:"updated quote number", + description:"updated description", + notes:"updated notes", + + } + const quote = new QuotesPage(adminPage); + await quote.navigateToQuotesPage(); + await quote.searchByName(quoteData.subject); + await quote.updateQuote(updateQuoteData); + }) + test('verify delete quote', async ({ adminPage }) => { + const quote = new QuotesPage(adminPage); + await quote.navigateToQuotesPage(); + await quote.searchByName(quoteData.subject); + await quote.deleteQuote(); + }) + +}) diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/security/acl-permission.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/security/acl-permission.spec.ts new file mode 100644 index 0000000..c197508 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/security/acl-permission.spec.ts @@ -0,0 +1,245 @@ +import { expect, test } from "../../fixtures/AdminFixtures"; + +import { AclPage } from "../../pages/AclPage"; +import { buildRoleData, RolesPage } from "../../pages/settings/RolesPage"; +import { buildUserData, UsersPage } from "../../pages/settings/UsersPage"; +import { + ACL, + ADMIN_EMAIL, + ADMIN_PASSWORD, + ALLOWLISTED_ROUTES, + GUARDED_ROUTES, + Session, + TEST_PASSWORD, + closeSessions, + expectAllowed, + expectDenied, + getRoute, + postRoute, + signInExpectingSuccess, +} from "../../utils/acl"; + +/** + * The ACL gate: which actions a role may perform. + * + * Every case here fixes the data scope at `global`, so a refusal can only come from the action + * gate. Record-level scoping is covered separately in `view-permission.spec.ts`. + * + * The suite deliberately asserts at all three layers, because only one of them is a security + * boundary: + * + * - menu and button visibility -> presentation, may hide a control + * - direct GET / POST -> the middleware, must refuse + * + * A test that only checks a hidden button proves nothing about the endpoint behind it. + */ +test.describe.serial("acl action authorization", () => { + /** + * A deliberately narrow role: leads read-only, nothing else. It holds `leads.view` but neither + * `leads.create`, `leads.edit` nor `leads.delete`, which makes it useful for both the + * "permission present" and "permission absent" halves of the matrix. + */ + const leadsReaderRole = buildRoleData({ + permissions: [ACL.leadsView], + }); + + const leadsReader = buildUserData({ + roleName: leadsReaderRole.name, + viewPermission: "global", + }); + + let readerSession: Session | undefined; + + test.beforeAll(async ({ browser, baseURL }) => { + const admin = await signInExpectingSuccess(browser, baseURL, ADMIN_EMAIL, ADMIN_PASSWORD); + + try { + await new RolesPage(admin.page).createRole(leadsReaderRole); + await new UsersPage(admin.page).createUser(leadsReader); + } finally { + await closeSessions(admin); + } + + readerSession = await signInExpectingSuccess( + browser, + baseURL, + leadsReader.email, + TEST_PASSWORD + ); + }); + + test.afterAll(async () => { + await closeSessions(readerSession); + }); + + test("an administrator reaches every guarded route", async ({ browser, baseURL }) => { + const admin = await signInExpectingSuccess(browser, baseURL, ADMIN_EMAIL, ADMIN_PASSWORD); + + try { + for (const route of GUARDED_ROUTES) { + const response = await getRoute(admin, route.path); + + expectAllowed(response, `${route.label} for an administrator`); + } + } finally { + await closeSessions(admin); + } + }); + + test("a held permission grants the route", async () => { + const response = await getRoute(readerSession!, "admin/leads"); + + expectAllowed(response, "lead listing for a user holding 'leads'"); + }); + + test("the dashboard is refused without its permission", async () => { + /** + * The reader role holds no `dashboard` key, so even the landing screen must be refused — + * this is the case that proves the middleware is not treating any route as implicitly + * public for an authenticated user. + */ + const response = await getRoute(readerSession!, "admin/dashboard"); + + expectDenied(response, "dashboard without the 'dashboard' permission"); + }); + + /** + * Every guarded route the reader role does not hold must be refused on a direct GET, with no + * reliance on the UI having hidden a link to it. + */ + for (const route of GUARDED_ROUTES.filter( + (candidate) => ![ACL.leads, ACL.leadsView].includes(candidate.permission as never) + )) { + test(`refuses a direct GET to ${route.path} without '${route.permission}'`, async () => { + const response = await getRoute(readerSession!, route.path); + + expectDenied(response, route.label); + }); + } + + test("refuses a direct POST to lead create without 'leads.create'", async () => { + /** + * Replaying the form submit by hand is the case a hidden button cannot cover. The payload + * is intentionally incomplete: authorization must refuse before validation ever runs, so a + * 422 here would itself be a failure. + */ + const response = await postRoute(readerSession!, "admin/leads/create", { + title: "unauthorized lead", + }); + + expectDenied(response, "lead store without 'leads.create'"); + + expect(response.status(), "authorization must refuse before validation").not.toBe(422); + }); + + test("refuses a direct POST to user create without the settings permission", async () => { + const response = await postRoute(readerSession!, "admin/settings/users/create", { + name: "unauthorized user", + email: `escalation-${Date.now()}@example.invalid`, + }); + + expectDenied(response, "user store without 'settings.user.users.create'"); + }); + + test("refuses mass-delete without the delete permission", async () => { + /** + * The URL segment is `mass-destroy`; `mass_delete` is only the route's name. Posting the + * name instead reaches a different route entirely and answers 405, which would mask + * whatever the real endpoint does. + */ + const response = await postRoute(readerSession!, "admin/leads/mass-destroy", { + "indices[]": "1", + }); + + expectDenied(response, "lead mass delete without 'leads.delete'"); + }); + + test("hides navigation entries the role does not hold", async () => { + const aclPage = new AclPage(readerSession!.page); + + await readerSession!.page.goto("admin/leads"); + + await aclPage.expectNavVisible("/admin/leads"); + + /** + * Paths rather than labels, matching the routes in the menu config that `Menu::getItems()` + * filters through `bouncer()->hasPermission()`. + */ + for (const path of [ + "/admin/quotes", + "/admin/contacts/persons", + "/admin/products", + "/admin/settings/settings", + ]) { + await aclPage.expectNavHidden(path); + } + }); + + test("hides create and delete controls the role does not hold", async () => { + const aclPage = new AclPage(readerSession!.page); + + await readerSession!.page.goto("admin/leads"); + + /** + * The role holds `leads` and `leads.view` only, so the listing renders but its write + * controls must not. + */ + await aclPage.expectControlHidden("Create Lead"); + }); + + test("allows self-service account routes for any authenticated user", async () => { + /** + * These sit on the middleware's allow-list. They must stay reachable for a role that holds + * almost nothing, otherwise a restricted user cannot manage their own profile. + */ + for (const path of ALLOWLISTED_ROUTES) { + const response = await getRoute(readerSession!, path); + + expectAllowed(response, `allow-listed route ${path}`); + } + }); + + test("refuses an unmapped route under a feature the role does not hold", async () => { + /** + * `admin.settings.users.search` is a real route with no entry of its own in `acl.php`. The + * middleware fails closed by authorizing it against the nearest mapped ancestor, so a role + * without any `settings.user.users` permission must not be able to enumerate users through + * it. + * + * A URL that simply does not exist would 404 before the middleware ever ran and would + * prove nothing, which is why this uses a route that genuinely resolves. + */ + const response = await getRoute(readerSession!, "admin/settings/users/search"); + + expectDenied(response, "an unmapped route under a feature the role lacks"); + }); + + test("allows an unmapped route under a feature the role does hold", async () => { + /** + * The other half of the fallback: `admin.leads.search` is equally unmapped, but the reader + * holds `leads`, so the ancestor lookup must let it through. Without this case a middleware + * that denied every unmapped route would look correct. + */ + const response = await getRoute(readerSession!, "admin/leads/search"); + + expectAllowed(response, "an unmapped route under a feature the role holds"); + }); + + test("refuses everything once the account is deactivated", async ({ browser, baseURL }) => { + const admin = await signInExpectingSuccess(browser, baseURL, ADMIN_EMAIL, ADMIN_PASSWORD); + + try { + await new UsersPage(admin.page).deactivateUser(leadsReader.email); + } finally { + await closeSessions(admin); + } + + /** + * The existing session must not survive deactivation — the middleware tears it down on the + * next request rather than waiting for the cookie to expire. + */ + const response = await getRoute(readerSession!, "admin/leads"); + + expectDenied(response, "an existing session after the account was deactivated"); + }); +}); diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/security/auth-security.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/security/auth-security.spec.ts new file mode 100644 index 0000000..c5185d0 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/security/auth-security.spec.ts @@ -0,0 +1,149 @@ +import { test, expect } from "../../fixtures/AdminFixtures"; + +/** + * Security regressions around admin authentication, exercised through the browser. + * + * Each case here maps to a fix: brute-force throttling that counts only failures, a logout that + * tears the whole session down, a forgot-password form that does not disclose which addresses are + * registered, authentication actually being required, and the installer staying shut on an + * installed application. + */ + +const LOGIN_URL = "admin/login"; + +/** + * Submit the login form and hand back the POST response, so the status code can be asserted + * directly rather than inferred from what the page happens to render. + */ +async function submitLogin(page, email: string, password: string) { + await page.goto(LOGIN_URL); + + await page.locator('input[name="email"]').fill(email); + await page.locator('input[name="password"]').fill(password); + + const [response] = await Promise.all([ + page.waitForResponse( + (r) => r.url().includes("/admin/login") && r.request().method() === "POST" + ), + page.getByRole("button", { name: "Sign In" }).click(), + ]); + + return response; +} + +test.describe("admin authentication hardening", () => { + test("throttles repeated failed logins", async ({ page }) => { + /** + * An address that cannot match an account, so a real administrator is never locked out by + * this test and no account state is touched. + */ + const email = `brute-force-probe-${Date.now()}@example.invalid`; + + for (let attempt = 1; attempt <= 5; attempt++) { + const response = await submitLogin(page, email, "wrong-password"); + + expect(response.status(), `attempt ${attempt} should not be throttled yet`).not.toBe(429); + } + + const blocked = await submitLogin(page, email, "wrong-password"); + + expect(blocked.status()).toBe(429); + }); + + test("does not throttle a correct sign-in", async ({ browser, baseURL }) => { + /** + * Counting every attempt rather than only the failures would throttle this suite itself, + * which signs in for each of its cases from a single address. + * + * Each sign-in runs in its own browser context: visiting the login page while already + * signed in redirects to the dashboard, so reusing one context would leave no form to fill. + */ + for (let attempt = 1; attempt <= 3; attempt++) { + const context = await browser.newContext({ baseURL }); + const page = await context.newPage(); + + try { + const response = await submitLogin(page, "admin@example.com", "admin123"); + + expect(response.status(), `sign-in ${attempt} should not be throttled`).not.toBe(429); + + await expect(page).toHaveURL(/\/admin\/dashboard/); + } finally { + await context.close(); + } + } + }); + + test("requires authentication for admin pages", async ({ page }) => { + await page.context().clearCookies(); + + await page.goto("admin/dashboard"); + + await expect(page).toHaveURL(/\/admin\/login/); + }); + + test("ends the session on logout so the dashboard is no longer reachable", async ({ adminPage }) => { + await expect(adminPage).toHaveURL(/\/admin\/dashboard/); + + const profileToggle = adminPage.getByRole("banner").getByRole("button").last(); + + await profileToggle.click(); + await adminPage.getByRole("link", { name: "Sign Out" }).click(); + + await expect(adminPage).toHaveURL(/\/admin\/login/); + + /** + * Returning to a page that required authentication must not serve it again — the session + * is invalidated on logout, not merely forgotten. + */ + await adminPage.goto("admin/dashboard"); + + await expect(adminPage).toHaveURL(/\/admin\/login/); + }); + + test("does not disclose whether an email belongs to an account", async ({ page }) => { + await page.goto("admin/forget-password"); + + await page.locator('input[name="email"]').fill(`unknown-${Date.now()}@example.invalid`); + await page.getByRole("button", { name: "Reset" }).click(); + + /** + * The old behaviour answered "this email does not exist", turning the form into a way of + * testing which addresses are registered users. + */ + await expect(page.getByText(/does not exist|not exist|no user|not found/i)).toHaveCount(0); + }); +}); + +test.describe("installer lockdown", () => { + /** + * The installer API overwrites the administrator account and re-runs migrations against live + * data. On an installed application every one of these must be refused. + */ + const installerEndpoints = [ + "install/api/admin-config-setup", + "install/api/run-migration", + "install/api/run-seeder", + "install/api/env-file-setup", + ]; + + test("redirects the installer page away on an installed application", async ({ page }) => { + await page.goto("install"); + + await expect(page).not.toHaveURL(/\/install$/); + }); + + for (const endpoint of installerEndpoints) { + test(`refuses ${endpoint}`, async ({ page }) => { + const response = await page.request.post(endpoint, { + failOnStatusCode: false, + maxRedirects: 0, + }); + + expect( + response.status(), + `${endpoint} must not execute on an installed application` + ).not.toBe(200); + }); + } +}); diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/security/privilege-escalation.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/security/privilege-escalation.spec.ts new file mode 100644 index 0000000..d3d8eb5 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/security/privilege-escalation.spec.ts @@ -0,0 +1,333 @@ +import { expect, test } from "../../fixtures/AdminFixtures"; + +import { GroupPage } from "../../pages/settings/GroupsPage"; +import { buildRoleData, RolesPage } from "../../pages/settings/RolesPage"; +import { buildUserData, UsersPage } from "../../pages/settings/UsersPage"; +import { + ACL, + ADMIN_EMAIL, + ADMIN_PASSWORD, + Session, + TEST_PASSWORD, + closeSessions, + expectDenied, + findRecordId, + postRoute, + runSuffix, + signInExpectingSuccess, +} from "../../utils/acl"; + +/** + * Privilege escalation: a delegated administrator must never be able to grant, to anyone including + * themselves, more authority than they already hold. + * + * The acting user throughout is a "manager" who can administer roles and users but holds only a + * narrow slice of the lead permissions. Every case asks the same question from a different angle: + * can that manager end up with `leads.delete`, a full administrator role, or a wider data scope + * than the `group` scope they were given? + */ +test.describe.serial("privilege escalation guards", () => { + const suffix = runSuffix(); + + /** + * The manager can administer roles and users, and holds leads read + edit — but deliberately + * not `leads.delete`, which becomes the permission every escalation attempt tries to obtain. + */ + const managerRole = buildRoleData({ + name: `ACL Manager Role ${suffix}`, + /** + * Leaf keys only. Listing `leads` here would tick the whole subtree and hand the manager + * the very `leads.delete` every case below is trying to prove they cannot obtain. + */ + permissions: [ + ACL.dashboard, + ACL.leadsView, + ACL.leadsEdit, + ACL.settingsRolesCreate, + ACL.settingsRolesEdit, + ACL.settingsUsersCreate, + ACL.settingsUsersEdit, + ], + }); + + /** + * The manager is scoped to `group`, which the user form requires a group for + * (`required_if:view_permission,group`) — so the group has to exist before the account does. + */ + const managerGroup = { + name: `ACL Manager Group ${suffix}`, + description: "Group backing the escalation suite's manager account", + }; + + const manager = buildUserData({ + name: `ACL Manager ${suffix}`, + roleName: managerRole.name, + viewPermission: "group", + groups: [managerGroup.name], + }); + + let managerSession: Session | undefined; + + /** + * The manager's own role id, needed to build payloads that are *valid* — the controller + * validates before it authorizes, so an incomplete payload answers 422 and never reaches the + * escalation guard the test is trying to exercise. + */ + let managerRoleId: number | null = null; + + /** + * The manager's own user id, so the self-edit cases target the real account. Resolved from the + * users datagrid JSON, because the listing renders its edit control without a scrapable id. + */ + let managerUserId: number | null = null; + + /** + * The manager's group id, required to keep a `group`-scoped update payload valid. + */ + let managerGroupId: number | null = null; + + test.beforeAll(async ({ browser, baseURL }) => { + const admin = await signInExpectingSuccess(browser, baseURL, ADMIN_EMAIL, ADMIN_PASSWORD); + + try { + const groupPage = new GroupPage(admin.page); + + await groupPage.navigateToGroups(); + await groupPage.createGroup(managerGroup); + + await new RolesPage(admin.page).createRole(managerRole); + await new UsersPage(admin.page).createUser(manager); + + managerRoleId = await findRecordId( + admin, + "admin/settings/roles", + "name", + managerRole.name + ); + + managerUserId = await findRecordId( + admin, + "admin/settings/users", + "name", + manager.name + ); + + managerGroupId = await findRecordId( + admin, + "admin/settings/groups", + "name", + managerGroup.name + ); + } finally { + await closeSessions(admin); + } + + managerSession = await signInExpectingSuccess(browser, baseURL, manager.email, TEST_PASSWORD); + }); + + test.afterAll(async () => { + await closeSessions(managerSession); + }); + + test.describe("the access-control tree is pruned to the actor's own permissions", () => { + test("offers permissions the manager holds", async () => { + const rolesPage = new RolesPage(managerSession!.page); + + await rolesPage.navigateToCreateRole(); + await rolesPage.permissionTypeSelect.selectOption("custom"); + + expect( + await rolesPage.isPermissionOffered(ACL.leadsEdit), + "a permission the manager holds should be grantable" + ).toBeTruthy(); + }); + + test("hides permissions the manager does not hold", async () => { + const rolesPage = new RolesPage(managerSession!.page); + + await rolesPage.navigateToCreateRole(); + await rolesPage.permissionTypeSelect.selectOption("custom"); + + /** + * `Acl::getAuthorizedItems()` prunes the tree, so `leads.delete` must not even be + * offered. The tree is only the first line — the server-side cases below prove it is + * not the only one. + */ + expect( + await rolesPage.isPermissionOffered(ACL.leadsDelete), + "a permission the manager lacks must not be offered" + ).toBeFalsy(); + }); + }); + + test("a manager may create a role that is a subset of their own", async () => { + const rolesPage = new RolesPage(managerSession!.page); + + const subsetRole = buildRoleData({ + name: `ACL Subset Role ${suffix}`, + permissions: [ACL.leads, ACL.leadsView], + }); + + await rolesPage.createRole(subsetRole); + }); + + test("refuses a role carrying a permission the manager lacks", async () => { + /** + * The tree never offers `leads.delete`, so this posts the form directly — the case that + * matters, because a pruned UI is not an authorization control. + */ + const response = await postRoute(managerSession!, "admin/settings/roles/create", { + name: `Escalated Role ${suffix}`, + description: "attempts to grant a permission the actor lacks", + permission_type: "custom", + "permissions[]": ACL.leadsDelete, + }); + + expectDenied(response, "a role granting 'leads.delete' from an actor who lacks it"); + }); + + test("refuses promoting a role to full administrator", async () => { + const response = await postRoute(managerSession!, "admin/settings/roles/create", { + name: `Escalated Admin Role ${suffix}`, + description: "attempts to create a full administrator role", + permission_type: "all", + }); + + expectDenied(response, "a full administrator role created by a non-administrator"); + }); + + test("refuses editing a role broader than the manager's own", async () => { + const rolesPage = new RolesPage(managerSession!.page); + + /** + * The seeded Administrator role holds `permission_type = all`, so it is strictly broader + * than the manager's. Editing it would be the shortest path to full control. + */ + await rolesPage.searchRole("Administrator"); + + /** + * The update route is a PUT, so the verb is spoofed through `_method` exactly as the admin + * panel's own forms do — posting without it answers 405 and never reaches the guard. + */ + const response = await postRoute(managerSession!, "admin/settings/roles/edit/1", { + _method: "put", + name: "Administrator", + description: "tampered by a non-administrator", + permission_type: "all", + }); + + expectDenied(response, "editing a role broader than the actor's own"); + }); + + test("refuses assigning the administrator role to a new user", async () => { + /** + * `individual` scope keeps the payload valid without a group (the form requires one only + * for `group`), so the only thing left for the controller to object to is the role. + */ + const response = await postRoute(managerSession!, "admin/settings/users/create", { + name: `Escalated User ${suffix}`, + email: `escalated.${suffix}@example.test`, + password: TEST_PASSWORD, + confirm_password: TEST_PASSWORD, + role_id: "1", + view_permission: "individual", + }); + + expectDenied(response, "assigning the administrator role by a non-administrator"); + }); + + test("refuses granting a data scope wider than the manager's own", async () => { + /** + * The manager's scope is `group`. Creating a `global` user would hand that account + * visibility the manager does not have, and is refused by the scope ceiling. + */ + const response = await postRoute(managerSession!, "admin/settings/users/create", { + name: `Wider Scope User ${suffix}`, + email: `wider.${suffix}@example.test`, + password: TEST_PASSWORD, + confirm_password: TEST_PASSWORD, + role_id: String(managerRoleId ?? ""), + view_permission: "global", + }); + + expectDenied(response, "granting 'global' scope from a 'group'-scoped actor"); + }); + + test("allows granting a scope at or below the manager's own", async () => { + const usersPage = new UsersPage(managerSession!.page); + + const narrowerUser = buildUserData({ + name: `Narrower Scope User ${suffix}`, + roleName: managerRole.name, + viewPermission: "individual", + }); + + /** + * The ceiling narrows, it does not forbid delegation outright — a manager must still be + * able to create accounts at or below their own authority, or role administration becomes + * unusable. + */ + await usersPage.createUser(narrowerUser); + }); + + test("refuses changing the manager's own role", async () => { + expect(managerUserId, "the manager's own user id should resolve").not.toBeNull(); + + const response = await postRoute( + managerSession!, + `admin/settings/users/edit/${managerUserId}`, + { + _method: "put", + name: manager.name, + email: manager.email, + role_id: "1", + view_permission: "group", + "groups[]": String(managerGroupId ?? ""), + } + ); + + expectDenied(response, "a user changing their own role"); + }); + + test("refuses widening the manager's own data scope", async () => { + expect(managerUserId, "the manager's own user id should resolve").not.toBeNull(); + + const response = await postRoute( + managerSession!, + `admin/settings/users/edit/${managerUserId}`, + { + _method: "put", + name: manager.name, + email: manager.email, + role_id: String(managerRoleId ?? ""), + view_permission: "global", + } + ); + + expectDenied(response, "a user widening their own data scope"); + }); + + test("refuses editing the role assigned to the manager's own account", async () => { + const rolesPage = new RolesPage(managerSession!.page); + + await rolesPage.openRoleForEdit(managerRole.name); + + /** + * Nobody, administrators included, may edit the role attached to their own account — the + * one guard that closes the loop on every other ceiling in this suite. + */ + await expect(rolesPage.currentRoleEditError.first()).toBeVisible(); + }); + + test("the manager never gains the permission every attempt was after", async () => { + /** + * The closing assertion: after all of the above, a direct mass-delete must still be + * refused. If any earlier guard leaked, this is where it shows. + */ + const response = await postRoute(managerSession!, "admin/leads/mass-destroy", { + "indices[]": "1", + }); + + expectDenied(response, "lead mass delete after every escalation attempt"); + }); +}); diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/security/view-permission.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/security/view-permission.spec.ts new file mode 100644 index 0000000..042e4a2 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/security/view-permission.spec.ts @@ -0,0 +1,283 @@ +import { expect, test } from "../../fixtures/AdminFixtures"; + +import { AclPage } from "../../pages/AclPage"; +import { GroupPage, groupData } from "../../pages/settings/GroupsPage"; +import { buildRoleData, RolesPage } from "../../pages/settings/RolesPage"; +import { buildUserData, UsersPage } from "../../pages/settings/UsersPage"; +import { + ACL, + ADMIN_EMAIL, + ADMIN_PASSWORD, + Session, + TEST_PASSWORD, + closeSessions, + expectAllowed, + expectDenied, + getRoute, + findRecordId, + runSuffix, + signInExpectingSuccess, +} from "../../utils/acl"; + +/** + * The data-scope gate: whose records an action may touch. + * + * Every user in this suite holds an identical, generous ACL role, so a record being hidden can + * only be the work of `view_permission`. That separation is the whole point — a suite that varies + * both dimensions at once cannot attribute a refusal to either. + * + * Shape of the fixture: + * + * globalUser -> view_permission = global -> sees every person + * groupUserA -> view_permission = group (G) -> sees A's and B's people + * groupUserB -> view_permission = group (G) -> sees A's and B's people + * individualUser -> view_permission = individual -> sees only their own + */ +test.describe.serial("view permission data scoping", () => { + const suffix = runSuffix(); + + /** + * Only leaf keys are listed — the tree pulls in `contacts` and `contacts.persons` as ancestors. + * `contacts.persons.view` is included deliberately: without it, the "opened directly by id" + * case would be refused by the ACL gate and prove nothing about data scope. + */ + const contactsRole = buildRoleData({ + permissions: [ + ACL.dashboard, + ACL.contactsPersonsQuickCreate, + ACL.contactsPersonsView, + ], + }); + + const sharedGroup = { + name: `ACL Scope Group ${suffix}`, + description: "Group shared by the view-permission suite", + }; + + const globalUser = buildUserData({ roleName: contactsRole.name, viewPermission: "global" }); + const groupUserA = buildUserData({ + roleName: contactsRole.name, + viewPermission: "group", + groups: [sharedGroup.name], + }); + const groupUserB = buildUserData({ + roleName: contactsRole.name, + viewPermission: "group", + groups: [sharedGroup.name], + }); + const individualUser = buildUserData({ + roleName: contactsRole.name, + viewPermission: "individual", + }); + + /** + * One person per owner, named distinctly so a listing assertion is unambiguous. + */ + const personOfGlobal = `Global Person ${suffix}`; + const personOfGroupA = `GroupA Person ${suffix}`; + const personOfGroupB = `GroupB Person ${suffix}`; + const personOfIndividual = `Individual Person ${suffix}`; + + let globalSession: Session | undefined; + let groupASession: Session | undefined; + let groupBSession: Session | undefined; + let individualSession: Session | undefined; + + test.beforeAll(async ({ browser, baseURL }) => { + const admin = await signInExpectingSuccess(browser, baseURL, ADMIN_EMAIL, ADMIN_PASSWORD); + + try { + const groupPage = new GroupPage(admin.page); + + await groupPage.navigateToGroups(); + await groupPage.createGroup(sharedGroup); + + await new RolesPage(admin.page).createRole(contactsRole); + + const usersPage = new UsersPage(admin.page); + + await usersPage.createUser(globalUser); + await usersPage.createUser(groupUserA); + await usersPage.createUser(groupUserB); + await usersPage.createUser(individualUser); + } finally { + await closeSessions(admin); + } + + globalSession = await signInExpectingSuccess(browser, baseURL, globalUser.email, TEST_PASSWORD); + groupASession = await signInExpectingSuccess(browser, baseURL, groupUserA.email, TEST_PASSWORD); + groupBSession = await signInExpectingSuccess(browser, baseURL, groupUserB.email, TEST_PASSWORD); + individualSession = await signInExpectingSuccess( + browser, + baseURL, + individualUser.email, + TEST_PASSWORD + ); + + /** + * Each person is created by the user who should own it. Creating them as an administrator + * and reassigning would leave ownership to the form's default, which is exactly the + * behaviour under test. + */ + await new AclPage(globalSession.page).createPerson(personOfGlobal, `g.${suffix}@example.test`); + await new AclPage(groupASession.page).createPerson(personOfGroupA, `ga.${suffix}@example.test`); + await new AclPage(groupBSession.page).createPerson(personOfGroupB, `gb.${suffix}@example.test`); + await new AclPage(individualSession.page).createPerson( + personOfIndividual, + `i.${suffix}@example.test` + ); + }); + + test.afterAll(async () => { + await closeSessions(globalSession, groupASession, groupBSession, individualSession); + }); + + test("global scope sees records owned by everyone", async () => { + const aclPage = new AclPage(globalSession!.page); + + for (const person of [personOfGlobal, personOfGroupA, personOfGroupB, personOfIndividual]) { + await aclPage.expectRecordListed("admin/contacts/persons", person); + } + }); + + test("individual scope sees only its own records", async () => { + const aclPage = new AclPage(individualSession!.page); + + await aclPage.expectRecordListed("admin/contacts/persons", personOfIndividual); + + for (const person of [personOfGlobal, personOfGroupA, personOfGroupB]) { + await aclPage.expectRecordNotListed("admin/contacts/persons", person); + } + }); + + test("group scope sees records owned by fellow group members", async () => { + const aclPage = new AclPage(groupASession!.page); + + /** + * A and B share one group, so each must see the other's record — this is the assertion that + * separates group scope from individual scope. + */ + await aclPage.expectRecordListed("admin/contacts/persons", personOfGroupA); + await aclPage.expectRecordListed("admin/contacts/persons", personOfGroupB); + }); + + test("group scope does not see records owned outside the group", async () => { + const aclPage = new AclPage(groupASession!.page); + + for (const person of [personOfGlobal, personOfIndividual]) { + await aclPage.expectRecordNotListed("admin/contacts/persons", person); + } + }); + + test("group scope is symmetric between members", async () => { + const aclPage = new AclPage(groupBSession!.page); + + await aclPage.expectRecordListed("admin/contacts/persons", personOfGroupA); + await aclPage.expectRecordListed("admin/contacts/persons", personOfGroupB); + }); + + test("a record created under a scope is owned, not orphaned", async () => { + /** + * A person created with a null owner is invisible to every non-global user, including the + * one who created it. Re-reading the creator's own listing is what catches that. + */ + const aclPage = new AclPage(individualSession!.page); + + await aclPage.expectRecordListed("admin/contacts/persons", personOfIndividual); + }); + + /** + * KNOWN DEFECT — marked fixme so it is recorded without failing the suite. + * + * `PersonController::store()` assigns `user_id` from the acting user only when the request + * carries `quick_add`. A person created through the standard create form without picking a + * Sales Owner is therefore saved with `user_id = null` and disappears from its own creator's + * listing the moment that creator is scoped to `group` or `individual`. + * + * `LeadRepository::create()` already guards exactly this case for the person it creates, with a + * comment naming the same failure mode, so the standard form path is missing a guard its + * sibling paths have. + * + * Verified on branch 2.2: five persons created this way all persisted with `user_id = NULL`, + * and the individual-scope creator's listing reported "0 - 0 of 0". + */ + test.fixme("a person created through the standard form is owned by its creator", async () => { + const aclPage = new AclPage(individualSession!.page); + + const orphanCandidate = `Standard Form Person ${suffix}`; + + await individualSession!.page.goto("admin/contacts/persons/create"); + await individualSession!.page.waitForLoadState("networkidle"); + + await aclPage.personNameTextbox.fill(orphanCandidate); + await aclPage.personEmailTextbox.fill(`sf.${suffix}@example.test`); + await aclPage.personPhoneTextbox.fill(AclPage.uniquePhone()); + + await aclPage.savePersonButton.click(); + await individualSession!.page.waitForLoadState("networkidle"); + + await aclPage.expectRecordListed("admin/contacts/persons", orphanCandidate); + }); + + test("the action gate stays open while the scope narrows the result", async () => { + /** + * A narrowed scope must not turn into a 401 on the listing itself — the user still holds + * `contacts.persons`, so the page renders and simply contains fewer rows. + */ + const response = await getRoute(individualSession!, "admin/contacts/persons"); + + expectAllowed(response, "person listing for an individual-scope user holding the permission"); + }); + + test("the user listing is itself scoped by view permission", async () => { + /** + * `UserDataGrid` runs the same `getAuthorizedUserIds()` filter, so a restricted user must + * not enumerate the whole staff list. Without the settings permission the request is + * refused outright, which is an equally acceptable outcome here. + */ + const response = await getRoute(individualSession!, "admin/settings/users"); + + expectDenied(response, "user listing for a role without the settings permission"); + }); + + test("a person owned by another user cannot be opened directly", async () => { + /** + * Listing filters hide a record; `preventUnauthorizedAccess()` is what stops it being + * fetched by id anyway. + * + * The id is resolved from the datagrid's JSON as the global user, because the person + * listing renders no per-row links to scrape. Replaying that id as the individual user is + * the actual assertion. + */ + const personId = await findRecordId( + globalSession!, + "admin/contacts/persons", + "person_name", + personOfGroupA + ); + + expect( + personId, + "the global user should be able to resolve another owner's person id" + ).not.toBeNull(); + + const response = await getRoute(individualSession!, `admin/contacts/persons/view/${personId}`); + + expectDenied(response, "another owner's person fetched directly by id"); + }); + + test("a scoped user cannot resolve another owner's record through the grid json", async () => { + /** + * The same lookup run as the individual user must come back empty — the grid's JSON is + * scoped by the identical `getAuthorizedUserIds()` filter that hides the row in the UI. + */ + const personId = await findRecordId( + individualSession!, + "admin/contacts/persons", + "person_name", + personOfGroupA + ); + + expect(personId, "another owner's person must not appear in a scoped grid response").toBeNull(); + }); +}); diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/attritube.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/attritube.spec.ts new file mode 100644 index 0000000..df37ea7 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/attritube.spec.ts @@ -0,0 +1,31 @@ + +import { generateFullName, generateSKU } from "../../utils/faker"; +import { attributeData, AttributesPage } from "../../pages/settings/AttributesPage"; +import { test } from "../../fixtures/AdminFixtures"; +import { AdminPage } from "../../pages/AdminPage"; +test.describe("attribute managment ", async () => { + + test("should create an attribute", async ({ adminPage }) => { + const attributePage = new AttributesPage(adminPage); + await attributePage.navigateToAttributes(); + await attributePage.createAttribute(attributeData); + + }); + test("verify update attribute", async ({ adminPage }) => { + const attributePage = new AttributesPage(adminPage) + await attributePage.navigateToAttributes(); + await attributePage.searchAttribute(attributeData.name.toLowerCase() + ); + await attributePage.updateAttribute(attributeData); + + }) + test("verify delete attribute",async({adminPage})=>{ + const attributePage= new AttributesPage(adminPage); + await attributePage.navigateToAttributes(); + await attributePage.searchAttribute(attributeData.name); + await attributePage.deleteAttribute(); + }) + + + +}) diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/campaign.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/campaign.spec.ts new file mode 100644 index 0000000..ae6a0b8 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/campaign.spec.ts @@ -0,0 +1,44 @@ +import { test } from "../../fixtures/AdminFixtures"; +import { AdminPage } from "../../pages/AdminPage"; +import { CampaignData, campaignData, CampaignsPage } from "../../pages/settings/CampaignsPage"; +import { eventdata, EventsPage } from "../../pages/settings/EventsPage"; + + + +test.describe('campaign management ', () => { + const updatedCampaignData: CampaignData = { + name: "updated name", + event: eventdata, + subject: "updated subject", + emailTemplateId: "hslafuieiwfnew", + active: 1 + + } + + test('verify campaing create', async ({ adminPage }) => { + /*before creating campaign created event for the campaing */ + const event = await new EventsPage(adminPage); + await event.navigateToEvents(); + await event.createEvent(campaignData.event); + /*creating campaing after created event*/ + const campaign = new CampaignsPage(adminPage); + await campaign.navigateToCampaigns(); + await campaign.createCampaign(campaignData); + + }) + test('verify udpate campaign', async ({ adminPage }) => { + const campaign = new CampaignsPage(adminPage); + await campaign.navigateToCampaigns(); + await campaign.searchByName(campaignData.name); + await campaign.editCampaign(campaignData); + }) + test('verify delete campaign', async ({ adminPage }) => { + const campaign = new CampaignsPage(adminPage); + await campaign.navigateToCampaigns(); + await campaign.searchByName(campaignData.name); + await campaign.deleteCampaign(); + + }); + + +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/data-transfer.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/data-transfer.spec.ts new file mode 100644 index 0000000..7ac3459 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/data-transfer.spec.ts @@ -0,0 +1,135 @@ +import path from "path"; +import { fileURLToPath } from "url"; +import { expect, test } from "../../fixtures/AdminFixtures"; +import { DataTransferPage } from "../../pages/settings/DataTransferPage"; +import OrganizationPage, { organizationData } from "../../pages/OrganizationPage"; +import PersonsPage, { buildPersonData } from "../../pages/PersonsPage"; +import { allowedNodeEnvironmentFlags } from "process"; +const updatedCsvFileName = 'leads_updated.csv'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const csvFileName ={ + leads: 'leads.csv', + updatedLeads: 'leads_updated.csv', + persons: 'persons.csv', + updatedPersons: 'persons_updated.csv', + products: 'products.csv', + updatedProducts: 'products_updated.csv', + +} + + +test.describe("data transfer product management",async()=>{ + test("verify create import product",async({adminPage})=>{ + + const dataTransferPage=new DataTransferPage(adminPage ); + const csvPath = path.resolve(__dirname, `../../data/data-transfer/${csvFileName.products}`); + + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.createImport(csvPath,'products'); + } + ) + test("verify update import product",async({adminPage})=>{ + const updatedCsvPath = path.resolve(__dirname, `../../data/data-transfer/${csvFileName.updatedProducts}`); + const dataTransferPage=new DataTransferPage(adminPage ); + + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.updateImport(updatedCsvPath); + await expect(dataTransferPage.successMessage.first()).toBeVisible(); + } + ) + test("verify delete import product",async({adminPage})=>{ + const dataTransferPage=new DataTransferPage(adminPage ); + + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.deleteImport(); + await expect(dataTransferPage.successMessage.first()).toBeVisible(); + } + ) +}) + + +test.describe("data transfer lead management",async()=>{ + let personId:string | null; + + + test("verify create import lead",async({adminPage})=>{ + const csvPath = path.resolve(__dirname, `../../data/data-transfer/${csvFileName.leads}`); + const person = new PersonsPage(adminPage); + const organization=new OrganizationPage(adminPage); + + + await person.navigageToPersonsPage(); + + /** + * Its own person: email and contact number are unique in Krayin, so sharing one object with + * the person, lead and quote specs makes whichever runs later fail validation. + */ + await person.createPerson(buildPersonData()); + personId = await person.firstPerson.textContent(); + + + const dataTransferPage=new DataTransferPage(adminPage ); + await dataTransferPage.updateCsv(csvPath, 'person_id', personId?.toString() || '1' ); + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.createImport(csvPath,'leads'); + } + ) + test("verify update import lead",async({adminPage})=>{ + const updatedCsvPath = path.resolve(__dirname, `../../data/data-transfer/${csvFileName.updatedLeads}`); + const person = new PersonsPage(adminPage); + const dataTransferPage=new DataTransferPage(adminPage ); + await person.navigageToPersonsPage(); + personId = await person.firstPerson.textContent(); + + await dataTransferPage.updateCsv(updatedCsvPath, 'person_id', personId?.toString() || '1' ); + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.updateImport(updatedCsvPath); + await expect(dataTransferPage.successMessage.first()).toBeVisible(); + } + ) + test("verify delete import lead",async({adminPage})=>{ + const dataTransferPage=new DataTransferPage(adminPage ); + + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.deleteImport(); + await expect(dataTransferPage.successMessage.first()).toBeVisible(); + } + ) +}) +test.describe("data transfer person management",async()=>{ + let organizationId:string | null; + test("verify create import person",async({adminPage})=>{ + const csvPath = path.resolve(__dirname, `../../data/data-transfer/${csvFileName.persons}`); + const organization=new OrganizationPage(adminPage); + await organization.navigateToOrganization(); + await organization.createOrganization(organizationData); + + const dataTransferPage=new DataTransferPage(adminPage ); + organizationId=await organization.firstOrganization.textContent(); + await dataTransferPage.updateCsv(csvPath, 'organization_id', organizationId?.toString() || '1' ); + + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.createImport(csvPath,'persons'); + } + ) + test("verify update import person",async({adminPage})=>{ + const updatedCsvPath = path.resolve(__dirname, `../../data/data-transfer/${csvFileName.updatedPersons}`); + const dataTransferPage=new DataTransferPage(adminPage ); + await dataTransferPage.updateCsv(updatedCsvPath, 'organization_id', organizationId?.toString() || '1' ); + + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.updateImport(updatedCsvPath); + await expect(dataTransferPage.successMessage.first()).toBeVisible(); + } + + ) + test("verify delete import person",async({adminPage})=>{ + const dataTransferPage=new DataTransferPage(adminPage ); + + await dataTransferPage.navigateToDataTransfer(); + await dataTransferPage.deleteImport(); + await expect(dataTransferPage.successMessage.first()).toBeVisible(); + } + ) +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/email-tamplet.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/email-tamplet.spec.ts new file mode 100644 index 0000000..f93c5f2 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/email-tamplet.spec.ts @@ -0,0 +1,22 @@ +import { test } from "../../fixtures/AdminFixtures"; +import { emailTemplateData, EmailTemplatePage } from "../../pages/settings/EmailTemplatesPage"; + +test.describe("email template management", () => { + test("verify create an email template", async ({ adminPage }) => { + const emailTemplatePage = new EmailTemplatePage(adminPage); + await emailTemplatePage.navigateToEmailTemplates(); + await emailTemplatePage.createTemplate(emailTemplateData); + }); + + test("verify update an email template", async ({ adminPage }) => { + const emailTemplatePage = new EmailTemplatePage(adminPage); + await emailTemplatePage.navigateToEmailTemplates(); + await emailTemplatePage.updateEmailTemplate(emailTemplateData); + }); + + test("verify delete an email template", async ({ adminPage }) => { + const emailTemplatePage = new EmailTemplatePage(adminPage); + await emailTemplatePage.navigateToEmailTemplates(); + await emailTemplatePage.deleteEmailTemplate(); + }); +}); \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/events.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/events.spec.ts new file mode 100644 index 0000000..adcd348 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/events.spec.ts @@ -0,0 +1,28 @@ +import { env } from "process"; +import { test } from "../../fixtures/AdminFixtures"; +import { eventdata, EventsPage } from "../../pages/settings/EventsPage"; +import { AdminPage } from "../../pages/AdminPage"; + +test.describe('event management', () => { + + + + test('create event', async ({ adminPage }) => { + const event = await new EventsPage(adminPage); + await event.navigateToEvents(); + await event.createEvent(eventdata); + + }) + test('verify update event', async ({ adminPage }) => { + const event = await new EventsPage(adminPage); + await event.navigateToEvents(); + await event.searchByName(eventdata.name); + await event.editEvents(eventdata); + }) + test('verify delete event', async ({ adminPage }) => { + const event = await new EventsPage(adminPage); + await event.navigateToEvents(); + await event.searchByName(eventdata.name); + await event.deleteEvent(); + }) +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/group.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/group.spec.ts new file mode 100644 index 0000000..9bd80be --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/group.spec.ts @@ -0,0 +1,25 @@ +import { test } from "../../fixtures/AdminFixtures"; +import { groupData, GroupPage } from "../../pages/settings/GroupsPage"; + +test.describe("group managment",async()=>{ + test("verify create group",async({adminPage})=>{ + const groupPage=new GroupPage(adminPage); + await groupPage.navigateToGroups(); + await groupPage.createGroup(groupData); + }) + test("verify update group",async({adminPage})=>{ + const groupPage=new GroupPage(adminPage); + await groupPage.navigateToGroups(); + const updatedGroupData={ + name: groupData.name+" Updated", + description: groupData.description+" Updated" + } + await groupPage.updateGroup(updatedGroupData); + }) + test("verify delete group",async({adminPage})=>{ + const groupPage=new GroupPage(adminPage); + await groupPage.navigateToGroups(); + + }) + +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/roles.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/roles.spec.ts new file mode 100644 index 0000000..5af52aa --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/roles.spec.ts @@ -0,0 +1,117 @@ +import { expect, test } from "../../fixtures/AdminFixtures"; +import { buildRoleData, RolesPage } from "../../pages/settings/RolesPage"; +import { ACL } from "../../utils/acl"; + +/** + * Role management CRUD, plus the access-control tree behaviour the ACL suites depend on. + * + * These run as an administrator, so nothing here is an authorization assertion — the escalation + * rules are covered in `tests/security/privilege-escalation.spec.ts`. + */ +test.describe.serial("role management", () => { + const role = buildRoleData({ + permissions: [ACL.leads, ACL.leadsView], + }); + + test("creates a custom role with selected permissions", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + await rolesPage.createRole(role); + }); + + test("creates a full administrator role", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + const adminRole = buildRoleData({ permissionType: "all" }); + + await rolesPage.createRole(adminRole); + + /** + * Selecting `all` must hide the permission tree entirely — the checkboxes are only + * meaningful for a custom role. + */ + await rolesPage.navigateToCreateRole(); + await rolesPage.permissionTypeSelect.selectOption("all"); + + await expect(rolesPage.permissionLabel(ACL.leads)).toHaveCount(0); + }); + + test("requires a name and a description", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + await rolesPage.navigateToCreateRole(); + await rolesPage.saveRoleButton.click(); + + /** + * The form must not submit — staying on the create URL is what proves validation fired. + */ + await expect(adminPage).toHaveURL(/\/admin\/settings\/roles\/create/); + }); + + test("requires at least one permission for a custom role", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + const emptyRole = buildRoleData({ permissions: [] }); + + await rolesPage.submitRoleForm(emptyRole); + + await expect(rolesPage.createSuccessMessage).toHaveCount(0); + }); + + test("selecting a child permission also selects its parent", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + await rolesPage.navigateToCreateRole(); + await rolesPage.permissionTypeSelect.selectOption("custom"); + await rolesPage.selectPermission(ACL.leadsDelete); + + /** + * The tree's default selection type is hierarchical, so a role can never hold + * `leads.delete` without also holding `leads`. Middleware route resolution relies on this. + */ + await expect(rolesPage.permissionCheckbox(ACL.leads)).toBeChecked(); + }); + + test("updates a role", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + await rolesPage.updateRole(role.name, { + description: "Updated by the authorization suite", + permissions: [ACL.leadsEdit], + }); + + await expect(rolesPage.updateSuccessMessage.first()).toBeVisible(); + }); + + test("refuses to delete a role that is assigned to a user", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + /** + * The seeded Administrator role is assigned to the signed-in account, so it is covered by + * both the "in use" guard and the "your own role" guard. + */ + await rolesPage.deleteRole("Administrator"); + + await expect(rolesPage.deleteSuccessMessage).toHaveCount(0); + }); + + test("refuses to edit the role assigned to the current account", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + await rolesPage.openRoleForEdit("Administrator"); + + /** + * The guard redirects back to the listing with a flash rather than opening the form, so a + * user can never widen their own role. + */ + await expect(rolesPage.currentRoleEditError.first()).toBeVisible(); + }); + + test("deletes an unused role", async ({ adminPage }) => { + const rolesPage = new RolesPage(adminPage); + + await rolesPage.deleteRole(role.name); + + await expect(rolesPage.deleteSuccessMessage.first()).toBeVisible(); + }); +}); diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/source.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/source.spec.ts new file mode 100644 index 0000000..9ad26ea --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/source.spec.ts @@ -0,0 +1,31 @@ +import { verify } from "crypto"; +import { expect, test } from "../../fixtures/AdminFixtures"; +import { SourcesPage } from "../../pages/settings/SourcesPage"; +import { generateFirstName } from "../../utils/faker"; + +test.describe("source managment",async()=>{ + const sourceName=generateFirstName(); + const updateSourceName=sourceName+"udated"; + test('verify create source',async({adminPage})=>{ + const source=new SourcesPage(adminPage); + await source.navigateToSources(); + await source.createSourceLink.click(); + await source.sourceForm(sourceName); + }) + test('verify update source',async({adminPage})=>{ + const source = new SourcesPage(adminPage); + await source.navigateToSources(); + await source.searchSource(sourceName); + await source.firstEditIcon.click(); + await source.sourceForm(updateSourceName); + }) + test("verify delete source",async ({adminPage})=>{ + const source= new SourcesPage(adminPage); + await source.navigateToSources(); + await source.searchSource(updateSourceName); + await source.deleteSource(); + + }) + + +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/tag.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/tag.spec.ts new file mode 100644 index 0000000..0f4a7b2 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/tag.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from "../../fixtures/AdminFixtures"; +import { TagsPage } from "../../pages/settings/TagsPage"; + +test.describe("tag management",async()=>{ + test("verify create tag",async({adminPage})=>{ + const tagsPage=new TagsPage(adminPage); + await tagsPage.navigateToTags(); + const tagName="Tag "+Math.floor(Math.random()*1000); + await tagsPage.createTag(tagName); + await tagsPage.seacrhTag(tagName); + await expect(adminPage.getByText(tagName).first()).toBeVisible(); + }) + test("verify update tag",async({adminPage})=>{ + const tagsPage=new TagsPage(adminPage); + await tagsPage.navigateToTags(); + const tagName="Tag "+Math.floor(Math.random()*1000); + await tagsPage.createTag(tagName); + const updatedTagName=tagName+" Updated"; + await tagsPage.udpateTag(updatedTagName); + await tagsPage.seacrhTag(updatedTagName); + await expect(adminPage.getByText(updatedTagName).first()).toBeVisible(); + }) + test("verify delete tag",async({adminPage})=>{ + const tagsPage=new TagsPage(adminPage); + await tagsPage.navigateToTags(); + const tagName="Tag "+Math.floor(Math.random()*1000); + await tagsPage.createTag(tagName); + await tagsPage.seacrhTag(tagName); + await expect(adminPage.getByText(tagName).first()).toBeVisible(); + await tagsPage.deleteTag(); + await tagsPage.seacrhTag(tagName); + await expect(adminPage.getByText(tagName).first()).toBeHidden(); + }) +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/warehouse.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/warehouse.spec.ts new file mode 100644 index 0000000..5b316a5 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/warehouse.spec.ts @@ -0,0 +1,37 @@ +import { test } from "../../fixtures/AdminFixtures"; +import { AdminPage } from "../../pages/AdminPage"; +import { WarehouseData, warehouseData, WarehousePage } from "../../pages/settings/WarehousesPage"; +import { generateDescription, generateEmail, generateLastName, generatePhoneNumber } from "../../utils/faker"; + +test.describe('warehouse management', async () => { + const updateWareshousedata:WarehouseData={ + name: generateLastName(), + description: generateDescription().slice(0,20), + contactName: generateLastName(), + contactEmail:generateEmail(), + contactAddress: 'Plot 123, Industrial Area, Meerut, UP 250001', + country: 'India', + state: 'Uttar Pradesh', + city: 'Meerut', + zipcode: '250001', + phone: generatePhoneNumber(), + } + test('create warehouse', async ({ adminPage }) => { + const warehouse = new WarehousePage(adminPage); + await warehouse.navigateToWarehouses(); + await warehouse.warehouseCreateLink.click(); + await warehouse.warehouseForm(warehouseData); + + }) + test('verify update warehouse',async({adminPage})=>{ + const warehouse= new WarehousePage(adminPage); + await warehouse.navigateToWarehouses(); + await warehouse.firstEditButton.click(); + await warehouse.warehouseForm(updateWareshousedata); + }) + test('verify delete warehouse',async({adminPage})=>{ + const warehouse= new WarehousePage(adminPage); + await warehouse.navigateToWarehouses(); + await warehouse.deleteWarehouse(); + }) +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/tests/settings/webhook.spec.ts b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/webhook.spec.ts new file mode 100644 index 0000000..4afcf57 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/tests/settings/webhook.spec.ts @@ -0,0 +1,23 @@ +import { test } from "../../fixtures/AdminFixtures"; +import { webhookData, WebhookPage } from "../../pages/settings/WebhooksPage"; + +test.describe("webhook management", () => { + test("verify create a webhook", async ({ adminPage }) => { + const webhookPage = new WebhookPage(adminPage); + await webhookPage.navigateToWebhooks(); + await webhookPage.createWebhook(webhookData); + }); + test("verify update a webhook", async ({ adminPage }) => { + const webhookPage = new WebhookPage(adminPage); + await webhookPage.navigateToWebhooks(); + await webhookPage.searchWebhook(webhookData.name); + await webhookPage.updateFirstWebhook(webhookData); + }); + + test("verify delete a webhook", async ({ adminPage }) => { + const webhookPage = new WebhookPage(adminPage); + await webhookPage.navigateToWebhooks(); + await webhookPage.searchWebhook(webhookData.name); + await webhookPage.deleteWebhook(); + }); +}) \ No newline at end of file diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/acl.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/acl.ts new file mode 100644 index 0000000..6f4fa0c --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/acl.ts @@ -0,0 +1,276 @@ +import { Browser, BrowserContext, expect, Page, APIResponse } from "@playwright/test"; + +import { AdminPage } from "../pages/AdminPage"; + +/** + * Shared vocabulary and helpers for the authorization suites. + * + * Krayin authorization has two orthogonal dimensions and the helpers below are split along the + * same seam: + * + * - ACL -> which actions a role may perform (roles.permission_type + roles.permissions) + * - View scope -> whose records those actions touch (users.view_permission) + * + * Everything here works against a signed-in browser context so a probe carries the same session + * cookies the UI would, which is the only way a direct-request test proves anything about the + * middleware. + */ + +export const ADMIN_EMAIL = "admin@example.com"; +export const ADMIN_PASSWORD = "admin123"; + +/** + * Password handed to every user these suites create. Kept above Krayin's six character minimum. + */ +export const TEST_PASSWORD = "password123"; + +export type ViewPermission = "global" | "group" | "individual"; + +export type PermissionType = "all" | "custom"; + +/** + * ACL keys used across the suites, named rather than repeated as string literals so a rename in + * `Admin/Config/acl.php` breaks in one place. + */ +export const ACL = { + dashboard: "dashboard", + + leads: "leads", + leadsCreate: "leads.create", + leadsView: "leads.view", + leadsEdit: "leads.edit", + leadsDelete: "leads.delete", + + quotes: "quotes", + quotesCreate: "quotes.create", + + contacts: "contacts", + contactsPersons: "contacts.persons", + contactsPersonsCreate: "contacts.persons.create", + contactsPersonsView: "contacts.persons.view", + contactsPersonsQuickCreate: "contacts.persons.create.quick-create", + contactsPersonsDelete: "contacts.persons.delete", + + settings: "settings", + settingsGroups: "settings.user.groups", + settingsRoles: "settings.user.roles", + settingsRolesCreate: "settings.user.roles.create", + settingsRolesEdit: "settings.user.roles.edit", + settingsRolesDelete: "settings.user.roles.delete", + settingsUsers: "settings.user.users", + settingsUsersCreate: "settings.user.users.create", + settingsUsersEdit: "settings.user.users.edit", + settingsUsersDelete: "settings.user.users.delete", +} as const; + +/** + * Admin routes exercised by the direct-request probes, paired with the ACL key that must guard + * them. Paths are relative so they resolve against the configured `baseURL`. + */ +export const GUARDED_ROUTES: Array<{ path: string; permission: string; label: string }> = [ + { path: "admin/leads", permission: ACL.leads, label: "lead listing" }, + { path: "admin/leads/create", permission: ACL.leadsCreate, label: "lead create form" }, + { path: "admin/quotes", permission: ACL.quotes, label: "quote listing" }, + { path: "admin/quotes/create", permission: ACL.quotesCreate, label: "quote create form" }, + { path: "admin/contacts/persons", permission: ACL.contactsPersons, label: "person listing" }, + { path: "admin/settings/roles", permission: ACL.settingsRoles, label: "role listing" }, + { path: "admin/settings/roles/create", permission: ACL.settingsRolesCreate, label: "role create form" }, + { path: "admin/settings/users", permission: ACL.settingsUsers, label: "user listing" }, + { path: "admin/settings/groups", permission: ACL.settingsGroups, label: "group listing" }, +]; + +/** + * Routes every authenticated user may reach regardless of role, mirroring the middleware's + * allow-list. A regression that widens the allow-list shows up as a route appearing here that + * should not. + */ +export const ALLOWLISTED_ROUTES = [ + "admin/account", +]; + +export type Session = { + context: BrowserContext; + page: Page; + email: string; +}; + +/** + * Sign in as an arbitrary user in a brand new browser context. + * + * A separate context per user is deliberate: the admin fixture's storage state must not leak into + * a test that is meant to act as a restricted user, and reusing one context would let an earlier + * session's cookies answer for a later assertion. + */ +export async function signInAs( + browser: Browser, + baseURL: string | undefined, + email: string, + password: string +): Promise { + const context = await browser.newContext({ baseURL }); + const page = await context.newPage(); + + await new AdminPage(page).adminLogin(email, password); + + await page.waitForLoadState("networkidle"); + + return { context, page, email }; +} + +/** + * Sign in and assert the attempt actually authenticated, so a suite never proceeds to + * authorization assertions while silently signed out — an unauthenticated request is redirected to + * the login page and would pass a naive "not 200" check for the wrong reason. + * + * The landing URL is deliberately not pinned to the dashboard: a user whose role lacks the + * `dashboard` permission is dropped on the first screen they can reach (`/admin/leads`, say), so + * leaving the login page is what actually signals success. + */ +export async function signInExpectingSuccess( + browser: Browser, + baseURL: string | undefined, + email: string, + password: string +): Promise { + const session = await signInAs(browser, baseURL, email, password); + + await expect(session.page, `${email} should be signed in`).not.toHaveURL(/\/admin\/login/); + + return session; +} + +/** + * Laravel accepts the `XSRF-TOKEN` cookie echoed back as an `X-XSRF-TOKEN` header, which is how + * the admin panel's own axios calls pass CSRF verification. Reusing that keeps a POST probe + * failing on authorization rather than on a missing token. + */ +export async function csrfHeaders(context: BrowserContext): Promise> { + const cookies = await context.cookies(); + + const token = cookies.find((cookie) => cookie.name === "XSRF-TOKEN"); + + return { + "X-XSRF-TOKEN": token ? decodeURIComponent(token.value) : "", + "X-Requested-With": "XMLHttpRequest", + }; +} + +/** + * Issue a GET against an admin route using the session's cookies, without following redirects, so + * the raw status is what gets asserted. + */ +export async function getRoute(session: Session, path: string): Promise { + return session.page.request.get(path, { + failOnStatusCode: false, + maxRedirects: 0, + }); +} + +/** + * Issue a POST against an admin route with CSRF headers attached. + */ +export async function postRoute( + session: Session, + path: string, + form: Record = {} +): Promise { + return session.page.request.post(path, { + failOnStatusCode: false, + maxRedirects: 0, + headers: await csrfHeaders(session.context), + form, + }); +} + +/** + * Assert a response was refused by authorization. + * + * 401 is what `Bouncer::allow()` aborts with. A redirect to the login page (302) is accepted only + * as a session-level refusal; a 200 always fails, because rendering the page means the guard did + * not run. + */ +export function expectDenied(response: APIResponse, label: string): void { + expect( + response.status(), + `${label} must be refused for a user without the permission (got ${response.status()})` + ).not.toBe(200); + + expect( + [401, 403, 302, 419].includes(response.status()), + `${label} should be refused with 401/403 or a redirect, got ${response.status()}` + ).toBeTruthy(); +} + +/** + * Assert a response was served. + */ +export function expectAllowed(response: APIResponse, label: string): void { + expect(response.status(), `${label} should be reachable`).toBe(200); +} + +/** + * Close every session opened by a test, tolerating one that failed to open. + */ +export async function closeSessions(...sessions: Array): Promise { + for (const session of sessions) { + await session?.context.close(); + } +} + +/** + * A run-scoped suffix, so records created by one run never collide with a previous one and a + * failed run leaves behind identifiable rows. + */ +export function runSuffix(): string { + return `${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 1000)}`; +} + +/** + * Resolve a record id out of a datagrid's own JSON response. + * + * The person listing renders no per-row links, so there is no href to scrape for an id. The grid + * populates itself from this endpoint, so asking it directly is both more stable than parsing the + * DOM and closer to what the page actually does. + * + * Returns `null` when the record is not present for this session — which for a scoped user is a + * meaningful answer, not a failure. + */ +export async function findRecordId( + session: Session, + path: string, + column: string, + value: string +): Promise { + const response = await session.page.request.get( + `${path}?pagination[page]=1&pagination[per_page]=200`, + { + headers: { "X-Requested-With": "XMLHttpRequest", Accept: "application/json" }, + failOnStatusCode: false, + } + ); + + if (response.status() !== 200) { + return null; + } + + const body = await response.json(); + + /** + * A grid column is not always a scalar: the users grid renders its name cell from an object + * (`{ image, name }`), which the Blade reads as `record.name.name`. Unwrap that shape before + * comparing, or every user lookup silently misses. + */ + const cellText = (cell: unknown): string => { + if (cell && typeof cell === "object" && "name" in (cell as Record)) { + return String((cell as Record).name ?? "").trim(); + } + + return String(cell ?? "").trim(); + }; + + const match = (body.records ?? []).find( + (record: Record) => cellText(record[column]) === value + ); + + return match ? Number(match.id) : null; +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/address.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/address.ts new file mode 100644 index 0000000..7df8fdc --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/address.ts @@ -0,0 +1,58 @@ +import * as forms from './form'; +import { generateRandomNumericString } from './faker'; + +const address = async (page) => { + await page.fill('input[name="billing.company_name"]', forms.form.lastName); + await page.fill('input[name="billing.first_name"]', forms.form.firstName); + await page.fill('input[name="billing.last_name"]', forms.form.lastName); + await page.fill('input[name="billing.email"]', forms.form.email); + await page.fill('input[name="billing.address.[0]"]', forms.form.firstName); + await page.selectOption('select[name="billing.country"]', 'IN'); + await page.selectOption('select[name="billing.state"]', 'UP'); + await page.fill('input[name="billing.city"]', forms.form.lastName); + await page.fill('input[name="billing.postcode"]', generateRandomNumericString(6)); + await page.fill('input[name="billing.phone"]', forms.form.phone); + + const exists = await page.$$('input[name="billing.save_address"]'); + + if (exists.length != 0) { + await page.click('input[name="billing.save_address"] + label'); + await page.press('input[name="billing.phone"]', 'Enter'); + } else { + const checkbox = await page.$$('input[name="billing.use_for_shipping"]'); + + if (Math.floor(Math.random() * 20) % 3 == 1 ? false : true) { + if (! checkbox[0].isChecked()) { + await page.click('input[name="billing.use_for_shipping"] + label'); + } + } else { + if (checkbox[0].isChecked()) { + await page.click('input[name="billing.use_for_shipping"] + label'); + } + + await page.fill('input[name="shipping.company_name"]', forms.form.lastName); + await page.fill('input[name="shipping.first_name"]', forms.form.firstName); + await page.fill('input[name="shipping.last_name"]', forms.form.lastName); + await page.fill('input[name="shipping.email"]', forms.form.email); + await page.fill('input[name="shipping.address.[0]"]', forms.form.firstName); + await page.selectOption('select[name="shipping.country"]', 'IN'); + await page.selectOption('select[name="shipping.state"]', 'UP'); + await page.fill('input[name="shipping.city"]', forms.form.lastName); + await page.fill('input[name="shipping.postcode"]', '201301'); + await page.fill('input[name="shipping.phone"]', forms.form.phone); + } + + const nextButton = await page.$$('button.primary-button:visible'); + await nextButton[0].click(); + } + + const output = await forms.testForm(page) + + if (output == null) { + return address(page); + } else { + return 'done'; + } +} + +export default address; diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/customer.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/customer.ts new file mode 100644 index 0000000..930cf7d --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/customer.ts @@ -0,0 +1,159 @@ +import { expect } from "../setup"; +import { + generateName, + generateFirstName, + generateLastName, + generateEmail, + generateDescription, +} from "./faker"; + +export async function register(page) { + const credentials = { + firstName: generateFirstName(), + lastName: generateLastName(), + email: generateEmail(), + password: "admin123", + }; + await page.getByPlaceholder("First Name").fill(credentials.firstName); + await page.getByPlaceholder("Last Name").fill(credentials.lastName); + await page.getByPlaceholder("email@example.com").fill(credentials.email); + await page + .getByPlaceholder("Password", { exact: true }) + .fill(credentials.password); + await page.getByPlaceholder("Confirm Password").fill(credentials.password); + + + const isAgreementVisible = await page.locator("#agreement").nth(1).isVisible(); + + if (isAgreementVisible) { + await page.getByText("I agree with this statement.").click(); + } + + const isNewsletterVisible = await page + .locator("#main form div") + .filter({ hasText: "Subscribe to newsletter" }) + .locator("label") + .first() + .isVisible(); + + if (isNewsletterVisible) { + await page + .locator("#main form div") + .filter({ hasText: "Subscribe to newsletter" }) + .locator("label") + .first() + .click(); + } + + await page.getByRole("button", { name: "Register" }).click(); + + await expect( + page + .getByText( + "Account created successfully, an e-mail has been sent for verification." + ) + .first() + ).toBeVisible(); + + return credentials; +} + +export async function loginAsCustomer(page) { + const credentials = await register(page); + + await page.goto("customer/login"); + await page.getByPlaceholder("email@example.com").click(); + await page.getByPlaceholder("email@example.com").fill(credentials.email); + await page.getByPlaceholder("email@example.com").press("Tab"); + await page.getByPlaceholder("Password").fill(credentials.password); + await page.getByRole("button", { name: "Sign In" }).click(); + + return credentials; +} + +export async function addAddress(page) { + await page.getByLabel("Profile").click(); + await page.getByRole("link", { name: "Profile" }).click(); + await page.getByRole("link", { name: " Address " }).click(); + await page.getByRole("link", { name: "Add Address" }).click(); + await page.getByPlaceholder("Company Name").click(); + await page.getByPlaceholder("Company Name").fill(generateName()); + await page.getByPlaceholder("Company Name").press("Tab"); + await page.getByPlaceholder("First Name").fill(generateFirstName()); + await page.getByPlaceholder("First Name").press("Tab"); + await page.getByPlaceholder("Last Name").fill(generateLastName()); + await page.getByPlaceholder("Last Name").press("Tab"); + await page + .getByPlaceholder("Email", { exact: true }) + .fill("test@example.com"); + await page.getByPlaceholder("Email", { exact: true }).press("Tab"); + await page.getByPlaceholder("Vat ID").press("Tab"); + await page.getByPlaceholder("Street Address").fill("Demo"); + await page.getByPlaceholder("Street Address").press("Tab"); + await page.getByLabel("Country").selectOption("DZ"); + await page.getByPlaceholder("State").click(); + await page.getByPlaceholder("State").fill("any"); + await page.getByPlaceholder("City").click(); + await page.getByPlaceholder("City").fill("any"); + await page.getByPlaceholder("Post Code").click(); + await page.getByPlaceholder("Post Code").fill("123456"); + await page.getByPlaceholder("Phone").click(); + await page.getByPlaceholder("Phone").fill("9876543210"); + await page + .locator("#main form div") + .filter({ hasText: "Set as Default" }) + .locator("label") + .first() + .click(); + await page + .locator("#main form div") + .filter({ hasText: "Set as Default" }) + .locator("label") + .first() + .click(); + await page.getByRole("button", { name: "Save" }).click(); + + await expect( + page.getByText("Address have been successfully added.").first() + ).toBeVisible(); +} + +export async function addWishlist(page) { + await page.locator(".action-items > span").first().click(); + await page + .locator( + "div:nth-child(9) > div:nth-child(2) > div > .-mt-9 > .action-items > span" + ) + .first() + .click(); + + await expect( + page.getByText("Item Successfully Added To Wishlist").first() + ).toBeVisible(); +} + +export async function addReview(page) { + const review = { + title: generateName(), + comment: generateDescription(), + }; + + await page + .locator("#main div") + .filter({ hasText: "New Products View All New" }) + .getByLabel("Arctic Touchscreen Winter") + .click(); + await page.getByRole("button", { name: "Reviews" }).click(); + await page.locator("#review-tab").getByText("Write a Review").click(); + await page.locator("#review-tab span").nth(3).click(); + await page.locator("#review-tab span").nth(4).click(); + await page.getByPlaceholder("Title").fill(review.title); + await page.getByPlaceholder("Comment").fill(review.comment); + await page.getByRole("button", { name: "Submit Review" }).click(); + + await expect( + page.getByText("Review submitted successfully.").first() + ).toBeVisible(); + + return review; +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/faker.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/faker.ts new file mode 100644 index 0000000..dc86f41 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/faker.ts @@ -0,0 +1,856 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const usedNames = new Set(); +const usedEmails = new Set(); +const usedNumbers = new Set(); +const usedSlugs = new Set(); +const usedCurrencies = new Set(); +const usedLocales = new Set(); + + + +export function generateName() { + const adjectives = [ + "Cool", + "Smart", + "Fast", + "Sleek", + "Innovative", + "Shiny", + "Bold", + "Elegant", + "Epic", + "Mystic", + "Brilliant", + "Luminous", + ]; + + const nouns = [ + "Star", + "Vision", + "Echo", + "Spark", + "Horizon", + "Nova", + "Shadow", + "Wave", + "Pulse", + "Vortex", + "Zenith", + "Element", + ]; + + let name = ""; + + do { + const adj = adjectives[Math.floor(Math.random() * adjectives.length)]; + const noun = nouns[Math.floor(Math.random() * nouns.length)]; + + name = `${adj} ${noun}`; + } while (usedNames.has(name)); + + usedNames.add(name); + + return name; +} + +export function generateFirstName() { + const firstNames = [ + "James", + "Emma", + "Liam", + "Olivia", + "Noah", + "Ava", + "William", + "Sophia", + "Benjamin", + "Isabella", + "Lucas", + "Mia", + ]; + + return firstNames[Math.floor(Math.random() * firstNames.length)]; +} + +export function generateLastName() { + const lastNames = [ + "Smith", + "Johnson", + "Brown", + "Williams", + "Jones", + "Garcia", + "Miller", + "Davis", + "Rodriguez", + "Martinez", + "Hernandez", + "Lopez", + ]; + + return lastNames[Math.floor(Math.random() * lastNames.length)]; +} + +export function generateFullName() { + return `${generateFirstName()} ${generateLastName()}`; +} + +export function generateEmail() { + const adjectives = [ + "Cool", + "Smart", + "Fast", + "Sleek", + "Innovative", + "Shiny", + "Bold", + "Elegant", + "Epic", + "Mystic", + "Brilliant", + "Luminous", + ]; + + const nouns = [ + "Star", + "Vision", + "Echo", + "Spark", + "Horizon", + "Nova", + "Shadow", + "Wave", + "Pulse", + "Vortex", + "Zenith", + "Element", + ]; + + let email = ""; + + do { + const adj = adjectives[Math.floor(Math.random() * adjectives.length)]; + const noun = nouns[Math.floor(Math.random() * nouns.length)]; + const number = Math.floor(1000 + Math.random() * 9000); + + email = `${adj}${noun}${number}@example.com`.toLowerCase(); + } while (usedEmails.has(email)); + + usedEmails.add(email); + + return email; +} + +export function generatePhoneNumber() { + let phoneNumber; + + do { + phoneNumber = Math.floor(6000000000 + Math.random() * 4000000000); + } while (usedNumbers.has(phoneNumber)); + + usedNumbers.add(phoneNumber); + + return `${phoneNumber}`; +} + +export function generateSKU() { + const letters = Array.from({ length: 3 }, () => + String.fromCharCode(65 + Math.floor(Math.random() * 26)) + ).join(""); + + const numbers = Math.floor(1000 + Math.random() * 9000); + + return `${letters}${numbers}`; +} + +export function generateSlug(delimiter = "-") { + let slug; + + do { + const name = generateName(); + + const randomStr = Math.random().toString(36).substring(2, 8); + + slug = `${name + .toLowerCase() + .replace(/\s+/g, delimiter)}${delimiter}${randomStr}`; + } while (usedSlugs.has(slug)); + + usedSlugs.add(slug); + + return slug; +} + +export function generateDescription(length = 255) { + const phrases = [ + "An innovative and sleek design.", + "Built for speed and efficiency.", + "Experience the future today.", + "A perfect blend of style and power.", + "Engineered to perfection.", + "Designed for those who dream big.", + "Unleash creativity with this masterpiece.", + "A game-changer in every way.", + "Smart, fast, and reliable.", + "The perfect companion for your journey.", + "Crafted with precision and excellence.", + "Innovation that redefines possibilities.", + "Enhancing your experience like never before.", + "Where technology meets elegance.", + "Power, performance, and perfection combined.", + "Redefining the way you experience the world.", + "A masterpiece of engineering and design.", + "Unmatched quality and exceptional performance.", + "Designed to elevate your lifestyle.", + "Beyond expectations, beyond limits.", + ]; + + let description = ""; + + while (description.length < length) { + let phrase = phrases[Math.floor(Math.random() * phrases.length)]; + + if (description.length + phrase.length <= length) { + description += (description ? " " : "") + phrase; + } else { + description += + " " + phrase.substring(0, length - description.length); + break; + } + } + + return description.trim(); +} + +/** + * Generates a random numeric string with specified length or within a numeric range. + * + * @param {number} [length=10] - The length of the random string (used only when min and max are undefined) + * @param {number|null|undefined} [min=undefined] - Minimum value (inclusive) when generating a number in range + * @param {number|null|undefined} [max=undefined] - Maximum value (inclusive) when generating a number in range + * @returns {string} - Random numeric string + */ +export function generateRandomNumericString(length: number = 10, min?: number | null, max?: number | null): string { + // Generate a number within range + if (min !== null && min !== undefined && max !== null && max !== undefined) { + // Input validation + if (!Number.isInteger(min) || !Number.isInteger(max)) { + throw new Error("Min and max must be integers when provided."); + } + + if (min > max) { + throw new Error("Min value cannot be greater than max value."); + } + + // Generate a random number within the range and convert to string + return Math.floor(Math.random() * (max - min + 1) + min).toString(); + } + + // Generate a random string of specified length + if (!Number.isInteger(length) || length <= 0) { + throw new Error("Length must be a positive integer."); + } + + // More efficient method for generating random digits + let result = ''; + for (let i = 0; i < length; i++) { + result += Math.floor(Math.random() * 10); + } + + // Ensure first character is not zero for a consistent length string + if (length > 1 && result[0] === '0') { + result = String(1 + Math.floor(Math.random() * 9)) + result.substring(1); + } + + return result; +} + +export function generateHostname() { + const words = [ + "tech", + "cloud", + "byte", + "stream", + "nexus", + "core", + "pulse", + "data", + "sync", + "wave", + "hub", + "zone", + ]; + + const domains = [".com", ".net", ".io", ".ai", ".xyz", ".co"]; + + const part1 = words[Math.floor(Math.random() * words.length)]; + const part2 = words[Math.floor(Math.random() * words.length)]; + const domain = domains[Math.floor(Math.random() * domains.length)]; + + return `https://${part1}${part2}${domain}`; +} + +export function generateCurrency() { + const currencies = [ + { + name: "US Dollar", + code: "USD", + symbol: "$", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Euro", + code: "EUR", + symbol: "€", + decimalDigits: "2", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "British Pound", + code: "GBP", + symbol: "£", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Japanese Yen", + code: "JPY", + symbol: "¥", + decimalDigits: "0", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Australian Dollar", + code: "AUD", + symbol: "A$", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Canadian Dollar", + code: "CAD", + symbol: "C$", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Swiss Franc", + code: "CHF", + symbol: "CHF", + decimalDigits: "2", + groupSeparator: "'", + decimalSeparator: ".", + }, + { + name: "Chinese Yuan", + code: "CNY", + symbol: "¥", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Indian Rupee", + code: "INR", + symbol: "₹", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "South Korean Won", + code: "KRW", + symbol: "₩", + decimalDigits: "0", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Mexican Peso", + code: "MXN", + symbol: "MX$", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Russian Ruble", + code: "RUB", + symbol: "₽", + decimalDigits: "2", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "Brazilian Real", + code: "BRL", + symbol: "R$", + decimalDigits: "2", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "South African Rand", + code: "ZAR", + symbol: "R", + decimalDigits: "2", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "New Zealand Dollar", + code: "NZD", + symbol: "NZ$", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Singapore Dollar", + code: "SGD", + symbol: "S$", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Hong Kong Dollar", + code: "HKD", + symbol: "HK$", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Norwegian Krone", + code: "NOK", + symbol: "kr", + decimalDigits: "2", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "Swedish Krona", + code: "SEK", + symbol: "kr", + decimalDigits: "2", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "Danish Krone", + code: "DKK", + symbol: "kr", + decimalDigits: "2", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Polish Złoty", + code: "PLN", + symbol: "zł", + decimalDigits: "2", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "Turkish Lira", + code: "TRY", + symbol: "₺", + decimalDigits: "2", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Thai Baht", + code: "THB", + symbol: "฿", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Indonesian Rupiah", + code: "IDR", + symbol: "Rp", + decimalDigits: "0", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Malaysian Ringgit", + code: "MYR", + symbol: "RM", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Philippine Peso", + code: "PHP", + symbol: "₱", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Israeli Shekel", + code: "ILS", + symbol: "₪", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Saudi Riyal", + code: "SAR", + symbol: "﷼", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "UAE Dirham", + code: "AED", + symbol: "د.إ", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Czech Koruna", + code: "CZK", + symbol: "Kč", + decimalDigits: "2", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "Hungarian Forint", + code: "HUF", + symbol: "Ft", + decimalDigits: "0", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "Romanian Leu", + code: "RON", + symbol: "lei", + decimalDigits: "2", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Bulgarian Lev", + code: "BGN", + symbol: "лв", + decimalDigits: "2", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "Croatian Kuna", + code: "HRK", + symbol: "kn", + decimalDigits: "2", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Icelandic Króna", + code: "ISK", + symbol: "kr", + decimalDigits: "0", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Ukrainian Hryvnia", + code: "UAH", + symbol: "₴", + decimalDigits: "2", + groupSeparator: " ", + decimalSeparator: ",", + }, + { + name: "Pakistani Rupee", + code: "PKR", + symbol: "₨", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Bangladeshi Taka", + code: "BDT", + symbol: "৳", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Sri Lankan Rupee", + code: "LKR", + symbol: "Rs", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Nepalese Rupee", + code: "NPR", + symbol: "₨", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Kuwaiti Dinar", + code: "KWD", + symbol: "د.ك", + decimalDigits: "3", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Qatari Riyal", + code: "QAR", + symbol: "ر.ق", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Omani Rial", + code: "OMR", + symbol: "ر.ع.", + decimalDigits: "3", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Bahraini Dinar", + code: "BHD", + symbol: "ب.د", + decimalDigits: "3", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Argentine Peso", + code: "ARS", + symbol: "$", + decimalDigits: "2", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Chilean Peso", + code: "CLP", + symbol: "$", + decimalDigits: "0", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Colombian Peso", + code: "COP", + symbol: "$", + decimalDigits: "0", + groupSeparator: ".", + decimalSeparator: ",", + }, + { + name: "Peruvian Sol", + code: "PEN", + symbol: "S/.", + decimalDigits: "2", + groupSeparator: ",", + decimalSeparator: ".", + }, + { + name: "Venezuelan Bolívar", + code: "VES", + symbol: "Bs.", + decimalDigits: "2", + groupSeparator: ".", + decimalSeparator: ",", + }, + ]; + + if (usedCurrencies.size >= currencies.length) { + throw new Error("All currencies have been used."); + } + + let currency; + + do { + const randomIndex = Math.floor(Math.random() * currencies.length); + + currency = currencies[randomIndex]; + } while (usedCurrencies.has(currency.code)); + + usedCurrencies.add(currency.code); + + return currency; +} + +export function generateLocale() { + const locales = [ + { + name: "English", + code: "en", + direction: "LTR" + }, + { + name: "French", + code: "fr", + direction: "LTR" + }, + { + name: "Spanish", + code: "es", + direction: "LTR" + }, + { + name: "Arabic", + code: "ar", + direction: "RTL" + }, + { + name: "Hebrew", + code: "he", + direction: "RTL" + }, + { + name: "Japanese", + code: "ja", + direction: "LTR" + }, + { + name: "Chinese", + code: "zh_CN", + direction: "LTR" + }, + { + name: "Hindi", + code: "hi_IN", + direction: "LTR" + }, + { + name: "Bengali", + code: "bn", + direction: "LTR" + }, + { + name: "German", + code: "de", + direction: "LTR" + }, + { + name: "Persian", + code: "fa", + direction: "LTR" + }, + { + name: "Italian", + code: "it", + direction: "LTR" + }, + { + name: "Dutch", + code: "nl", + direction: "LTR" + }, + ]; + + if (usedLocales.size >= locales.length) { + throw new Error("All locales have been used."); + } + + let locale; + + do { + const randomIndex = Math.floor(Math.random() * locales.length); + locale = locales[randomIndex]; + } while (usedLocales.has(locale.code)); + + usedLocales.add(locale.code); + + return locale; +} + +export function randomElement(array) { + return array[Math.floor(Math.random() * array.length)]; +} + +export function getImageFile( + directory = path.resolve(__dirname, "../data/images/") +) { + if (!fs.existsSync(directory)) { + throw new Error(`Directory does not exist: ${directory}`); + } + + const files = fs.readdirSync(directory); + const imageFiles = files.filter((file) => + /\.(gif|jpeg|jpg|png|svg|webp)$/i.test(file) + ); + + if (!imageFiles.length) { + throw new Error("No image files found in the directory."); + } + + const randomIndex = Math.floor(Math.random() * imageFiles.length); + + return path.join(directory, imageFiles[randomIndex]); +} + +export function generateRandomDateTime(end = new Date(new Date().setFullYear(new Date().getFullYear() + 10))) { + const start = new Date(); + const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} + +export function generateLocation() { + const location = [ + "New York", + "Los Angeles", + "Chicago", + "Houston", + "Phoenix", + "Philadelphia", + "San Antonio", + "San Diego", + "Dallas", + "San Jose", + "Austin", + "Jacksonville", + "San Francisco", + "Indianapolis", + "Columbus", + "Fort Worth", + ]; + + return location[Math.floor(Math.random() * location.length)]; +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/form.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/form.ts new file mode 100644 index 0000000..ba18713 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/form.ts @@ -0,0 +1,198 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function getRandomImageFile( + directory = path.resolve(__dirname, "../data/images/") +) { + if (!fs.existsSync(directory)) { + throw new Error(`Directory does not exist: ${directory}`); + } + + const files = fs.readdirSync(directory); + const imageFiles = files.filter((file) => + /\.(gif|jpeg|jpg|png|svg|webp)$/i.test(file) + ); + + if (!imageFiles.length) { + throw new Error("No image files found in the directory."); + } + + const randomIndex = Math.floor(Math.random() * imageFiles.length); + return path.join(directory, imageFiles[randomIndex]); +} + +const testForm = async (page) => { + const getError = await page + .waitForSelector(".text-red-600.text-xs.italic", { timeout: 2000 }) + .catch(() => null); + var message = ""; + + if (getError) { + const errors = await page.$$(".text-red-600.text-xs.italic"); + + for (let error of errors) { + message = await error.evaluate((el) => el.innerText); + } + return null; + } else { + const iconExists = await page + .waitForSelector( + ".flex.items-center.break-all.text-sm > .icon-toast-done.rounded-full.bg-white.text-2xl", + { timeout: 5000 } + ) + .catch(() => null); + + if (iconExists) { + const messages = await page.$$( + ".flex.items-center.break-all.text-sm > .icon-toast-done.rounded-full.bg-white.text-2xl" + ); + const icons = await page.$$( + ".flex.items-center.break-all.text-sm + .cursor-pointer.underline" + ); + + message = await messages[0].evaluate( + (el) => el.parentNode.innerText + ); + await icons[0].click(); + return null; + } else { + return page.url(); + } + } +}; + +const form = { + firstName: `Test${Math.random().toString(36).substring(7)}`, + lastName: `User${Math.random().toString(36).substring(7)}`, + email: `test${Math.random().toString(36).substring(7)}@example.com`, + phone: `555${Math.floor(Math.random() * 10000000) + .toString() + .padStart(7, "0")}`, +}; + +function generateRandomPassword(minLength, maxLength) { + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"; + const length = + Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength; + let password = ""; + + for (let i = 0; i < length; i++) { + password += characters.charAt( + Math.floor(Math.random() * characters.length) + ); + } + + return password; +} + +function generateRandomStringWithSpaces(length) { + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + if (Math.random() < 0.1) { + result += " "; + } else { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters[randomIndex]; + } + } + return result; +} + +function generateRandomProductName() { + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + let result = ""; + for (let i = 1; i < 3; i++) { + if (Math.random() < 0.1) { + result += " "; + } else { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters[randomIndex]; + } + } + return result; +} + +function generateRandomHtmlContent() { + const elements = ["span", "a", "em", "strong", "b", "i", "u"]; + const randomElement = elements[Math.floor(Math.random() * elements.length)]; + const randomText = Math.random().toString(36).substring(7); + const randomHref = `https://example.com/${Math.random() + .toString(36) + .substring(7)}`; + + switch (randomElement) { + case "a": + return `${randomText}`; + case "span": + return `${randomText}`; + case "em": + return `${randomText}`; + case "strong": + return `${randomText}`; + case "b": + return `${randomText}`; + case "i": + return `${randomText}`; + case "u": + return `${randomText}`; + default: + return `${randomText}`; + } +} + +async function fillParagraphWithRandomHtml(multi) { + const randomHtmlContent = new Array(multi) + .fill("") + .map(() => generateRandomHtmlContent()) + .join(" "); + + return randomHtmlContent; +} + +function generateRandomUrl() { + const protocols = ["http", "https"]; + const domain = `example${Math.random().toString(36).substring(7)}.com`; + const path = `/${Math.random().toString(36).substring(7)}`; + const protocol = protocols[Math.floor(Math.random() * protocols.length)]; + return `${protocol}://${domain}${path}`; +} + +function generateRandomDateTimeRange() { + // Generate valid "from" and "to" dates + const fromDate = new Date(2024, 9, 29, 12, 0, 0); // Start: 2024-10-29 12:00:00 + const toDate = new Date(2024, 10, 29, 12, 0, 0); // End: 2024-11-29 12:00:00 + + // Randomize within the range + const randomFrom = new Date( + fromDate.getTime() + + Math.random() * (toDate.getTime() - fromDate.getTime() - 86400000) + ); // Ensure `to` is at least 1 day ahead + const randomTo = new Date( + randomFrom.getTime() + + Math.random() * (toDate.getTime() - randomFrom.getTime()) + ); + + return { + from: randomFrom.toISOString().replace("T", " ").split(".")[0], + to: randomTo.toISOString().replace("T", " ").split(".")[0], + }; +} + +export { + form, + getRandomImageFile, + testForm, + generateRandomPassword, + generateRandomStringWithSpaces, + fillParagraphWithRandomHtml, + generateRandomProductName, + generateRandomUrl, + generateRandomDateTimeRange, +}; diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/gdpr.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/gdpr.ts new file mode 100644 index 0000000..461b64b --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/gdpr.ts @@ -0,0 +1,221 @@ +import { expect } from "../setup"; +import { generateDescription } from "../utils/faker"; + +export async function enableGDPR(adminPage) { + /** + * Navigating to the gdpr configuration page. + */ + await adminPage.goto("admin/configuration/general/gdpr"); + + const isEnabled = adminPage.locator("label > div").first().check(); + + /** + * If not enabled, then we enable it. + */ + if (!isEnabled) { + const gdprsettingToggle = adminPage.locator("label > div").first(); + await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 }); + await adminPage.locator("label > div").first().click(); + } + + /** + * Verifying enable state. + */ + const toggleInput = adminPage.locator("label > div").first(); + await expect(toggleInput).toBeChecked(); + + /** + * Click the 'Save Configuration' button. + */ + await adminPage.getByRole("button", { name: "Save Configuration" }).click(); +} + +export async function disableGDPR(adminPage) { + /** + * Navigating to the gdpr configuration page. + */ + await adminPage.goto("admin/configuration/general/gdpr"); + + const isDisabled = adminPage.locator("label > div").first().uncheck(); + + /** + * If not disabled, then we disable it. + */ + if (!isDisabled) { + const gdprsettingToggle = adminPage.locator("label > div").first(); + await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 }); + await adminPage.locator("label > div").first().click(); + } + + /** + * Verifying disable state. + */ + const toggleInput = adminPage.locator("label > div").first(); + await expect(toggleInput).not.toBeChecked(); + + /** + * Click the 'Save Configuration' button. + */ + await adminPage.getByRole("button", { name: "Save Configuration" }).click(); +} + +export async function enableGDPRAgreement(adminPage) { + const agreement = { + checkboxLabel: "I agree with this statement.", + content: generateDescription(), + }; + + /** + * Navigating to the gdpr configuration page. + */ + await adminPage.goto("admin/configuration/general/gdpr"); + const isEnabled = adminPage + .locator("div:nth-child(4) > div > .mb-4 > .relative > div") + .first() + .check(); + + /** + * If not enabled, then we enable it. + */ + if (!isEnabled) { + const gdprsettingToggle = adminPage + .locator("div:nth-child(4) > div > .mb-4 > .relative > div") + .first(); + await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 }); + await adminPage + .locator("div:nth-child(4) > div > .mb-4 > .relative > div") + .first() + .click(); + } + + /** + * Verifying enable state. + */ + const toggleInput = adminPage + .locator("div:nth-child(4) > div > .mb-4 > .relative > div") + .first(); + await expect(toggleInput).toBeChecked(); + + /** + * Fill the agreement checkbox label. + */ + await adminPage + .getByRole("textbox", { name: "Agreement Checkbox Label" }) + .click(); + await adminPage + .getByRole("textbox", { + name: "Agreement Checkbox Label", + }) + .fill(agreement.checkboxLabel); + await adminPage.click('button[type="submit"].primary-button:visible'); + + /** + * Fill the agreement content. + */ + await adminPage.waitForSelector('#general_gdpr__agreement__agreement_content__ifr', { state: 'visible' }); + await adminPage.frameLocator('#general_gdpr__agreement__agreement_content__ifr').locator('body').clear(); + await adminPage.fillInTinymce( + "#general_gdpr__agreement__agreement_content__ifr", + agreement.content + ); + + /** + * Save the configuration. + */ + await adminPage.click('button[type="submit"].primary-button:visible'); + + return agreement; +} + +export async function disableGDPRAgreement(adminPage) { + /** + * Navigating to the gdpr configuration page. + */ + await adminPage.goto("admin/configuration/general/gdpr"); + + const isDisabled = adminPage + .locator("div:nth-child(4) > div > .mb-4 > .relative > div") + .first() + .uncheck(); + + /** + * If not disabled, then we disable it. + */ + if (!isDisabled) { + const gdprsettingToggle = adminPage + .locator("div:nth-child(4) > div > .mb-4 > .relative > div") + .first(); + await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 }); + await adminPage + .locator("div:nth-child(4) > div > .mb-4 > .relative > div") + .first() + .click(); + } + + /** + * Verifying disable state. + */ + const toggleInput = adminPage + .locator("div:nth-child(4) > div > .mb-4 > .relative > div") + .first(); + await expect(toggleInput).not.toBeChecked(); + + /** + * Click the 'Save Configuration' button. + */ + await adminPage.getByRole("button", { name: "Save Configuration" }).click(); +} + +export async function enableCookiesNotice(adminPage, position = "bottom-left") { + /** + * Navigating to the gdpr configuration page. + */ + await adminPage.goto("admin/configuration/general/gdpr"); + + const isEnabled = adminPage + .locator("div:nth-child(6) > div > .mb-4 > .relative > div") + .check(); + + /** + * If not enabled, then we enable it. + */ + if (!isEnabled) { + const gdprsettingToggle = adminPage.locator( + "div:nth-child(6) > div > .mb-4 > .relative > div" + ); + await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 }); + await adminPage + .locator("div:nth-child(6) > div > .mb-4 > .relative > div") + .click(); + } + + /** + * Verifying enable state. + */ + const toggleInput = adminPage.locator( + "div:nth-child(6) > div > .mb-4 > .relative > div" + ); + await expect(toggleInput).toBeChecked(); + + await adminPage + .locator('select[name="general[gdpr][cookie][position]"]') + .selectOption(position); + + await adminPage + .getByRole("textbox", { + name: "Static Block Identifier", + }) + .fill("cookie block"); + + await adminPage + .getByRole("textbox", { + name: "Description", + }) + .fill("this website uses cookies to ensure you"); + + /** + * Save the configuration. + */ + await adminPage.click('button[type="submit"].primary-button:visible'); +} + diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/lang.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/lang.ts new file mode 100644 index 0000000..c011ea7 --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/lang.ts @@ -0,0 +1,17 @@ +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Absolute path to /lang directory +const LANG_DIR = path.resolve( + __dirname, + '../../../../../src/Resources/lang/' +); + + export const fetchConfig= async () => { + const res = await fetch(LANG_DIR); + const appConfig = await res.json(); + return appConfig; + }; diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/savedata.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/savedata.ts new file mode 100644 index 0000000..a8f14aa --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/savedata.ts @@ -0,0 +1,49 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const dataFile = path.resolve( + __dirname, + "../test-data/runtime.json" +); + +function readJson(): Record { + if (!fs.existsSync(dataFile)) { + return {}; + } + + try { + return JSON.parse(fs.readFileSync(dataFile, "utf-8")); + } catch { + // Corrupt or partial write protection + return {}; + } +} + +function writeJson(data: Record) { + const dir = path.dirname(dataFile); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(dataFile, JSON.stringify(data, null, 2)); +} + +export function saveData(key: string, value: unknown): void { + const data = readJson(); + data[key] = value; + writeJson(data); +} + +export function getData(key: string): T { + const data = readJson(); + + if (!(key in data)) { + throw new Error(`❌ Runtime data key not found: "${key}"`); + } + + return data[key] as T; +} diff --git a/packages/Webkul/Admin/tests/e2e-pw/utils/tinymce.ts b/packages/Webkul/Admin/tests/e2e-pw/utils/tinymce.ts new file mode 100644 index 0000000..1918c9c --- /dev/null +++ b/packages/Webkul/Admin/tests/e2e-pw/utils/tinymce.ts @@ -0,0 +1,29 @@ +import { Page, expect } from "@playwright/test"; + +export class CommonPage { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + /** + * Fill TinyMCE editor inside an iframe + * @param iframeSelector - iframe CSS or XPath selector + * @param content - text content to insert into TinyMCE + */ + async fillInTinymce(iframeSelector: string, content: string) { + await this.page.waitForSelector(iframeSelector); + const iframe = this.page.frameLocator(iframeSelector); + const editorBody = iframe.locator("body"); + + await expect(editorBody).toBeVisible(); + + await editorBody.click(); + await editorBody.press("Control+A"); // Select all existing content + await editorBody.press("Backspace"); // Clear existing content + await editorBody.pressSequentially(content); + + await expect(editorBody).toHaveText(content); + } +} \ No newline at end of file diff --git a/packages/Webkul/Admin/vite.config.js b/packages/Webkul/Admin/vite.config.js new file mode 100644 index 0000000..23ca05c --- /dev/null +++ b/packages/Webkul/Admin/vite.config.js @@ -0,0 +1,48 @@ +import { defineConfig, loadEnv } from "vite"; +import vue from "@vitejs/plugin-vue"; +import laravel from "laravel-vite-plugin"; +import path from "path"; + +export default defineConfig(({ mode }) => { + const envDir = "../../../"; + + Object.assign(process.env, loadEnv(mode, envDir)); + + return { + build: { + emptyOutDir: true, + }, + + envDir, + + server: { + host: process.env.VITE_HOST || "localhost", + port: process.env.VITE_PORT || 5173, + cors: true, + }, + + plugins: [ + vue(), + + laravel({ + hotFile: "../../../public/admin-vite.hot", + publicDirectory: "../../../public", + buildDirectory: "admin/build", + input: [ + "src/Resources/assets/css/app.css", + "src/Resources/assets/js/app.js", + "src/Resources/assets/js/chart.js", + ], + refresh: true, + }), + ], + + experimental: { + renderBuiltUrl(filename, { hostId, hostType, type }) { + if (hostType === "css") { + return path.basename(filename); + } + }, + }, + }; +}); diff --git a/packages/Webkul/Attribute/composer.json b/packages/Webkul/Attribute/composer.json new file mode 100755 index 0000000..6b4552b --- /dev/null +++ b/packages/Webkul/Attribute/composer.json @@ -0,0 +1,27 @@ +{ + "name": "krayin/laravel-attribute", + "license": "MIT", + "authors": [ + { + "name": "Jitendra Singh", + "email": "jitendra@webkul.com" + } + ], + "require": { + "krayin/laravel-core": "^1.0" + }, + "autoload": { + "psr-4": { + "Webkul\\Attribute\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Webkul\\Attribute\\Providers\\AttributeServiceProvider" + ], + "aliases": {} + } + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/packages/Webkul/Attribute/src/Config/attribute_lookups.php b/packages/Webkul/Attribute/src/Config/attribute_lookups.php new file mode 100644 index 0000000..ca5d8ed --- /dev/null +++ b/packages/Webkul/Attribute/src/Config/attribute_lookups.php @@ -0,0 +1,5 @@ +increments('id'); + $table->string('code'); + $table->string('name'); + $table->string('type'); + $table->string('lookup_type')->nullable(); + $table->string('entity_type'); + $table->integer('sort_order')->nullable(); + $table->string('validation')->nullable(); + $table->boolean('is_required')->default(0); + $table->boolean('is_unique')->default(0); + $table->boolean('quick_add')->default(0); + $table->boolean('is_user_defined')->default(1); + $table->unique(['code', 'entity_type']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('attributes'); + } +}; diff --git a/packages/Webkul/Attribute/src/Database/Migrations/2021_04_02_080837_create_attribute_options_table.php b/packages/Webkul/Attribute/src/Database/Migrations/2021_04_02_080837_create_attribute_options_table.php new file mode 100644 index 0000000..015e07f --- /dev/null +++ b/packages/Webkul/Attribute/src/Database/Migrations/2021_04_02_080837_create_attribute_options_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->string('name')->nullable(); + $table->integer('sort_order')->nullable(); + $table->integer('attribute_id')->unsigned(); + $table->foreign('attribute_id')->references('id')->on('attributes')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('attribute_options'); + } +}; diff --git a/packages/Webkul/Attribute/src/Database/Migrations/2021_04_06_122751_create_attribute_values_table.php b/packages/Webkul/Attribute/src/Database/Migrations/2021_04_06_122751_create_attribute_values_table.php new file mode 100644 index 0000000..79b4f9d --- /dev/null +++ b/packages/Webkul/Attribute/src/Database/Migrations/2021_04_06_122751_create_attribute_values_table.php @@ -0,0 +1,44 @@ +increments('id'); + $table->string('entity_type')->default('leads'); + $table->text('text_value')->nullable(); + $table->boolean('boolean_value')->nullable(); + $table->integer('integer_value')->nullable(); + $table->double('float_value')->nullable(); + $table->datetime('datetime_value')->nullable(); + $table->date('date_value')->nullable(); + $table->json('json_value')->nullable(); + + $table->integer('entity_id')->unsigned(); + $table->integer('attribute_id')->unsigned(); + + $table->foreign('attribute_id')->references('id')->on('attributes')->onDelete('cascade'); + $table->unique(['entity_type', 'entity_id', 'attribute_id'], 'entity_type_attribute_value_index_unique'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('attribute_values'); + } +}; diff --git a/packages/Webkul/Attribute/src/Database/Migrations/2025_07_02_191710_alter_attribute_values_table.php b/packages/Webkul/Attribute/src/Database/Migrations/2025_07_02_191710_alter_attribute_values_table.php new file mode 100644 index 0000000..b0792b9 --- /dev/null +++ b/packages/Webkul/Attribute/src/Database/Migrations/2025_07_02_191710_alter_attribute_values_table.php @@ -0,0 +1,39 @@ +string('unique_id')->nullable(); + }); + + $tablePrefix = DB::getTablePrefix(); + + DB::statement('UPDATE '.$tablePrefix."attribute_values SET unique_id = CONCAT(entity_id, '|', attribute_id)"); + + Schema::table('attribute_values', function (Blueprint $table) { + $table->unique('unique_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('attribute_values', function (Blueprint $table) { + $table->dropUnique(['unique_id']); + + $table->dropColumn('unique_id'); + }); + } +}; diff --git a/packages/Webkul/Attribute/src/Models/Attribute.php b/packages/Webkul/Attribute/src/Models/Attribute.php new file mode 100644 index 0000000..06c901a --- /dev/null +++ b/packages/Webkul/Attribute/src/Models/Attribute.php @@ -0,0 +1,35 @@ +hasMany(AttributeOptionProxy::modelClass()); + } +} diff --git a/packages/Webkul/Attribute/src/Models/AttributeOption.php b/packages/Webkul/Attribute/src/Models/AttributeOption.php new file mode 100644 index 0000000..2ec0622 --- /dev/null +++ b/packages/Webkul/Attribute/src/Models/AttributeOption.php @@ -0,0 +1,30 @@ +belongsTo(AttributeProxy::modelClass()); + } +} diff --git a/packages/Webkul/Attribute/src/Models/AttributeOptionProxy.php b/packages/Webkul/Attribute/src/Models/AttributeOptionProxy.php new file mode 100644 index 0000000..63de852 --- /dev/null +++ b/packages/Webkul/Attribute/src/Models/AttributeOptionProxy.php @@ -0,0 +1,7 @@ + 'array', + ]; + + /** + * The attributes that are fillable for the model. + * + * @var array + */ + protected $fillable = [ + 'attribute_id', + 'text_value', + 'boolean_value', + 'integer_value', + 'float_value', + 'datetime_value', + 'date_value', + 'json_value', + 'entity_id', + 'entity_type', + ]; + + /** + * The attributes that are used for logging activity. + * + * @var array + */ + public static $attributeTypeFields = [ + 'text' => 'text_value', + 'textarea' => 'text_value', + 'price' => 'float_value', + 'boolean' => 'boolean_value', + 'select' => 'integer_value', + 'multiselect' => 'text_value', + 'checkbox' => 'text_value', + 'email' => 'json_value', + 'address' => 'json_value', + 'phone' => 'json_value', + 'lookup' => 'integer_value', + 'datetime' => 'datetime_value', + 'date' => 'date_value', + 'file' => 'text_value', + 'image' => 'text_value', + ]; + + /** + * Get the attribute that owns the attribute value. + */ + public function attribute() + { + return $this->belongsTo(AttributeProxy::modelClass()); + } + + /** + * Get the parent entity model (leads, products, persons or organizations). + */ + public function entity() + { + return $this->morphTo(); + } +} diff --git a/packages/Webkul/Attribute/src/Models/AttributeValueProxy.php b/packages/Webkul/Attribute/src/Models/AttributeValueProxy.php new file mode 100644 index 0000000..777a768 --- /dev/null +++ b/packages/Webkul/Attribute/src/Models/AttributeValueProxy.php @@ -0,0 +1,7 @@ +loadMigrationsFrom(__DIR__.'/../Database/Migrations'); + } + + /** + * Register services. + * + * @return void + */ + public function register() + { + $this->registerConfig(); + } + + /** + * Register package config. + * + * @return void + */ + protected function registerConfig() + { + $this->mergeConfigFrom( + dirname(__DIR__).'/Config/attribute_lookups.php', 'attribute_lookups' + ); + } +} diff --git a/packages/Webkul/Attribute/src/Providers/ModuleServiceProvider.php b/packages/Webkul/Attribute/src/Providers/ModuleServiceProvider.php new file mode 100644 index 0000000..58946ae --- /dev/null +++ b/packages/Webkul/Attribute/src/Providers/ModuleServiceProvider.php @@ -0,0 +1,24 @@ +, + * 1: class-string, + * 2: class-string + * } + */ + protected $models = [ + Attribute::class, + AttributeOption::class, + AttributeValue::class, + ]; +} diff --git a/packages/Webkul/Attribute/src/Repositories/AttributeOptionRepository.php b/packages/Webkul/Attribute/src/Repositories/AttributeOptionRepository.php new file mode 100755 index 0000000..a1b5086 --- /dev/null +++ b/packages/Webkul/Attribute/src/Repositories/AttributeOptionRepository.php @@ -0,0 +1,18 @@ +model->create($data); + + if (in_array($attribute->type, ['select', 'multiselect', 'checkbox']) && count($options)) { + $sortOrder = 1; + + foreach ($options as $optionInputs) { + $this->attributeOptionRepository->create(array_merge([ + 'attribute_id' => $attribute->id, + 'sort_order' => $sortOrder++, + ], $optionInputs)); + } + } + + return $attribute; + } + + /** + * @param int $id + * @param string $attribute + * @return Attribute + */ + public function update(array $data, $id, $attribute = 'id') + { + $attribute = $this->find($id); + + $attribute->update($data); + + if (! in_array($attribute->type, ['select', 'multiselect', 'checkbox'])) { + return $attribute; + } + + if (! isset($data['options'])) { + return $attribute; + } + + foreach ($data['options'] as $optionId => $optionInputs) { + $isNew = $optionInputs['isNew'] == 'true'; + + if ($isNew) { + $this->attributeOptionRepository->create(array_merge([ + 'attribute_id' => $attribute->id, + ], $optionInputs)); + } else { + $isDelete = $optionInputs['isDelete'] == 'true'; + + if ($isDelete) { + $this->attributeOptionRepository->delete($optionId); + } else { + $this->attributeOptionRepository->update($optionInputs, $optionId); + } + } + } + + return $attribute; + } + + /** + * @param string $code + * @return Attribute + */ + public function getAttributeByCode($code) + { + static $attributes = []; + + if (array_key_exists($code, $attributes)) { + return $attributes[$code]; + } + + return $attributes[$code] = $this->findOneByField('code', $code); + } + + /** + * @param int $lookup + * @param string $query + * @param array $columns + * @return array + */ + public function getLookUpOptions($lookup, $query = '', $columns = []) + { + $lookup = config('attribute_lookups.'.$lookup); + + if (! count($columns)) { + $columns = [($lookup['value_column'] ?? 'id').' as id', ($lookup['label_column'] ?? 'name').' as name']; + } + + if (Str::contains($lookup['repository'], 'UserRepository')) { + $userRepository = app($lookup['repository'])->where('status', 1); + + $currentUser = auth()->guard('user')->user(); + + if ($currentUser?->view_permission === 'group') { + $query = urldecode($query); + + $userIds = bouncer()->getAuthorizedUserIds(); + + return $userRepository + ->when(! empty($userIds), fn ($queryBuilder) => $queryBuilder->whereIn('users.id', $userIds)) + ->when(! empty($query), fn ($queryBuilder) => $queryBuilder->where('users.name', 'like', "%{$query}%")) + ->get(); + } elseif ($currentUser?->view_permission === 'individual') { + return $userRepository->where('users.id', $currentUser->id)->get(); + } + + return $userRepository->where('users.name', 'like', '%'.urldecode($query).'%')->get(); + } + + return app($lookup['repository'])->findWhere([ + [$lookup['label_column'] ?? 'name', 'like', '%'.urldecode($query).'%'], + ], $columns); + } + + /** + * @param string $lookup + * @param int|array $entityId + * @param array $columns + * @return mixed + */ + public function getLookUpEntity($lookup, $entityId = null, $columns = []) + { + if (! $entityId) { + return; + } + + $lookup = config('attribute_lookups.'.$lookup); + + if (! count($columns)) { + $columns = [($lookup['value_column'] ?? 'id').' as id', ($lookup['label_column'] ?? 'name').' as name']; + } + + if (is_array($entityId)) { + return app($lookup['repository'])->findWhereIn( + 'id', + $entityId, + $columns + ); + } else { + return app($lookup['repository'])->find($entityId, $columns); + } + } + + /** + * Returns the attributes which can be added to a web form. + * + * The pipeline and the stage of a lead are decided by the web form itself, so they are never + * offered to the visitor and are excluded from the builder. + * + * @param array $excludedAttributeIds + * @return Collection + */ + public function getWebFormAttributes($excludedAttributeIds = []) + { + return $this->model + ->whereIn('entity_type', ['persons', 'leads']) + ->whereNotIn('id', $excludedAttributeIds) + ->whereNotIn('code', $this->restrictedWebFormAttributeCodes) + ->get(); + } + + /** + * Returns the ids of the attributes which are never exposed on a web form. + * + * @return array + */ + public function getRestrictedWebFormAttributeIds() + { + return $this->model + ->whereIn('code', $this->restrictedWebFormAttributeCodes) + ->pluck('id') + ->toArray(); + } +} diff --git a/packages/Webkul/Attribute/src/Repositories/AttributeValueRepository.php b/packages/Webkul/Attribute/src/Repositories/AttributeValueRepository.php new file mode 100755 index 0000000..38712c0 --- /dev/null +++ b/packages/Webkul/Attribute/src/Repositories/AttributeValueRepository.php @@ -0,0 +1,268 @@ + $data['entity_type']]; + + if (isset($data['quick_add'])) { + $conditions['quick_add'] = 1; + } + + $attributes = $this->attributeRepository->where($conditions)->get(); + } + + foreach ($attributes as $attribute) { + $typeColumn = $this->model::$attributeTypeFields[$attribute->type]; + + if ($attribute->type === 'boolean') { + $data[$attribute->code] = isset($data[$attribute->code]) && $data[$attribute->code] ? 1 : 0; + } + + if (! array_key_exists($attribute->code, $data)) { + continue; + } + + if ($attribute->type === 'price' + && isset($data[$attribute->code]) + && $data[$attribute->code] === '' + ) { + $data[$attribute->code] = null; + } + + if ($attribute->type === 'date' && $data[$attribute->code] === '') { + $data[$attribute->code] = null; + } + + if ($attribute->type === 'multiselect' || $attribute->type === 'checkbox') { + $data[$attribute->code] = implode(',', $data[$attribute->code]); + } + + if ($attribute->type === 'email' || $attribute->type === 'phone') { + $data[$attribute->code] = $this->sanitizeEmailAndPhone($data[$attribute->code]); + } + + if ($attribute->type === 'image' || $attribute->type === 'file') { + $data[$attribute->code] = $data[$attribute->code] instanceof UploadedFile + ? $data[$attribute->code]->store($data['entity_type'].'/'.$data['entity_id']) + : null; + } + + $attributeValue = $this->findOneWhere([ + 'entity_type' => $data['entity_type'], + 'entity_id' => $data['entity_id'], + 'attribute_id' => $attribute->id, + ]); + + if (! $attributeValue) { + $this->create([ + 'entity_type' => $data['entity_type'], + 'entity_id' => $data['entity_id'], + 'attribute_id' => $attribute->id, + $typeColumn => $data[$attribute->code], + ]); + } else { + $this->update([ + $typeColumn => $data[$attribute->code], + ], $attributeValue->id); + + if ($attribute->type == 'image' || $attribute->type == 'file') { + if ($attributeValue->text_value) { + Storage::delete($attributeValue->text_value); + } + } + } + } + } + + /** + * Is value unique. + * + * @param int $entityId + * @param string $entityType + * @param Attribute $attribute + * @param string $value + * @return bool + */ + public function isValueUnique($entityId, $entityType, $attribute, $value) + { + $query = $this->resetScope()->model + ->where('attribute_id', $attribute->id) + ->where('entity_type', $entityType) + ->where('entity_id', '!=', $entityId); + + /** + * If the attribute type is email or phone, check the JSON value. + */ + if (in_array($attribute->type, ['email', 'phone'])) { + $query->whereJsonContains($this->model::$attributeTypeFields[$attribute->type], [['value' => $value]]); + } else { + $query->where($this->model::$attributeTypeFields[$attribute->type], $value); + } + + return $query->get()->count() ? false : true; + } + + /** + * Removed null values from email and phone fields. + * + * @param array $data + * @return array + */ + public function sanitizeEmailAndPhone($data) + { + foreach ($data as $key => $row) { + if (is_null($row['value'])) { + unset($data[$key]); + } + } + + return $data; + } + + /** + * Replace placeholders with values + */ + public function getAttributeLabel(mixed $value, mixed $attribute) + { + switch ($attribute?->type) { + case 'price': + $label = core()->formatBasePrice($value); + + break; + + case 'boolean': + $label = $value ? 'Yes' : 'No'; + + break; + + case 'select': + case 'radio': + case 'lookup': + if ($attribute->lookup_type) { + $option = $this->attributeRepository->getLookUpEntity($attribute->lookup_type, $value); + } else { + $option = $attribute->options->where('id', $value)->first(); + } + + $label = $option?->name; + + break; + + case 'multiselect': + case 'checkbox': + if ($attribute->lookup_type) { + $options = $this->attributeRepository->getLookUpEntity($attribute->lookup_type, explode(',', $value)); + } else { + $options = $attribute->options->whereIn('id', explode(',', $value)); + } + + $optionsLabels = []; + + foreach ($options as $key => $option) { + $optionsLabels[] = $option->name; + } + + $label = implode(', ', $optionsLabels); + + break; + + case 'email': + case 'phone': + if (! is_array($value)) { + break; + } + + $optionsLabels = []; + + foreach ($value as $item) { + $optionsLabels[] = $item['value'].' ('.$item['label'].')'; + } + + $label = implode(', ', $optionsLabels); + + break; + + case 'address': + if ( + ! $value + || ! count(array_filter($value)) + ) { + break; + } + + $label = $value['address'].'
    ' + .$value['postcode'].' '.$value['city'].'
    ' + .core()->state_name($value['state']).'
    ' + .core()->country_name($value['country']).'
    '; + + break; + + case 'date': + if ($value) { + $label = Carbon::parse($value)->format('D M d, Y'); + } else { + $label = null; + } + + break; + + case 'datetime': + if ($value) { + $label = Carbon::parse($value)->format('D M d, Y H:i A'); + } else { + $label = null; + } + + break; + + default: + if ($value instanceof Carbon) { + $label = $value->format('D M d, Y H:i A'); + } else { + $label = $value; + } + + break; + } + + return $label ?? null; + } +} diff --git a/packages/Webkul/Attribute/src/Traits/CustomAttribute.php b/packages/Webkul/Attribute/src/Traits/CustomAttribute.php new file mode 100644 index 0000000..84ae7a9 --- /dev/null +++ b/packages/Webkul/Attribute/src/Traits/CustomAttribute.php @@ -0,0 +1,176 @@ + 'text_value', + 'textarea' => 'text_value', + 'price' => 'float_value', + 'boolean' => 'boolean_value', + 'select' => 'integer_value', + 'multiselect' => 'text_value', + 'checkbox' => 'text_value', + 'email' => 'json_value', + 'address' => 'json_value', + 'phone' => 'json_value', + 'lookup' => 'integer_value', + 'datetime' => 'datetime_value', + 'date' => 'date_value', + 'file' => 'text_value', + 'image' => 'text_value', + ]; + + /** + * Get the attribute values that owns the entity. + */ + public function attribute_values() + { + return $this->morphMany(AttributeValueProxy::modelClass(), 'entity'); + } + + /** + * Get an attribute from the model. + * + * @param string $key + * @return mixed + */ + public function getAttribute($key) + { + if (! method_exists(static::class, $key) && ! isset($this->attributes[$key])) { + if (isset($this->id)) { + $this->attributes[$key] = ''; + + $attribute = app(AttributeRepository::class)->getAttributeByCode($key); + + $this->attributes[$key] = $this->getCustomAttributeValue($attribute); + + return $this->getAttributeValue($key); + } + } + + return parent::getAttribute($key); + } + + /** + * @return array + */ + public function attributesToArray() + { + $attributes = parent::attributesToArray(); + + $hiddenAttributes = $this->getHidden(); + + if (isset($this->id)) { + $customAttributes = $this->getCustomAttributes(); + + foreach ($customAttributes as $attribute) { + if (in_array($attribute->code, $hiddenAttributes) && isset($this->attributes[$attribute->code])) { + continue; + } + + $attributes[$attribute->code] = $this->getCustomAttributeValue($attribute); + } + } + + return $attributes; + } + + /** + * Check in loaded family attributes. + * + * @return object + */ + public function getCustomAttributes() + { + static $attributes; + + if ($attributes) { + return $attributes; + } + + return $attributes = app(AttributeRepository::class)->where('entity_type', $this->getTable())->get(); + } + + /** + * Get an product attribute value. + * + * @return mixed + */ + public function getCustomAttributeValue($attribute) + { + if (! $attribute) { + return; + } + + $attributeValue = $this->attribute_values->where('attribute_id', $attribute->id)->first(); + + return $attributeValue[self::$attributeTypeFields[$attribute->type]] ?? null; + } + + /** + * Create a new instance of the given model. + * + * @param array $attributes + * @return Collection + */ + public function getLookUpAttributes($attributes) + { + $attributes = app(AttributeRepository::class)->scopeQuery(function ($query) use ($attributes) { + return $query->distinct() + ->where('type', 'lookup') + ->where('entity_type', request('entity_type')) + ->whereIn('code', array_keys($attributes, '', false)); + })->get(); + + return $attributes; + } + + /** + * Create a new instance of the given model. + * + * @param array $attributes + * @param bool $exists + * @return static + */ + public function newInstance($attributes = [], $exists = false) + { + // $attributes = $this->getLookUpAttributes($attributes); + + // Play with data here + + return parent::newInstance($attributes, $exists); + } + + /** + * Fill the model with an array of attributes. + * + * @return $this + * + * @throws MassAssignmentException + */ + public function fill(array $attributes) + { + // Play with data here + + return parent::fill($attributes); + } + + // Delete model's attribute values + public static function boot() + { + parent::boot(); + + static::deleting(function ($entity) { + $entity->attribute_values()->delete(); + }); + } +} diff --git a/packages/Webkul/Automation/src/Config/workflows.php b/packages/Webkul/Automation/src/Config/workflows.php new file mode 100644 index 0000000..7089788 --- /dev/null +++ b/packages/Webkul/Automation/src/Config/workflows.php @@ -0,0 +1,74 @@ + [ + + 'leads' => [ + 'name' => 'Leads', + 'class' => 'Webkul\Automation\Helpers\Entity\Lead', + 'events' => [ + [ + 'event' => 'lead.create.after', + 'name' => 'Created', + ], [ + 'event' => 'lead.update.after', + 'name' => 'Updated', + ], [ + 'event' => 'lead.delete.before', + 'name' => 'Deleted', + ], + ], + ], + + 'activities' => [ + 'name' => 'Activities', + 'class' => 'Webkul\Automation\Helpers\Entity\Activity', + 'events' => [ + [ + 'event' => 'activity.create.after', + 'name' => 'Created', + ], [ + 'event' => 'activity.update.after', + 'name' => 'Updated', + ], [ + 'event' => 'activity.delete.before', + 'name' => 'Deleted', + ], + ], + ], + + 'persons' => [ + 'name' => 'Persons', + 'class' => 'Webkul\Automation\Helpers\Entity\Person', + 'events' => [ + [ + 'event' => 'contacts.person.create.after', + 'name' => 'Created', + ], [ + 'event' => 'contacts.person.update.after', + 'name' => 'Updated', + ], [ + 'event' => 'contacts.person.delete.before', + 'name' => 'Deleted', + ], + ], + ], + + 'quotes' => [ + 'name' => 'Quotes', + 'class' => 'Webkul\Automation\Helpers\Entity\Quote', + 'events' => [ + [ + 'event' => 'quote.create.after', + 'name' => 'Created', + ], [ + 'event' => 'quote.update.after', + 'name' => 'Updated', + ], [ + 'event' => 'quote.delete.before', + 'name' => 'Deleted', + ], + ], + ], + ], +]; diff --git a/packages/Webkul/Automation/src/Contracts/Webhook.php b/packages/Webkul/Automation/src/Contracts/Webhook.php new file mode 100644 index 0000000..ac97275 --- /dev/null +++ b/packages/Webkul/Automation/src/Contracts/Webhook.php @@ -0,0 +1,5 @@ +increments('id'); + $table->string('name'); + $table->string('description')->nullable(); + $table->string('entity_type'); + $table->string('event'); + $table->string('condition_type')->default('and'); + $table->json('conditions')->nullable(); + $table->json('actions')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('workflows'); + } +}; diff --git a/packages/Webkul/Automation/src/Database/Migrations/2024_07_24_150821_create_webhooks_table.php b/packages/Webkul/Automation/src/Database/Migrations/2024_07_24_150821_create_webhooks_table.php new file mode 100644 index 0000000..9554e90 --- /dev/null +++ b/packages/Webkul/Automation/src/Database/Migrations/2024_07_24_150821_create_webhooks_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('name'); + $table->string('entity_type'); + $table->string('description')->nullable(); + $table->string('method'); + $table->string('end_point'); + $table->json('query_params')->nullable(); + $table->json('headers')->nullable(); + $table->string('payload_type'); + $table->string('raw_payload_type'); + $table->json('payload')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('webhooks'); + } +}; diff --git a/packages/Webkul/Automation/src/Helpers/Entity.php b/packages/Webkul/Automation/src/Helpers/Entity.php new file mode 100644 index 0000000..f9f5b21 --- /dev/null +++ b/packages/Webkul/Automation/src/Helpers/Entity.php @@ -0,0 +1,103 @@ + $entity) { + $object = app($entity['class']); + + $events[$key] = [ + 'id' => $key, + 'name' => $entity['name'], + 'events' => $entity['events'], + ]; + } + + return $events; + } + + /** + * Returns conditions to match for the entity + * + * @return array + */ + public function getConditions() + { + $entities = config('workflows.trigger_entities'); + + $conditions = []; + + foreach ($entities as $key => $entity) { + $object = app($entity['class']); + + $conditions[$key] = $object->getConditions(); + } + + return $conditions; + } + + /** + * Returns workflow actions + * + * @return array + */ + public function getActions() + { + $entities = config('workflows.trigger_entities'); + + $conditions = []; + + foreach ($entities as $key => $entity) { + $object = app($entity['class']); + + $conditions[$key] = $object->getActions(); + } + + return $conditions; + } + + /** + * Returns placeholders for email templates + * + * @return array + */ + public function getEmailTemplatePlaceholders() + { + $entities = config('workflows.trigger_entities'); + + $placeholders = []; + + foreach ($entities as $key => $entity) { + $object = app($entity['class']); + + $placeholders[] = $object->getEmailTemplatePlaceholders($entity); + } + + return $placeholders; + } +} diff --git a/packages/Webkul/Automation/src/Helpers/Entity/AbstractEntity.php b/packages/Webkul/Automation/src/Helpers/Entity/AbstractEntity.php new file mode 100644 index 0000000..8835c57 --- /dev/null +++ b/packages/Webkul/Automation/src/Helpers/Entity/AbstractEntity.php @@ -0,0 +1,232 @@ +getAttributes($this->entityType); + } + + /** + * Get attributes for entity. + */ + public function getAttributes(string $entityType, array $skipAttributes = ['textarea', 'image', 'file', 'address']): array + { + $attributes = []; + + foreach ($this->attributeRepository->findByField('entity_type', $entityType) as $attribute) { + if (in_array($attribute->type, $skipAttributes)) { + continue; + } + + if ($attribute->lookup_type) { + $options = []; + } else { + $options = $attribute->options; + } + + $attributes[] = [ + 'id' => $attribute->code, + 'type' => $attribute->type, + 'name' => $attribute->name, + 'lookup_type' => $attribute->lookup_type, + 'options' => $options, + ]; + } + + return $attributes; + } + + /** + * Returns placeholders for email templates. + */ + public function getEmailTemplatePlaceholders(array $entity): array + { + $menuItems = []; + + foreach ($this->getAttributes($this->entityType) as $attribute) { + $menuItems[] = [ + 'text' => $attribute['name'], + 'value' => '{%'.$this->entityType.'.'.$attribute['id'].'%}', + ]; + } + + return [ + 'text' => $entity['name'], + 'menu' => $menuItems, + ]; + } + + /** + * Replace placeholders with values. + */ + public function replacePlaceholders(mixed $entity, string $content): string + { + foreach ($this->getAttributes($this->entityType, []) as $attribute) { + $value = ''; + + switch ($attribute['type']) { + case 'price': + $value = core()->formatBasePrice($entity->{$attribute['id']}); + + break; + + case 'boolean': + $value = $entity->{$attribute['id']} ? trans('admin::app.common.yes') : trans('admin::app.common.no'); + + break; + + case 'select': + case 'radio': + case 'lookup': + if ($attribute['lookup_type']) { + $option = $this->attributeRepository->getLookUpEntity($attribute['lookup_type'], $entity->{$attribute['id']}); + } else { + $option = $attribute['options']->where('id', $entity->{$attribute['id']})->first(); + } + + $value = $option ? $option->name : ''; + + break; + + case 'multiselect': + case 'checkbox': + if ($attribute['lookup_type']) { + $options = $this->attributeRepository->getLookUpEntity($attribute['lookup_type'], explode(',', $entity->{$attribute['id']})); + } else { + $options = $attribute['options']->whereIn('id', explode(',', $entity->{$attribute['id']})); + } + + $optionsLabels = []; + + foreach ($options as $key => $option) { + $optionsLabels[] = $option->name; + } + + $value = implode(', ', $optionsLabels); + + break; + + case 'email': + case 'phone': + if (! is_array($entity->{$attribute['id']})) { + break; + } + + $optionsLabels = []; + + foreach ($entity->{$attribute['id']} as $item) { + $optionsLabels[] = $item['value'].' ('.$item['label'].')'; + } + + $value = implode(', ', $optionsLabels); + + break; + + case 'address': + if (! $entity->{$attribute['id']} || ! count(array_filter($entity->{$attribute['id']}))) { + break; + } + + $value = $entity->{$attribute['id']}['address'].'
    ' + .$entity->{$attribute['id']}['postcode'].' '.$entity->{$attribute['id']}['city'].'
    ' + .core()->state_name($entity->{$attribute['id']}['state']).'
    ' + .core()->country_name($entity->{$attribute['id']}['country']).'
    '; + + break; + + case 'date': + if ($entity->{$attribute['id']}) { + $value = ! is_object($entity->{$attribute['id']}) + ? Carbon::parse($entity->{$attribute['id']}) + : $entity->{$attribute['id']}->format('D M d, Y'); + } else { + $value = 'N/A'; + } + + break; + + case 'datetime': + if ($entity->{$attribute['id']}) { + $value = ! is_object($entity->{$attribute['id']}) + ? Carbon::parse($entity->{$attribute['id']}) + : $entity->{$attribute['id']}->format('D M d, Y H:i A'); + } else { + $value = 'N/A'; + } + + break; + + default: + $value = $entity->{$attribute['id']}; + + break; + } + + $content = strtr($content, [ + '{%'.$this->entityType.'.'.$attribute['id'].'%}' => $value, + '{% '.$this->entityType.'.'.$attribute['id'].' %}' => $value, + ]); + } + + return $content; + } + + /** + * Trigger webhook. + * + * @return void + */ + public function triggerWebhook(int $webhookId, mixed $entity) + { + $webhook = $this->webhookRepository->findOrFail($webhookId); + + $payload = [ + 'method' => $webhook->method, + 'query_params' => $this->replacePlaceholders($entity, json_encode($webhook->query_params)), + 'end_point' => $this->replacePlaceholders($entity, $webhook->end_point), + 'payload' => $this->replacePlaceholders($entity, json_encode($webhook->payload)), + 'headers' => $this->replacePlaceholders($entity, json_encode($webhook->headers)), + ]; + + $this->webhookService->triggerWebhook($payload); + } +} diff --git a/packages/Webkul/Automation/src/Helpers/Entity/Activity.php b/packages/Webkul/Automation/src/Helpers/Entity/Activity.php new file mode 100644 index 0000000..5368630 --- /dev/null +++ b/packages/Webkul/Automation/src/Helpers/Entity/Activity.php @@ -0,0 +1,324 @@ + 'title', + 'type' => 'text', + 'name' => 'Title', + 'lookup_type' => null, + 'options' => collect(), + ], [ + 'id' => 'type', + 'type' => 'multiselect', + 'name' => 'Type', + 'lookup_type' => null, + 'options' => collect([ + (object) [ + 'id' => 'note', + 'name' => 'Note', + ], (object) [ + 'id' => 'call', + 'name' => 'Call', + ], (object) [ + 'id' => 'meeting', + 'name' => 'Meeting', + ], (object) [ + 'id' => 'lunch', + 'name' => 'Lunch', + ], (object) [ + 'id' => 'file', + 'name' => 'File', + ], + ]), + ], [ + 'id' => 'location', + 'type' => 'text', + 'name' => 'Location', + 'lookup_type' => null, + 'options' => collect(), + ], [ + 'id' => 'comment', + 'type' => 'textarea', + 'name' => 'Comment', + 'lookup_type' => null, + 'options' => collect(), + ], [ + 'id' => 'schedule_from', + 'type' => 'datetime', + 'name' => 'Schedule From', + 'lookup_type' => null, + 'options' => collect(), + ], [ + 'id' => 'schedule_to', + 'type' => 'datetime', + 'name' => 'Schedule To', + 'lookup_type' => null, + 'options' => collect(), + ], [ + 'id' => 'user_id', + 'type' => 'select', + 'name' => 'User', + 'lookup_type' => 'users', + 'options' => $this->attributeRepository->getLookUpOptions('users'), + ], + ]; + + return $attributes; + } + + /** + * Returns placeholders for email templates. + */ + public function getEmailTemplatePlaceholders(array $entity): array + { + $emailTemplates = parent::getEmailTemplatePlaceholders($entity); + + $emailTemplates['menu'][] = [ + 'text' => 'Participants', + 'value' => '{%activities.participants%}', + ]; + + return $emailTemplates; + } + + /** + * Replace placeholders with values. + */ + public function replacePlaceholders(mixed $entity, string $content): string + { + $content = parent::replacePlaceholders($entity, $content); + + $value = '

      '; + + foreach ($entity->participants as $participant) { + $value .= '
    • '.($participant->user ? $participant->user->name : $participant->person->name).'
    • '; + } + + $value .= '
    '; + + return strtr($content, [ + '{%'.$this->entityType.'.participants%}' => $value, + '{% '.$this->entityType.'.participants %}' => $value, + ]); + } + + /** + * Listing of the entities. + */ + public function getEntity(mixed $entity): mixed + { + if (! $entity instanceof ContractsActivity) { + $entity = $this->activityRepository->find($entity); + } + + return $entity; + } + + /** + * Returns workflow actions. + */ + public function getActions(): array + { + $emailTemplates = $this->emailTemplateRepository->all(['id', 'name']); + + $webhooksOptions = $this->webhookRepository->all(['id', 'name']); + + return [ + [ + 'id' => 'update_related_leads', + 'name' => trans('admin::app.settings.workflows.helpers.update-related-leads'), + 'attributes' => $this->getAttributes('leads'), + ], [ + 'id' => 'send_email_to_sales_owner', + 'name' => trans('admin::app.settings.workflows.helpers.send-email-to-sales-owner'), + 'options' => $emailTemplates, + ], [ + 'id' => 'send_email_to_participants', + 'name' => trans('admin::app.settings.workflows.helpers.send-email-to-participants'), + 'options' => $emailTemplates, + ], [ + 'id' => 'trigger_webhook', + 'name' => trans('admin::app.settings.workflows.helpers.add-webhook'), + 'options' => $webhooksOptions, + ], + ]; + } + + /** + * Execute workflow actions. + */ + public function executeActions(mixed $workflow, mixed $activity): void + { + foreach ($workflow->actions as $action) { + switch ($action['id']) { + case 'update_related_leads': + $leadIds = $this->activityRepository->getModel() + ->leftJoin('lead_activities', 'activities.id', 'lead_activities.activity_id') + ->leftJoin('leads', 'lead_activities.lead_id', 'leads.id') + ->addSelect('leads.id') + ->where('activities.id', $activity->id) + ->pluck('id'); + + foreach ($leadIds as $leadId) { + $this->leadRepository->update( + [ + 'entity_type' => 'leads', + $action['attribute'] => $action['value'], + ], + $leadId, + [$action['attribute']] + ); + } + + break; + + case 'send_email_to_sales_owner': + $emailTemplate = $this->emailTemplateRepository->find($action['value']); + + if (! $emailTemplate) { + break; + } + + try { + Mail::queue(new Common([ + 'to' => $activity->user->email, + 'subject' => $this->replacePlaceholders($activity, $emailTemplate->subject), + 'body' => $this->replacePlaceholders($activity, $emailTemplate->content), + 'attachments' => [ + [ + 'name' => 'invite.ics', + 'mime' => 'text/calendar', + 'content' => $this->getICSContent($activity), + ], + ], + ])); + } catch (\Exception $e) { + } + + break; + + case 'send_email_to_participants': + $emailTemplate = $this->emailTemplateRepository->find($action['value']); + + if (! $emailTemplate) { + break; + } + + try { + foreach ($activity->participants as $participant) { + Mail::queue(new Common([ + 'to' => $participant->user + ? $participant->user->email + : data_get($participant->person->emails, '*.value'), + 'subject' => $this->replacePlaceholders($activity, $emailTemplate->subject), + 'body' => $this->replacePlaceholders($activity, $emailTemplate->content), + 'attachments' => [ + [ + 'name' => 'invite.ics', + 'mime' => 'text/calendar', + 'content' => $this->getICSContent($activity), + ], + ], + ])); + } + } catch (\Exception $e) { + } + + break; + + case 'trigger_webhook': + try { + $this->triggerWebhook($action['value'], $activity); + } catch (\Exception $e) { + report($e); + } + + break; + } + } + } + + /** + * Returns .ics file for attachments. + */ + public function getICSContent(ContractsActivity $activity): string + { + $content = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Krayincrm//Krayincrm//EN', + 'METHOD:REQUEST', + 'BEGIN:VEVENT', + 'UID:'.time().'-'.$activity->id, + 'DTSTAMP:'.Carbon::now()->utc()->format('Ymd\THis\Z'), + 'CREATED:'.$activity->created_at->copy()->utc()->format('Ymd\THis\Z'), + 'SEQUENCE:1', + 'ORGANIZER;CN='.$activity->user->name.':MAILTO:'.$activity->user->email, + ]; + + foreach ($activity->participants as $participant) { + if ($participant->user) { + $content[] = 'ATTENDEE;ROLE=REQ-PARTICIPANT;CN='.$participant->user->name.';PARTSTAT=NEEDS-ACTION:MAILTO:'.$participant->user->email; + } else { + foreach (data_get($participant->person->emails, '*.value') as $email) { + $content[] = 'ATTENDEE;ROLE=REQ-PARTICIPANT;CN='.$participant->person->name.';PARTSTAT=NEEDS-ACTION:MAILTO:'.$email; + } + } + } + + $content = array_merge($content, [ + 'DTSTART:'.$activity->schedule_from->copy()->utc()->format('Ymd\THis\Z'), + 'DTEND:'.$activity->schedule_to->copy()->utc()->format('Ymd\THis\Z'), + 'SUMMARY:'.$activity->title, + 'LOCATION:'.$activity->location, + 'DESCRIPTION:'.$activity->comment, + 'END:VEVENT', + 'END:VCALENDAR', + ]); + + return implode("\r\n", $content); + } +} diff --git a/packages/Webkul/Automation/src/Helpers/Entity/Lead.php b/packages/Webkul/Automation/src/Helpers/Entity/Lead.php new file mode 100644 index 0000000..130bb01 --- /dev/null +++ b/packages/Webkul/Automation/src/Helpers/Entity/Lead.php @@ -0,0 +1,210 @@ +leadRepository->find($entity); + } + + return $entity; + } + + /** + * Returns attributes. + */ + public function getAttributes(string $entityType, array $skipAttributes = ['textarea', 'image', 'file', 'address']): array + { + return parent::getAttributes($entityType, $skipAttributes); + } + + /** + * Returns workflow actions. + */ + public function getActions(): array + { + $emailTemplates = $this->emailTemplateRepository->all(['id', 'name']); + + $webhooksOptions = $this->webhookRepository->all(['id', 'name']); + + return [ + [ + 'id' => 'update_lead', + 'name' => trans('admin::app.settings.workflows.helpers.update-lead'), + 'attributes' => $this->getAttributes('leads'), + ], [ + 'id' => 'update_person', + 'name' => trans('admin::app.settings.workflows.helpers.update-person'), + 'attributes' => $this->getAttributes('persons'), + ], [ + 'id' => 'send_email_to_person', + 'name' => trans('admin::app.settings.workflows.helpers.send-email-to-person'), + 'options' => $emailTemplates, + ], [ + 'id' => 'send_email_to_sales_owner', + 'name' => trans('admin::app.settings.workflows.helpers.send-email-to-sales-owner'), + 'options' => $emailTemplates, + ], [ + 'id' => 'add_tag', + 'name' => trans('admin::app.settings.workflows.helpers.add-tag'), + ], [ + 'id' => 'add_note_as_activity', + 'name' => trans('admin::app.settings.workflows.helpers.add-note-as-activity'), + ], [ + 'id' => 'trigger_webhook', + 'name' => trans('admin::app.settings.workflows.helpers.add-webhook'), + 'options' => $webhooksOptions, + ], + ]; + } + + /** + * Execute workflow actions. + */ + public function executeActions(mixed $workflow, mixed $lead): void + { + foreach ($workflow->actions as $action) { + switch ($action['id']) { + case 'update_lead': + $this->leadRepository->update( + [ + 'entity_type' => 'leads', + $action['attribute'] => $action['value'], + ], + $lead->id, + [$action['attribute']] + ); + + break; + + case 'update_person': + $this->personRepository->update([ + 'entity_type' => 'persons', + $action['attribute'] => $action['value'], + ], $lead->person_id); + + break; + + case 'send_email_to_person': + $emailTemplate = $this->emailTemplateRepository->find($action['value']); + + if (! $emailTemplate) { + break; + } + + try { + Mail::queue(new Common([ + 'to' => data_get($lead->person->emails, '*.value'), + 'subject' => $this->replacePlaceholders($lead, $emailTemplate->subject), + 'body' => $this->replacePlaceholders($lead, $emailTemplate->content), + ])); + } catch (\Exception $e) { + } + + break; + + case 'send_email_to_sales_owner': + $emailTemplate = $this->emailTemplateRepository->find($action['value']); + + if (! $emailTemplate) { + break; + } + + try { + Mail::queue(new Common([ + 'to' => $lead->user->email, + 'subject' => $this->replacePlaceholders($lead, $emailTemplate->subject), + 'body' => $this->replacePlaceholders($lead, $emailTemplate->content), + ])); + } catch (\Exception $e) { + } + + break; + + case 'add_tag': + $colors = [ + '#337CFF', + '#FEBF00', + '#E5549F', + '#27B6BB', + '#FB8A3F', + '#43AF52', + ]; + + if (! $tag = $this->tagRepository->findOneByField('name', $action['value'])) { + $tag = $this->tagRepository->create([ + 'name' => $action['value'], + 'color' => $colors[rand(0, 5)], + 'user_id' => auth()->guard('user')->user()->id, + ]); + } + + if (! $lead->tags->contains($tag->id)) { + $lead->tags()->attach($tag->id); + } + + break; + + case 'add_note_as_activity': + $activity = $this->activityRepository->create([ + 'type' => 'note', + 'comment' => $action['value'], + 'is_done' => 1, + 'user_id' => auth()->guard('user')->user()->id, + ]); + + $lead->activities()->attach($activity->id); + + break; + + case 'trigger_webhook': + try { + $this->triggerWebhook($action['value'], $lead); + } catch (\Exception $e) { + report($e); + } + + break; + } + } + } +} diff --git a/packages/Webkul/Automation/src/Helpers/Entity/Person.php b/packages/Webkul/Automation/src/Helpers/Entity/Person.php new file mode 100644 index 0000000..0bece2c --- /dev/null +++ b/packages/Webkul/Automation/src/Helpers/Entity/Person.php @@ -0,0 +1,140 @@ +personRepository->find($entity); + } + + return $entity; + } + + /** + * Returns workflow actions. + */ + public function getActions(): array + { + $emailTemplates = $this->emailTemplateRepository->all(['id', 'name']); + + $webhooksOptions = $this->webhookRepository->all(['id', 'name']); + + return [ + [ + 'id' => 'update_person', + 'name' => trans('admin::app.settings.workflows.helpers.update-person'), + 'attributes' => $this->getAttributes('persons'), + ], [ + 'id' => 'update_related_leads', + 'name' => trans('admin::app.settings.workflows.helpers.update-related-leads'), + 'attributes' => $this->getAttributes('leads'), + ], [ + 'id' => 'send_email_to_person', + 'name' => trans('admin::app.settings.workflows.helpers.send-email-to-person'), + 'options' => $emailTemplates, + ], [ + 'id' => 'trigger_webhook', + 'name' => trans('admin::app.settings.workflows.helpers.add-webhook'), + 'options' => $webhooksOptions, + ], + ]; + } + + /** + * Execute workflow actions. + */ + public function executeActions(mixed $workflow, mixed $person): void + { + foreach ($workflow->actions as $action) { + switch ($action['id']) { + case 'update_person': + $this->personRepository->update([ + 'entity_type' => 'persons', + $action['attribute'] => $action['value'], + ], $person->id); + + break; + + case 'update_related_leads': + $leads = $this->leadRepository->findByField('person_id', $person->id); + + foreach ($leads as $lead) { + $this->leadRepository->update( + [ + 'entity_type' => 'leads', + $action['attribute'] => $action['value'], + ], + $lead->id, + [$action['attribute']] + ); + } + + break; + + case 'send_email_to_person': + $emailTemplate = $this->emailTemplateRepository->find($action['value']); + + if (! $emailTemplate) { + break; + } + + try { + Mail::queue(new Common([ + 'to' => data_get($person->emails, '*.value'), + 'subject' => $this->replacePlaceholders($person, $emailTemplate->subject), + 'body' => $this->replacePlaceholders($person, $emailTemplate->content), + ])); + } catch (\Exception $e) { + report($e); + } + + break; + + case 'trigger_webhook': + try { + $this->triggerWebhook($action['value'], $person); + } catch (\Exception $e) { + report($e); + } + + break; + } + } + } +} diff --git a/packages/Webkul/Automation/src/Helpers/Entity/Quote.php b/packages/Webkul/Automation/src/Helpers/Entity/Quote.php new file mode 100644 index 0000000..f758bd7 --- /dev/null +++ b/packages/Webkul/Automation/src/Helpers/Entity/Quote.php @@ -0,0 +1,172 @@ +quoteRepository->find($entity); + } + + return $entity; + } + + /** + * Returns workflow actions. + */ + public function getActions(): array + { + $emailTemplates = $this->emailTemplateRepository->all(['id', 'name']); + + $webhookOptions = $this->webhookRepository->all(['id', 'name']); + + return [ + [ + 'id' => 'update_quote', + 'name' => trans('admin::app.settings.workflows.helpers.update-quote'), + 'attributes' => $this->getAttributes('quotes'), + ], [ + 'id' => 'update_person', + 'name' => trans('admin::app.settings.workflows.helpers.update-person'), + 'attributes' => $this->getAttributes('persons'), + ], [ + 'id' => 'update_related_leads', + 'name' => trans('admin::app.settings.workflows.helpers.update-related-leads'), + 'attributes' => $this->getAttributes('leads'), + ], [ + 'id' => 'send_email_to_person', + 'name' => trans('admin::app.settings.workflows.helpers.send-email-to-person'), + 'options' => $emailTemplates, + ], [ + 'id' => 'send_email_to_sales_owner', + 'name' => trans('admin::app.settings.workflows.helpers.send-email-to-sales-owner'), + 'options' => $emailTemplates, + ], [ + 'id' => 'trigger_webhook', + 'name' => trans('admin::app.settings.workflows.helpers.add-webhook'), + 'options' => $webhookOptions, + ], + ]; + } + + /** + * Execute workflow actions. + */ + public function executeActions(mixed $workflow, mixed $quote): void + { + foreach ($workflow->actions as $action) { + switch ($action['id']) { + case 'update_quote': + $this->quoteRepository->update([ + 'entity_type' => 'quotes', + $action['attribute'] => $action['value'], + ], $quote->id); + + break; + + case 'update_person': + $this->personRepository->update([ + 'entity_type' => 'persons', + $action['attribute'] => $action['value'], + ], $quote->person_id); + + break; + + case 'update_related_leads': + foreach ($quote->leads as $lead) { + $this->leadRepository->update( + [ + 'entity_type' => 'leads', + $action['attribute'] => $action['value'], + ], + $lead->id, + [$action['attribute']] + ); + } + + break; + + case 'send_email_to_person': + $emailTemplate = $this->emailTemplateRepository->find($action['value']); + + if (! $emailTemplate) { + break; + } + + try { + Mail::queue(new Common([ + 'to' => data_get($quote->person->emails, '*.value'), + 'subject' => $this->replacePlaceholders($quote, $emailTemplate->subject), + 'body' => $this->replacePlaceholders($quote, $emailTemplate->content), + ])); + } catch (\Exception $e) { + } + + break; + + case 'send_email_to_sales_owner': + $emailTemplate = $this->emailTemplateRepository->find($action['value']); + + if (! $emailTemplate) { + break; + } + + try { + Mail::queue(new Common([ + 'to' => $quote->user->email, + 'subject' => $this->replacePlaceholders($quote, $emailTemplate->subject), + 'body' => $this->replacePlaceholders($quote, $emailTemplate->content), + ])); + } catch (\Exception $e) { + } + + break; + + case 'trigger_webhook': + try { + $this->triggerWebhook($action['value'], $quote); + } catch (\Exception $e) { + report($e); + } + + break; + } + } + } +} diff --git a/packages/Webkul/Automation/src/Helpers/Validator.php b/packages/Webkul/Automation/src/Helpers/Validator.php new file mode 100644 index 0000000..b2048e7 --- /dev/null +++ b/packages/Webkul/Automation/src/Helpers/Validator.php @@ -0,0 +1,185 @@ +conditions) { + return true; + } + + $validConditionCount = $totalConditionCount = 0; + + foreach ($workflow->conditions as $condition) { + if (! $condition['attribute'] + || ! isset($condition['value']) + || is_null($condition['value']) + || $condition['value'] == '' + ) { + continue; + } + + $totalConditionCount++; + + if ($workflow->condition_type == 'and') { + if (! $this->validateEntity($condition, $entity)) { + return false; + } else { + $validConditionCount++; + } + } else { + if ($this->validateEntity($condition, $entity)) { + return true; + } + } + } + + return $validConditionCount == $totalConditionCount ? true : false; + } + + /** + * Validate object + * + * @param array $condition + * @param mixed $entity + * @return bool + */ + private function validateEntity($condition, $entity) + { + return $this->validateAttribute( + $condition, + $this->getAttributeValue($condition, $entity) + ); + } + + /** + * Return value for the attribute + * + * @param array $condition + * @param mixed $entity + * @return bool + */ + public function getAttributeValue($condition, $entity) + { + $value = $entity->{$condition['attribute']}; + + if (! in_array($condition['attribute_type'], ['multiselect', 'checkbox'])) { + return $value; + } + + return $value ? explode(',', $value) : []; + } + + /** + * Validate attribute value for condition + * + * @param array $condition + * @param mixed $attributeValue + * @return bool + */ + public function validateAttribute($condition, $attributeValue) + { + switch ($condition['operator']) { + case '==': case '!=': + if (is_array($condition['value'])) { + if (! is_array($attributeValue)) { + return false; + } + + $result = ! empty(array_intersect($condition['value'], $attributeValue)); + } elseif (is_object($attributeValue)) { + $result = $attributeValue->value == $condition['value']; + } else { + if (is_array($attributeValue)) { + $result = count($attributeValue) == 1 && array_shift($attributeValue) == $condition['value']; + } else { + $result = $attributeValue == $condition['value']; + } + } + + break; + + case '<=': case '>': + if (! is_scalar($attributeValue)) { + return false; + } + + $result = $attributeValue <= $condition['value']; + + break; + + case '>=': case '<': + if (! is_scalar($attributeValue)) { + return false; + } + + $result = $attributeValue >= $condition['value']; + + break; + + case '{}': case '!{}': + if (is_scalar($attributeValue) && is_array($condition['value'])) { + foreach ($condition['value'] as $item) { + if (stripos($attributeValue, $item) !== false) { + $result = true; + + break; + } + } + } elseif (is_array($condition['value'])) { + if (! is_array($attributeValue)) { + return false; + } + + $result = ! empty(array_intersect($condition['value'], $attributeValue)); + } else { + if (is_array($attributeValue)) { + $result = self::validateArrayValues($attributeValue, $condition['value']); + } else { + $result = strpos($attributeValue, $condition['value']) !== false; + } + } + + break; + } + + if (in_array($condition['operator'], ['!=', '>', '<', '!{}'])) { + $result = ! $result; + } + + return $result; + } + + /** + * Validate the condition value against a multi dimensional array recursively + */ + private static function validateArrayValues(array $attributeValue, string $conditionValue): bool + { + if (in_array($conditionValue, $attributeValue, true) === true) { + return true; + } + + foreach ($attributeValue as $subValue) { + if (! is_array($subValue)) { + continue; + } + + if (self::validateArrayValues($subValue, $conditionValue) === true) { + return true; + } + } + + return false; + } +} diff --git a/packages/Webkul/Automation/src/Listeners/Entity.php b/packages/Webkul/Automation/src/Listeners/Entity.php new file mode 100644 index 0000000..0ca4f05 --- /dev/null +++ b/packages/Webkul/Automation/src/Listeners/Entity.php @@ -0,0 +1,45 @@ +workflowRepository->findByField('event', $eventName); + + foreach ($workflows as $workflow) { + $workflowEntity = app(config('workflows.trigger_entities.'.$workflow->entity_type.'.class')); + + $entity = $workflowEntity->getEntity($entity); + + if (! $this->validator->validate($workflow, $entity)) { + continue; + } + + try { + $workflowEntity->executeActions($workflow, $entity); + } catch (\Exception $e) { + logger()->error($e->getMessage()); + } + } + } +} diff --git a/packages/Webkul/Automation/src/Models/Webhook.php b/packages/Webkul/Automation/src/Models/Webhook.php new file mode 100644 index 0000000..0c29502 --- /dev/null +++ b/packages/Webkul/Automation/src/Models/Webhook.php @@ -0,0 +1,41 @@ + + */ + protected $fillable = [ + 'name', + 'entity_type', + 'description', + 'method', + 'end_point', + 'query_params', + 'headers', + 'payload_type', + 'raw_payload_type', + 'payload', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'query_params' => 'array', + 'headers' => 'array', + 'payload' => 'array', + ]; +} diff --git a/packages/Webkul/Automation/src/Models/WebhookProxy.php b/packages/Webkul/Automation/src/Models/WebhookProxy.php new file mode 100644 index 0000000..7e46e10 --- /dev/null +++ b/packages/Webkul/Automation/src/Models/WebhookProxy.php @@ -0,0 +1,7 @@ + 'array', + 'actions' => 'array', + ]; + + protected $fillable = [ + 'name', + 'description', + 'entity_type', + 'event', + 'condition_type', + 'conditions', + 'actions', + ]; +} diff --git a/packages/Webkul/Automation/src/Models/WorkflowProxy.php b/packages/Webkul/Automation/src/Models/WorkflowProxy.php new file mode 100644 index 0000000..36ae76b --- /dev/null +++ b/packages/Webkul/Automation/src/Models/WorkflowProxy.php @@ -0,0 +1,7 @@ +loadMigrationsFrom(__DIR__.'/../Database/Migrations'); + + Event::listen('*', function ($eventName, array $data) { + if (! in_array($eventName, data_get(config('workflows.trigger_entities'), '*.events.*.event'))) { + return; + } + + app(Entity::class)->process($eventName, current($data)); + }); + } + + /** + * Register services. + * + * @return void + */ + public function register() + { + $this->registerConfig(); + } + + /** + * Register package config. + * + * @return void + */ + protected function registerConfig() + { + $this->mergeConfigFrom(dirname(__DIR__).'/Config/workflows.php', 'workflows'); + } +} diff --git a/packages/Webkul/Automation/src/Repositories/WebhookRepository.php b/packages/Webkul/Automation/src/Repositories/WebhookRepository.php new file mode 100644 index 0000000..629f4d7 --- /dev/null +++ b/packages/Webkul/Automation/src/Repositories/WebhookRepository.php @@ -0,0 +1,17 @@ +client = new Client([ + 'timeout' => 30, + 'connect_timeout' => 10, + 'verify' => true, + 'http_errors' => false, + 'allow_redirects' => [ + 'max' => 5, + 'strict' => true, + 'referer' => false, + 'protocols' => ['http', 'https'], + 'on_redirect' => function ($request, $response, $uri) { + if (! $this->isSafeEndpoint((string) $uri)) { + throw new \RuntimeException('Blocked redirect to a disallowed webhook endpoint.'); + } + }, + ], + ]); + } + + /** + * Trigger the webhook. + */ + public function triggerWebhook(mixed $data): array + { + if ( + ! isset($data['method']) + || ! isset($data['end_point']) + ) { + return [ + 'status' => 'error', + 'response' => 'Missing required fields: method or end_point', + ]; + } + + $headers = isset($data['headers']) ? $this->parseJsonField($data['headers']) : []; + $payload = isset($data['payload']) ? $data['payload'] : null; + $data['end_point'] = $this->appendQueryParams($data['end_point'], $data['query_params'] ?? ''); + + if (! $this->isSafeEndpoint($data['end_point'])) { + return [ + 'status' => 'error', + 'response' => 'The webhook endpoint URL is not allowed.', + ]; + } + + $formattedHeaders = $this->formatHeaders($headers); + + $options = $this->buildRequestOptions($data['method'], $formattedHeaders, $payload); + + try { + $response = $this->client->request( + strtoupper($data['method']), + $data['end_point'], + $options, + ); + + return [ + 'status' => 'success', + 'response' => $response->getBody()->getContents(), + 'status_code' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + ]; + } catch (RequestException $e) { + return [ + 'status' => 'error', + 'response' => $e->hasResponse() ? Message::toString($e->getResponse()) : $e->getMessage(), + 'status_code' => $e->hasResponse() ? $e->getResponse()->getStatusCode() : null, + ]; + } catch (\Throwable $e) { + return [ + 'status' => 'error', + 'response' => $e->getMessage(), + ]; + } + } + + /** + * Guard against Server-Side Request Forgery (SSRF). Only plain HTTP(S) endpoints whose host + * resolves exclusively to public addresses are permitted. Endpoints that resolve to private, + * reserved, loopback, or link-local ranges (e.g. cloud metadata services) are rejected. + */ + protected function isSafeEndpoint(string $endPoint): bool + { + $scheme = strtolower((string) parse_url($endPoint, PHP_URL_SCHEME)); + + $host = parse_url($endPoint, PHP_URL_HOST); + + if ( + ! in_array($scheme, ['http', 'https']) + || empty($host) + ) { + return false; + } + + $host = trim($host, '[]'); + + if (filter_var($host, FILTER_VALIDATE_IP)) { + $ips = [$host]; + } else { + $records = array_merge( + @dns_get_record($host, DNS_A) ?: [], + @dns_get_record($host, DNS_AAAA) ?: [], + ); + + $ips = array_values(array_filter(array_map( + fn ($record) => $record['ip'] ?? $record['ipv6'] ?? null, + $records, + ))); + } + + if (empty($ips)) { + return false; + } + + foreach ($ips as $ip) { + if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return false; + } + } + + return true; + } + + /** + * Parse JSON field safely. + */ + protected function parseJsonField(mixed $field): array + { + if (is_array($field)) { + return $field; + } + + if (is_string($field)) { + $decoded = json_decode($field, true); + + if ( + json_last_error() === JSON_ERROR_NONE + && is_array($decoded) + ) { + return $decoded; + } + } + + return []; + } + + /** + * Build request options based on method and content type. + */ + protected function buildRequestOptions(string $method, array $headers, mixed $payload): array + { + $options = []; + + if (! empty($headers)) { + $options['headers'] = $headers; + } + + if ( + $payload !== null + && ! in_array(strtoupper($method), ['GET', 'HEAD']) + ) { + $contentType = $this->getContentType($headers); + + switch ($contentType) { + case 'application/json': + $options['json'] = $this->prepareJsonPayload($payload); + + break; + + case 'application/x-www-form-urlencoded': + $options['form_params'] = $this->prepareFormPayload($payload); + + break; + + case 'multipart/form-data': + $options['multipart'] = $this->prepareMultipartPayload($payload); + + break; + + case 'text/plain': + case 'text/xml': + case 'application/xml': + $options['body'] = $this->prepareRawPayload($payload); + + break; + + default: + $options = array_merge($options, $this->autoDetectPayloadFormat($payload)); + + break; + } + } + + return $options; + } + + /** + * Prepare JSON payload. + */ + protected function prepareJsonPayload(mixed $payload): mixed + { + if (is_string($payload)) { + $decoded = json_decode($payload, true); + + if (json_last_error() === JSON_ERROR_NONE) { + return $decoded; + } + + return $payload; + } + + if (is_array($payload)) { + return $this->formatPayload($payload); + } + + return $payload; + } + + /** + * Prepare form payload. + */ + protected function prepareFormPayload(mixed $payload): array + { + if (is_string($payload)) { + $decoded = json_decode($payload, true); + + if ( + json_last_error() === JSON_ERROR_NONE + && is_array($decoded) + ) { + return $this->formatPayload($decoded); + } + + parse_str($payload, $parsed); + + return $parsed ?: []; + } + + if (is_array($payload)) { + return $this->formatPayload($payload); + } + + return []; + } + + /** + * Prepare multipart payload. + */ + protected function prepareMultipartPayload(mixed $payload): array + { + $formattedPayload = $this->prepareFormPayload($payload); + + return $this->buildMultipartData($formattedPayload); + } + + /** + * Prepare raw payload. + */ + protected function prepareRawPayload(mixed $payload): string + { + if (is_string($payload)) { + return $payload; + } + + if (is_array($payload)) { + return json_encode($payload); + } + + return (string) $payload; + } + + /** + * Auto-detect payload format when no content-type is specified. + */ + protected function autoDetectPayloadFormat(mixed $payload): array + { + if (is_string($payload)) { + $decoded = json_decode($payload, true); + + if (json_last_error() === JSON_ERROR_NONE) { + return ['json' => $decoded]; + } + + if ( + strpos($payload, '=') !== false + && strpos($payload, '&') !== false + ) { + parse_str($payload, $parsed); + + return ['form_params' => $parsed]; + } + + return ['body' => $payload]; + } + + if (is_array($payload)) { + $formatted = $this->formatPayload($payload); + + return ['json' => $formatted]; + } + + return ['body' => (string) $payload]; + } + + /** + * Get content type from headers. + */ + protected function getContentType(array $headers): string + { + foreach ($headers as $key => $value) { + if (strtolower($key) === 'content-type') { + $contentType = strtolower(trim(explode(';', $value)[0])); + + return $contentType; + } + } + + return ''; + } + + /** + * Build multipart data array. + */ + protected function buildMultipartData(array $payload): array + { + $multipart = []; + + foreach ($payload as $key => $value) { + $multipart[] = [ + 'name' => $key, + 'contents' => is_array($value) ? json_encode($value) : (string) $value, + ]; + } + + return $multipart; + } + + /** + * Format headers array. + */ + protected function formatHeaders(array $headers): array + { + if (empty($headers)) { + return []; + } + + $formattedHeaders = []; + + if ($this->isKeyValuePairArray($headers)) { + foreach ($headers as $header) { + if ( + isset($header['key']) + && array_key_exists('value', $header) + ) { + if ( + isset($header['disabled']) + && $header['disabled'] + ) { + continue; + } + + if ( + isset($header['enabled']) + && ! $header['enabled'] + ) { + continue; + } + + $formattedHeaders[$header['key']] = $header['value']; + } + } + } else { + $formattedHeaders = $headers; + } + + return $formattedHeaders; + } + + /** + * Format any incoming payload into a clean associative array. + */ + protected function formatPayload(mixed $payload): array + { + if (empty($payload)) { + return []; + } + + if ( + is_array($payload) + && isset($payload['key']) + && array_key_exists('value', $payload) + ) { + return [$payload['key'] => $payload['value']]; + } + + if ( + is_array($payload) + && array_is_list($payload) + && $this->isKeyValuePairArray($payload) + ) { + $formatted = []; + + foreach ($payload as $item) { + if ( + isset($item['key']) + && array_key_exists('value', $item) + ) { + if ( + isset($item['disabled']) + && $item['disabled'] + ) { + continue; + } + + if ( + isset($item['enabled']) + && ! $item['enabled'] + ) { + continue; + } + + $formatted[$item['key']] = $item['value']; + } + } + + return $formatted; + } + + return is_array($payload) ? $payload : []; + } + + /** + * Check if array is a key-value pair array. + */ + protected function isKeyValuePairArray(array $array): bool + { + if (empty($array)) { + return false; + } + + if ( + isset($array['key']) + && array_key_exists('value', $array) + ) { + return true; + } + + if (array_is_list($array)) { + return collect($array)->every(fn ($item) => is_array($item) && isset($item['key']) && array_key_exists('value', $item) + ); + } + + return false; + } + + /** + * Append query parameters to the endpoint URL. + */ + protected function appendQueryParams(string $endPoint, string $queryParamsJson): string + { + $queryParams = json_decode($queryParamsJson, true); + + if ( + json_last_error() !== JSON_ERROR_NONE + || ! is_array($queryParams) + ) { + return $endPoint; + } + + $queryArray = []; + + foreach ($queryParams as $param) { + if ( + isset($param['key']) + && array_key_exists('value', $param) + ) { + $queryArray[$param['key']] = $param['value']; + } + } + + $queryString = http_build_query($queryArray); + + $glue = str_contains($endPoint, '?') ? '&' : '?'; + + return $endPoint.($queryString ? $glue.$queryString : ''); + } +} diff --git a/packages/Webkul/Contact/composer.json b/packages/Webkul/Contact/composer.json new file mode 100755 index 0000000..205e43f --- /dev/null +++ b/packages/Webkul/Contact/composer.json @@ -0,0 +1,28 @@ +{ + "name": "krayin/laravel-contact", + "license": "MIT", + "authors": [ + { + "name": "Jitendra Singh", + "email": "jitendra@webkul.com" + } + ], + "require": { + "krayin/laravel-attribute": "^1.0", + "krayin/laravel-core": "^1.0" + }, + "autoload": { + "psr-4": { + "Webkul\\Contact\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Webkul\\Contact\\Providers\\ContactServiceProvider" + ], + "aliases": {} + } + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/packages/Webkul/Contact/src/Contracts/Organization.php b/packages/Webkul/Contact/src/Contracts/Organization.php new file mode 100644 index 0000000..50b0e12 --- /dev/null +++ b/packages/Webkul/Contact/src/Contracts/Organization.php @@ -0,0 +1,5 @@ + $this->faker->name(), + 'emails' => [$this->faker->unique()->safeEmail()], + 'contact_numbers' => [$this->faker->randomNumber(9)], + ]; + } +} diff --git a/packages/Webkul/Contact/src/Database/Migrations/2021_04_09_051326_create_organizations_table.php b/packages/Webkul/Contact/src/Database/Migrations/2021_04_09_051326_create_organizations_table.php new file mode 100644 index 0000000..8a2e106 --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2021_04_09_051326_create_organizations_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->string('name'); + $table->json('address')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('organizations'); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2021_04_09_065617_create_persons_table.php b/packages/Webkul/Contact/src/Database/Migrations/2021_04_09_065617_create_persons_table.php new file mode 100644 index 0000000..fc98d3b --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2021_04_09_065617_create_persons_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->string('name'); + $table->json('emails'); + $table->json('contact_numbers')->nullable(); + + $table->integer('organization_id')->unsigned()->nullable(); + $table->foreign('organization_id')->references('id')->on('organizations')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('persons'); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2021_09_22_194103_add_unique_index_to_name_in_organizations_table.php b/packages/Webkul/Contact/src/Database/Migrations/2021_09_22_194103_add_unique_index_to_name_in_organizations_table.php new file mode 100644 index 0000000..d383cd1 --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2021_09_22_194103_add_unique_index_to_name_in_organizations_table.php @@ -0,0 +1,32 @@ +unique('name'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('organizations', function (Blueprint $table) { + $table->dropUnique('organizations_name_unique'); + }); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2024_07_31_092951_add_job_title_in_persons_table.php b/packages/Webkul/Contact/src/Database/Migrations/2024_07_31_092951_add_job_title_in_persons_table.php new file mode 100644 index 0000000..ca4514c --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2024_07_31_092951_add_job_title_in_persons_table.php @@ -0,0 +1,28 @@ +string('job_title')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('persons', function (Blueprint $table) { + $table->dropColumn('job_title'); + }); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2024_08_06_145943_create_person_tags_table.php b/packages/Webkul/Contact/src/Database/Migrations/2024_08_06_145943_create_person_tags_table.php new file mode 100644 index 0000000..cd7c7dc --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2024_08_06_145943_create_person_tags_table.php @@ -0,0 +1,30 @@ +integer('tag_id')->unsigned(); + $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); + + $table->integer('person_id')->unsigned(); + $table->foreign('person_id')->references('id')->on('persons')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('person_tags'); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2024_08_06_161212_create_person_activities_table.php b/packages/Webkul/Contact/src/Database/Migrations/2024_08_06_161212_create_person_activities_table.php new file mode 100644 index 0000000..90917ef --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2024_08_06_161212_create_person_activities_table.php @@ -0,0 +1,30 @@ +integer('activity_id')->unsigned(); + $table->foreign('activity_id')->references('id')->on('activities')->onDelete('cascade'); + + $table->integer('person_id')->unsigned(); + $table->foreign('person_id')->references('id')->on('persons')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('lead_activities'); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2024_08_14_102116_add_user_id_column_in_persons_table.php b/packages/Webkul/Contact/src/Database/Migrations/2024_08_14_102116_add_user_id_column_in_persons_table.php new file mode 100644 index 0000000..069e669 --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2024_08_14_102116_add_user_id_column_in_persons_table.php @@ -0,0 +1,30 @@ +integer('user_id')->unsigned()->nullable(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('persons', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->dropColumn('user_id'); + }); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2024_08_14_102136_add_user_id_column_in_organizations_table.php b/packages/Webkul/Contact/src/Database/Migrations/2024_08_14_102136_add_user_id_column_in_organizations_table.php new file mode 100644 index 0000000..98f2910 --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2024_08_14_102136_add_user_id_column_in_organizations_table.php @@ -0,0 +1,30 @@ +integer('user_id')->unsigned()->nullable(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('organizations', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->dropColumn('user_id'); + }); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2024_09_09_112201_add_unique_id_to_person_table.php b/packages/Webkul/Contact/src/Database/Migrations/2024_09_09_112201_add_unique_id_to_person_table.php new file mode 100644 index 0000000..d6a5b65 --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2024_09_09_112201_add_unique_id_to_person_table.php @@ -0,0 +1,41 @@ +string('unique_id')->nullable()->unique(); + }); + + $tableName = DB::getTablePrefix().'persons'; + + DB::statement(" + UPDATE {$tableName} + SET unique_id = CONCAT( + user_id, '|', + organization_id, '|', + JSON_UNQUOTE(JSON_EXTRACT(emails, '$[0].value')), '|', + JSON_UNQUOTE(JSON_EXTRACT(contact_numbers, '$[0].value')) + ) + "); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('persons', function (Blueprint $table) { + $table->dropColumn('unique_id'); + }); + } +}; diff --git a/packages/Webkul/Contact/src/Database/Migrations/2025_03_19_132236_update_organization_id_column_in_persons_table.php b/packages/Webkul/Contact/src/Database/Migrations/2025_03_19_132236_update_organization_id_column_in_persons_table.php new file mode 100644 index 0000000..bb26d4a --- /dev/null +++ b/packages/Webkul/Contact/src/Database/Migrations/2025_03_19_132236_update_organization_id_column_in_persons_table.php @@ -0,0 +1,32 @@ +dropForeign(['organization_id']); + + $table->foreign('organization_id')->references('id')->on('organizations')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('persons', function (Blueprint $table) { + $table->dropForeign(['organization_id']); + + $table->foreign('organization_id')->references('id')->on('organizations')->onDelete('cascade'); + }); + } +}; diff --git a/packages/Webkul/Contact/src/Models/Organization.php b/packages/Webkul/Contact/src/Models/Organization.php new file mode 100644 index 0000000..90e974b --- /dev/null +++ b/packages/Webkul/Contact/src/Models/Organization.php @@ -0,0 +1,47 @@ + 'array', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'address', + 'user_id', + ]; + + /** + * Get persons. + * + * @return HasMany + */ + public function persons() + { + return $this->hasMany(PersonProxy::modelClass()); + } + + /** + * Get the user that owns the lead. + */ + public function user() + { + return $this->belongsTo(UserProxy::modelClass()); + } +} diff --git a/packages/Webkul/Contact/src/Models/OrganizationProxy.php b/packages/Webkul/Contact/src/Models/OrganizationProxy.php new file mode 100644 index 0000000..1e6d9c0 --- /dev/null +++ b/packages/Webkul/Contact/src/Models/OrganizationProxy.php @@ -0,0 +1,7 @@ + 'array', + 'contact_numbers' => 'array', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'emails', + 'contact_numbers', + 'job_title', + 'user_id', + 'organization_id', + 'unique_id', + ]; + + /** + * Get the user that owns the lead. + */ + public function user(): BelongsTo + { + return $this->belongsTo(UserProxy::modelClass()); + } + + /** + * Get the organization that owns the person. + */ + public function organization(): BelongsTo + { + return $this->belongsTo(OrganizationProxy::modelClass()); + } + + /** + * Get the activities. + */ + public function activities(): BelongsToMany + { + return $this->belongsToMany(ActivityProxy::modelClass(), 'person_activities'); + } + + /** + * The tags that belong to the person. + */ + public function tags(): BelongsToMany + { + return $this->belongsToMany(TagProxy::modelClass(), 'person_tags'); + } + + /** + * Get the leads for the person. + */ + public function leads(): HasMany + { + return $this->hasMany(LeadProxy::modelClass(), 'person_id'); + } + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): PersonFactory + { + return PersonFactory::new(); + } +} diff --git a/packages/Webkul/Contact/src/Models/PersonProxy.php b/packages/Webkul/Contact/src/Models/PersonProxy.php new file mode 100644 index 0000000..f768f47 --- /dev/null +++ b/packages/Webkul/Contact/src/Models/PersonProxy.php @@ -0,0 +1,7 @@ +loadMigrationsFrom(__DIR__.'/../Database/Migrations'); + } + + /** + * Register services. + * + * @return void + */ + public function register() {} +} diff --git a/packages/Webkul/Contact/src/Providers/ModuleServiceProvider.php b/packages/Webkul/Contact/src/Providers/ModuleServiceProvider.php new file mode 100644 index 0000000..e3dee01 --- /dev/null +++ b/packages/Webkul/Contact/src/Providers/ModuleServiceProvider.php @@ -0,0 +1,15 @@ +attributeValueRepository->save(array_merge($data, [ + 'entity_id' => $organization->id, + ])); + + return $organization; + } + + /** + * Update. + * + * @param int $id + * @param array $attribute + * @return Organization + */ + public function update(array $data, $id, $attributes = []) + { + if (isset($data['user_id'])) { + $data['user_id'] = $data['user_id'] ?: null; + } + + $organization = parent::update($data, $id); + + /** + * If attributes are provided then only save the provided attributes and return. + */ + if (! empty($attributes)) { + $conditions = ['entity_type' => $data['entity_type']]; + + if (isset($data['quick_add'])) { + $conditions['quick_add'] = 1; + } + + $attributes = $this->attributeRepository->where($conditions) + ->whereIn('code', $attributes) + ->get(); + + $this->attributeValueRepository->save(array_merge($data, [ + 'entity_id' => $organization->id, + ]), $attributes); + + return $organization; + } + + $this->attributeValueRepository->save(array_merge($data, [ + 'entity_id' => $organization->id, + ])); + + return $organization; + } + + /** + * Delete organization and it's persons. + * + * @param int $id + * @return @void + */ + public function delete($id) + { + $organization = $this->findOrFail($id); + + DB::transaction(function () use ($organization, $id) { + $this->attributeValueRepository->deleteWhere([ + 'entity_id' => $id, + 'entity_type' => 'organizations', + ]); + + $organization->delete(); + }); + } +} diff --git a/packages/Webkul/Contact/src/Repositories/PersonRepository.php b/packages/Webkul/Contact/src/Repositories/PersonRepository.php new file mode 100644 index 0000000..f184069 --- /dev/null +++ b/packages/Webkul/Contact/src/Repositories/PersonRepository.php @@ -0,0 +1,184 @@ +sanitizeRequestedPersonData($data); + + if (! empty($data['organization_name'])) { + $organization = $this->fetchOrCreateOrganizationByName($data['organization_name']); + + $data['organization_id'] = $organization->id; + } + + if (isset($data['user_id'])) { + $data['user_id'] = $data['user_id'] ?: null; + } + + $person = parent::create($data); + + $this->attributeValueRepository->save(array_merge($data, [ + 'entity_id' => $person->id, + ])); + + return $person; + } + + /** + * Update. + * + * @return Person + */ + public function update(array $data, $id, $attributes = []) + { + $data = $this->sanitizeRequestedPersonData($data); + + $data['user_id'] = empty($data['user_id']) ? null : $data['user_id']; + + if (! empty($data['organization_name'])) { + $organization = $this->fetchOrCreateOrganizationByName($data['organization_name']); + + $data['organization_id'] = $organization->id; + + unset($data['organization_name']); + } + + $person = parent::update($data, $id); + + /** + * If attributes are provided then only save the provided attributes and return. + */ + if (! empty($attributes)) { + $conditions = ['entity_type' => $data['entity_type']]; + + if (isset($data['quick_add'])) { + $conditions['quick_add'] = 1; + } + + $attributes = $this->attributeRepository->where($conditions) + ->whereIn('code', $attributes) + ->get(); + + $this->attributeValueRepository->save(array_merge($data, [ + 'entity_id' => $person->id, + ]), $attributes); + + return $person; + } + + $this->attributeValueRepository->save(array_merge($data, [ + 'entity_id' => $person->id, + ])); + + return $person; + } + + /** + * Retrieves customers count based on date. + * + * @return int + */ + public function getCustomerCount($startDate, $endDate) + { + return $this + ->whereBetween('created_at', [$startDate, $endDate]) + ->get() + ->count(); + } + + /** + * Fetch or create an organization. + */ + public function fetchOrCreateOrganizationByName(string $organizationName) + { + $organization = $this->organizationRepository->findOneWhere([ + 'name' => $organizationName, + ]); + + return $organization ?: $this->organizationRepository->create([ + 'entity_type' => 'organizations', + 'name' => $organizationName, + ]); + } + + /** + * Sanitize requested person data and return the clean array. + */ + private function sanitizeRequestedPersonData(array $data): array + { + if ( + array_key_exists('organization_id', $data) + && empty($data['organization_id']) + ) { + $data['organization_id'] = null; + } + + $uniqueIdParts = array_filter([ + $data['user_id'] ?? null, + $data['organization_id'] ?? null, + $data['emails'][0]['value'] ?? null, + ]); + + $data['unique_id'] = implode('|', $uniqueIdParts); + + if (isset($data['contact_numbers'])) { + $data['contact_numbers'] = collect($data['contact_numbers'])->filter(fn ($number) => ! is_null($number['value']))->toArray(); + + $data['unique_id'] .= '|'.$data['contact_numbers'][0]['value']; + } + + return $data; + } +} diff --git a/packages/Webkul/Core/composer.json b/packages/Webkul/Core/composer.json new file mode 100755 index 0000000..e7efca4 --- /dev/null +++ b/packages/Webkul/Core/composer.json @@ -0,0 +1,26 @@ +{ + "name": "krayin/laravel-core", + "license": "MIT", + "authors": [ + { + "name": "Jitendra Singh", + "email": "jitendra@webkul.com" + } + ], + "require": { + }, + "autoload": { + "psr-4": { + "Webkul\\Core\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Webkul\\Core\\Providers\\CoreServiceProvider" + ], + "aliases": {} + } + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/packages/Webkul/Core/src/Acl.php b/packages/Webkul/Core/src/Acl.php new file mode 100644 index 0000000..0ae8b3a --- /dev/null +++ b/packages/Webkul/Core/src/Acl.php @@ -0,0 +1,169 @@ +items[] = $aclItem; + } + + /** + * Get all acl items. + */ + public function getItems(): Collection + { + if (! $this->items) { + $this->prepareAclItems(); + } + + return collect($this->items) + ->sortBy('sort') + ->values(); + } + + /** + * Get the acl items the acting user is allowed to grant. + * + * A full administrator may grant any permission and sees the whole tree. Everyone else — mirroring + * "you cannot grant a permission you do not have" in Salesforce/HubSpot — sees only the permissions + * their own role holds, so a role they build is always a subset of their own and never rejected. + */ + public function getAuthorizedItems(): Collection + { + $user = auth()->guard('user')->user(); + + if ($user?->role?->permission_type === 'all') { + return $this->getItems(); + } + + return $this->filterItemsByPermissions($this->getItems(), $user?->role?->permissions ?? []); + } + + /** + * Recursively keep only the acl items whose key the user holds, preserving any branch that has an + * authorized descendant. + */ + private function filterItemsByPermissions(Collection $items, array $permissions): Collection + { + return $items + ->map(function (AclItem $item) use ($permissions) { + $children = $this->filterItemsByPermissions($item->children, $permissions); + + if (! in_array($item->key, $permissions) && $children->isEmpty()) { + return null; + } + + return new AclItem( + key: $item->key, + name: $item->name, + route: $item->route, + sort: $item->sort, + children: $children, + ); + }) + ->filter() + ->values(); + } + + /** + * Acl Config. + */ + private function getAclConfig(): array + { + static $aclConfig; + + if ($aclConfig) { + return $aclConfig; + } + + $aclConfig = config('acl'); + + return $aclConfig; + } + + /** + * Get all roles. + */ + public function getRoles(): Collection + { + static $roles; + + if ($roles) { + return $roles; + } + + $roles = collect($this->getAclConfig()) + ->mapWithKeys(function ($role) { + if (is_array($role['route'])) { + return collect($role['route'])->mapWithKeys(function ($route) use ($role) { + return [$route => $role['key']]; + }); + } else { + return [$role['route'] => $role['key']]; + } + }); + + return $roles; + } + + /** + * Prepare acl items. + */ + private function prepareAclItems(): void + { + $aclWithDotNotation = []; + + foreach ($this->getAclConfig() as $item) { + $aclWithDotNotation[$item['key']] = $item; + } + + $acl = Arr::undot(Arr::dot($aclWithDotNotation)); + + foreach ($acl as $aclItemKey => $aclItem) { + $subAclItems = $this->processSubAclItems($aclItem); + + $this->addItem(new AclItem( + key: $aclItemKey, + name: trans($aclItem['name']), + route: $aclItem['route'], + sort: $aclItem['sort'], + children: $subAclItems, + )); + } + } + + /** + * Process sub acl items. + */ + private function processSubAclItems($aclItem): Collection + { + return collect($aclItem) + ->sortBy('sort') + ->filter(fn ($value, $key) => is_array($value) && $key !== 'route') + ->map(function ($subAclItem) { + $subSubAclItems = $this->processSubAclItems($subAclItem); + + return new AclItem( + key: $subAclItem['key'], + name: trans($subAclItem['name']), + route: $subAclItem['route'], + sort: $subAclItem['sort'], + children: $subSubAclItems, + ); + }); + } +} diff --git a/packages/Webkul/Core/src/Acl/AclItem.php b/packages/Webkul/Core/src/Acl/AclItem.php new file mode 100644 index 0000000..66b50af --- /dev/null +++ b/packages/Webkul/Core/src/Acl/AclItem.php @@ -0,0 +1,19 @@ + [ + DataTransferModuleServiceProvider::class, + AdminModuleServiceProvider::class, + AttributeModuleServiceProvider::class, + AutomationModuleServiceProvider::class, + ContactModuleServiceProvider::class, + CoreModuleServiceProvider::class, + DataGridModuleServiceProvider::class, + EmailTemplateModuleServiceProvider::class, + EmailModuleServiceProvider::class, + LeadModuleServiceProvider::class, + ProductModuleServiceProvider::class, + QuoteModuleServiceProvider::class, + TagModuleServiceProvider::class, + UserModuleServiceProvider::class, + WarehouseModuleServiceProvider::class, + WebFormModuleServiceProvider::class, + DataTransferModuleServiceProvider::class, + ], + + 'register_route_models' => true, +]; diff --git a/packages/Webkul/Core/src/Config/cors.php b/packages/Webkul/Core/src/Config/cors.php new file mode 100644 index 0000000..4222c11 --- /dev/null +++ b/packages/Webkul/Core/src/Config/cors.php @@ -0,0 +1,62 @@ + [ + 'admin/web-forms/forms/*', + ], + + /* + * Matches the request method. `['*']` allows all methods. + */ + 'allowed_methods' => ['*'], + + /* + * Matches the request origin. `['*']` allows all origins. Wildcards can be used, eg `*.mydomain.com` + */ + 'allowed_origins' => ['*'], + + /* + * Patterns that can be used with `preg_match` to match the origin. + */ + 'allowed_origins_patterns' => [], + + /* + * Sets the Access-Control-Allow-Headers response header. `['*']` allows all headers. + */ + 'allowed_headers' => ['*'], + + /* + * Sets the Access-Control-Expose-Headers response header with these headers. + */ + 'exposed_headers' => [], + + /* + * Sets the Access-Control-Max-Age response header when > 0. + */ + 'max_age' => 0, + + /* + * Sets the Access-Control-Allow-Credentials header. + */ + 'supports_credentials' => false, +]; diff --git a/packages/Webkul/Core/src/Config/sanctum.php b/packages/Webkul/Core/src/Config/sanctum.php new file mode 100644 index 0000000..f801168 --- /dev/null +++ b/packages/Webkul/Core/src/Config/sanctum.php @@ -0,0 +1,68 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '' + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['user'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. If this value is null, personal access tokens do + | not expire. This won't tweak the lifetime of first-party sessions. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'verify_csrf_token' => VerifyCsrfToken::class, + 'encrypt_cookies' => EncryptCookies::class, + ], + +]; diff --git a/packages/Webkul/Core/src/Console/Commands/Version.php b/packages/Webkul/Core/src/Console/Commands/Version.php new file mode 100644 index 0000000..5f88378 --- /dev/null +++ b/packages/Webkul/Core/src/Console/Commands/Version.php @@ -0,0 +1,42 @@ +comment('v'.core()->version()); + } +} diff --git a/packages/Webkul/Core/src/Contracts/CoreConfig.php b/packages/Webkul/Core/src/Contracts/CoreConfig.php new file mode 100644 index 0000000..6d89520 --- /dev/null +++ b/packages/Webkul/Core/src/Contracts/CoreConfig.php @@ -0,0 +1,5 @@ + $attribute])); + } + } +} diff --git a/packages/Webkul/Core/src/Core.php b/packages/Webkul/Core/src/Core.php new file mode 100644 index 0000000..a5aca21 --- /dev/null +++ b/packages/Webkul/Core/src/Core.php @@ -0,0 +1,247 @@ + $title) { + $options[] = [ + 'title' => $title, + 'value' => $key, + ]; + } + + return $options; + } + + /** + * Retrieve all countries. + * + * @return Collection + */ + public function countries() + { + return $this->countryRepository->all(); + } + + /** + * Returns country name by code. + */ + public function country_name(string $code): string + { + $country = $this->countryRepository->findOneByField('code', $code); + + return $country ? $country->name : ''; + } + + /** + * Returns state name by code. + */ + public function state_name(string $code): string + { + $state = $this->countryStateRepository->findOneByField('code', $code); + + return $state ? $state->name : $code; + } + + /** + * Retrieve all country states. + * + * @return Collection + */ + public function states(string $countryCode) + { + return $this->countryStateRepository->findByField('country_code', $countryCode); + } + + /** + * Retrieve all grouped states by country code. + * + * @return Collection + */ + public function groupedStatesByCountries() + { + $collection = []; + + foreach ($this->countryStateRepository->all() as $state) { + $collection[$state->country_code][] = $state->toArray(); + } + + return $collection; + } + + /** + * Retrieve all grouped states by country code. + * + * @return Collection + */ + public function findStateByCountryCode($countryCode = null, $stateCode = null) + { + $collection = []; + + $collection = $this->countryStateRepository->findByField([ + 'country_code' => $countryCode, + 'code' => $stateCode, + ]); + + if (count($collection)) { + return $collection->first(); + } else { + return false; + } + } + + /** + * Create singleton object through single facade. + * + * @param string $className + * @return mixed + */ + public function getSingletonInstance($className) + { + static $instances = []; + + if (array_key_exists($className, $instances)) { + return $instances[$className]; + } + + return $instances[$className] = app($className); + } + + /** + * Format date + * + * @return string + */ + public function formatDate($date, $format = 'd M Y h:iA') + { + return Carbon::parse($date)->format($format); + } + + /** + * Week range. + * + * @param string $date + * @param int $day + * @return string + */ + public function xWeekRange($date, $day) + { + $ts = strtotime($date); + + if (! $day) { + $start = (date('D', $ts) == 'Sun') ? $ts : strtotime('last sunday', $ts); + + return date('Y-m-d', $start); + } else { + $end = (date('D', $ts) == 'Sat') ? $ts : strtotime('next saturday', $ts); + + return date('Y-m-d', $end); + } + } + + /** + * Return currency symbol from currency code. + * + * @param float $price + * @return string + */ + public function currencySymbol($code) + { + $formatter = new \NumberFormatter(app()->getLocale().'@currency='.$code, \NumberFormatter::CURRENCY); + + return $formatter->getSymbol(\NumberFormatter::CURRENCY_SYMBOL); + } + + /** + * Format price with base currency symbol. This method also give ability to encode + * the base currency symbol and its optional. + * + * @param float $price + * @return string + */ + public function formatBasePrice($price) + { + if (is_null($price)) { + $price = 0; + } + + $formatter = new \NumberFormatter(app()->getLocale(), \NumberFormatter::CURRENCY); + + return $formatter->formatCurrency($price, config('app.currency')); + } + + /** + * Get the config field. + */ + public function getConfigField(string $fieldName): ?array + { + return system_config()->getConfigField($fieldName); + } + + /** + * Retrieve information for configuration. + */ + public function getConfigData(string $field): mixed + { + return system_config()->getConfigData($field); + } +} diff --git a/packages/Webkul/Core/src/Database/Migrations/2021_03_12_060658_create_core_config_table.php b/packages/Webkul/Core/src/Database/Migrations/2021_03_12_060658_create_core_config_table.php new file mode 100755 index 0000000..9c5f6cc --- /dev/null +++ b/packages/Webkul/Core/src/Database/Migrations/2021_03_12_060658_create_core_config_table.php @@ -0,0 +1,33 @@ +increments('id'); + $table->string('code'); + $table->string('value'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('core_config'); + } +}; diff --git a/packages/Webkul/Core/src/Database/Migrations/2021_04_12_173232_create_countries_table.php b/packages/Webkul/Core/src/Database/Migrations/2021_04_12_173232_create_countries_table.php new file mode 100644 index 0000000..274e574 --- /dev/null +++ b/packages/Webkul/Core/src/Database/Migrations/2021_04_12_173232_create_countries_table.php @@ -0,0 +1,32 @@ +increments('id'); + $table->string('code'); + $table->string('name'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('countries'); + } +}; diff --git a/packages/Webkul/Core/src/Database/Migrations/2021_04_12_173344_create_country_states_table.php b/packages/Webkul/Core/src/Database/Migrations/2021_04_12_173344_create_country_states_table.php new file mode 100644 index 0000000..5939026 --- /dev/null +++ b/packages/Webkul/Core/src/Database/Migrations/2021_04_12_173344_create_country_states_table.php @@ -0,0 +1,36 @@ +increments('id'); + $table->string('country_code'); + $table->string('code'); + $table->string('name'); + + $table->integer('country_id')->unsigned(); + $table->foreign('country_id')->references('id')->on('countries')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('country_states'); + } +}; diff --git a/packages/Webkul/Core/src/Database/Migrations/2025_01_29_133500_update_text_column_type_in_core_config_table.php b/packages/Webkul/Core/src/Database/Migrations/2025_01_29_133500_update_text_column_type_in_core_config_table.php new file mode 100644 index 0000000..2312701 --- /dev/null +++ b/packages/Webkul/Core/src/Database/Migrations/2025_01_29_133500_update_text_column_type_in_core_config_table.php @@ -0,0 +1,28 @@ +text('value')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('core_config', function (Blueprint $table) { + $table->string('value')->change(); + }); + } +}; diff --git a/packages/Webkul/Core/src/Eloquent/Repository.php b/packages/Webkul/Core/src/Eloquent/Repository.php new file mode 100755 index 0000000..fdce1a7 --- /dev/null +++ b/packages/Webkul/Core/src/Eloquent/Repository.php @@ -0,0 +1,146 @@ +findByField($field, $value, $columns = ['*']); + + return $model->first(); + } + + /** + * Find data by field and value + * + * @param string $field + * @param string $value + * @param array $columns + * @return mixed + */ + public function findOneWhere(array $where, $columns = ['*']) + { + $model = $this->findWhere($where, $columns); + + return $model->first(); + } + + /** + * Find data by id + * + * @param int $id + * @param array $columns + * @return mixed + */ + public function find($id, $columns = ['*']) + { + $this->applyCriteria(); + $this->applyScope(); + $model = $this->model->find($id, $columns); + $this->resetModel(); + + return $this->parserResult($model); + } + + /** + * Find data by id + * + * @param int $id + * @param array $columns + * @return mixed + */ + public function findOrFail($id, $columns = ['*']) + { + $this->applyCriteria(); + $this->applyScope(); + $model = $this->model->findOrFail($id, $columns); + $this->resetModel(); + + return $this->parserResult($model); + } + + /** + * Count results of repository + * + * @param string $columns + * @return int + */ + public function count(array $where = [], $columns = '*') + { + $this->applyCriteria(); + $this->applyScope(); + + if ($where) { + $this->applyConditions($where); + } + + $result = $this->model->count($columns); + $this->resetModel(); + $this->resetScope(); + + return $result; + } + + /** + * @param string $columns + * @return mixed + */ + public function sum($columns) + { + $this->applyCriteria(); + $this->applyScope(); + + $sum = $this->model->sum($columns); + $this->resetModel(); + + return $sum; + } + + /** + * @param string $columns + * @return mixed + */ + public function avg($columns) + { + $this->applyCriteria(); + $this->applyScope(); + + $avg = $this->model->avg($columns); + $this->resetModel(); + + return $avg; + } + + /** + * @return mixed + */ + public function getModel($data = []) + { + return $this->model; + } + + /** + * @throws RepositoryException + */ + public function resetModel() + { + $this->makeModel(); + + return $this; + } +} diff --git a/packages/Webkul/Core/src/Eloquent/TranslatableModel.php b/packages/Webkul/Core/src/Eloquent/TranslatableModel.php new file mode 100755 index 0000000..8d824c7 --- /dev/null +++ b/packages/Webkul/Core/src/Eloquent/TranslatableModel.php @@ -0,0 +1,23 @@ +defaultLocale) { + return $this->defaultLocale; + } + + return config('translatable.locale') ?: app()->make('translator')->getLocale(); + } +} diff --git a/packages/Webkul/Core/src/Exceptions/ViterNotFound.php b/packages/Webkul/Core/src/Exceptions/ViterNotFound.php new file mode 100755 index 0000000..5a7071a --- /dev/null +++ b/packages/Webkul/Core/src/Exceptions/ViterNotFound.php @@ -0,0 +1,17 @@ +getLocale(); + + $path = __DIR__."/../../../$packageName/src/Resources/lang/$currentLocale/app.php"; + + if (is_string($path) && is_readable($path)) { + return include $path; + } else { + $currentLocale = 'en'; + + $path = __DIR__."/../../../$packageName/src/Resources/lang/$currentLocale/app.php"; + + return include $path; + } + } +} diff --git a/packages/Webkul/Core/src/Http/helpers.php b/packages/Webkul/Core/src/Http/helpers.php new file mode 100755 index 0000000..9ab57a1 --- /dev/null +++ b/packages/Webkul/Core/src/Http/helpers.php @@ -0,0 +1,74 @@ +singleton(ViewRenderEventManager::class); + + $viewEventManager = app()->make(ViewRenderEventManager::class); + + $viewEventManager->handleRenderEvent($eventName, $params); + + return $viewEventManager->render(); + } +} + +if (! function_exists('vite')) { + /** + * Vite helper. + */ + function vite(): Vite + { + return app(Vite::class); + } +} diff --git a/packages/Webkul/Core/src/Menu.php b/packages/Webkul/Core/src/Menu.php new file mode 100644 index 0000000..dfde788 --- /dev/null +++ b/packages/Webkul/Core/src/Menu.php @@ -0,0 +1,239 @@ +items[] = $menuItem; + } + + /** + * Get all menu items. + */ + public function getItems(?string $area = null, string $key = ''): Collection + { + if (! $area) { + throw new \Exception('Area must be provided to get menu items.'); + } + + static $items; + + if ($items) { + return $items; + } + + $configMenu = collect(config("menu.$area"))->map(function ($item) { + return Arr::except([ + ...$item, + 'url' => route($item['route'], $item['params'] ?? []), + ], ['params']); + }); + + switch ($area) { + case self::ADMIN: + $this->configMenu = $configMenu + ->filter(fn ($item) => bouncer()->hasPermission($item['key'])) + ->toArray(); + break; + + default: + $this->configMenu = $configMenu->toArray(); + + break; + } + + if (! $this->items) { + $this->prepareMenuItems(); + } + + $items = collect($this->items)->sortBy(fn ($item) => $item->getPosition()); + + return $items; + } + + /** + * Get admin menu by key or keys. + */ + public function getAdminMenuByKey(array|string $keys): mixed + { + $items = $this->getItems('admin'); + + $keysArray = (array) $keys; + + $filteredItems = $items->filter(fn ($item) => in_array($item->getKey(), $keysArray)); + + return is_array($keys) ? $filteredItems : $filteredItems->first(); + } + + /** + * Resolve the display label for a menu key. + * + * Honours the rename configured under `general.settings.menu`, falling back + * to the given translation key when no custom name has been set. Every + * surface that shows a menu label must go through here so a rename applies + * consistently, not just in the sidebar. + */ + public function getLabel(string $key, ?string $fallbackTransKey = null): string + { + if ($this->configuredNames === null) { + $this->configuredNames = $this->loadConfiguredNames(); + } + + if (filled($this->configuredNames[$key] ?? null)) { + return $this->configuredNames[$key]; + } + + return $fallbackTransKey ? trans($fallbackTransKey) : ''; + } + + /** + * Load every custom menu name in a single query, keyed by menu key. + */ + private function loadConfiguredNames(): array + { + $prefix = 'general.settings.menu.'; + + return $this->coreConfigRepository + ->findWhere([['code', 'like', $prefix.'%']], ['code', 'value']) + ->mapWithKeys(fn ($config) => [Str::after($config->code, $prefix) => $config->value]) + ->all(); + } + + /** + * Prepare menu items. + */ + private function prepareMenuItems(): void + { + $menuWithDotNotation = []; + + foreach ($this->configMenu as $item) { + if (strpos(request()->url(), route($item['route'])) !== false) { + $this->currentKey = $item['key']; + } + + $menuWithDotNotation[$item['key']] = $item; + } + + $menu = Arr::undot(Arr::dot($menuWithDotNotation)); + + foreach ($menu as $menuItemKey => $menuItem) { + $this->addItem(new MenuItem( + key: $menuItemKey, + name: $this->getLabel($menuItemKey, $menuItem['name']), + route: $menuItem['route'], + url: $menuItem['url'], + sort: $menuItem['sort'], + icon: $menuItem['icon-class'], + info: trans($menuItem['info'] ?? ''), + children: $this->processSubMenuItems($menuItem), + )); + } + } + + /** + * Process sub menu items. + */ + private function processSubMenuItems($menuItem): Collection + { + return collect($menuItem) + ->sortBy('sort') + ->filter(fn ($value) => is_array($value)) + ->map(function ($subMenuItem) { + $subSubMenuItems = $this->processSubMenuItems($subMenuItem); + + return new MenuItem( + key: $subMenuItem['key'], + name: $this->getLabel($subMenuItem['key'], $subMenuItem['name']), + route: $subMenuItem['route'], + url: $subMenuItem['url'], + sort: $subMenuItem['sort'], + icon: $subMenuItem['icon-class'], + info: trans($subMenuItem['info'] ?? ''), + children: $subSubMenuItems, + ); + }); + } + + /** + * Get current active menu. + */ + public function getCurrentActiveMenu(?string $area = null): ?MenuItem + { + $currentKey = implode('.', array_slice(explode('.', $this->currentKey), 0, 2)); + + return $this->findMatchingItem($this->getItems($area), $currentKey); + } + + /** + * Finding the matching item. + */ + private function findMatchingItem($items, $currentKey): ?MenuItem + { + foreach ($items as $item) { + if ($item->key == $currentKey) { + return $item; + } + + if ($item->haveChildren()) { + $matchingChild = $this->findMatchingItem($item->getChildren(), $currentKey); + + if ($matchingChild) { + return $matchingChild; + } + } + } + + return null; + } +} diff --git a/packages/Webkul/Core/src/Menu/MenuItem.php b/packages/Webkul/Core/src/Menu/MenuItem.php new file mode 100644 index 0000000..c185488 --- /dev/null +++ b/packages/Webkul/Core/src/Menu/MenuItem.php @@ -0,0 +1,200 @@ +name = $name; + + return $this; + } + + /** + * Get name of menu item. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Set position of menu item. + */ + public function setPosition(int $sort): self + { + $this->sort = $sort; + + return $this; + } + + /** + * Get position of menu item. + */ + public function getPosition() + { + return $this->sort; + } + + /** + * Set icon of menu item. + */ + public function setIcon(string $icon): self + { + $this->icon = $icon; + + return $this; + } + + /** + * Get the icon of menu item. + */ + public function getIcon(): string + { + return $this->icon; + } + + /** + * Set info of menu item. + */ + public function setInfo(string $info): self + { + $this->info = $info; + + return $this; + } + + /** + * Get info of menu item. + */ + public function getInfo(): string + { + return $this->info; + } + + /** + * Set route of menu item. + */ + public function setRoute(string $route): self + { + $this->route = $route; + + return $this; + } + + /** + * Get current route. + */ + public function getRoute(): string + { + return $this->route; + } + + /** + * Set url of menu item. + */ + public function setUrl(string $url): self + { + $this->url = $url; + + return $this; + } + + /** + * Get the url of the menu item. + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * Set the key of the menu item. + */ + public function setKey(string $key): self + { + $this->key = $key; + + return $this; + } + + /** + * Get the key of the menu item. + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Set children of menu item. + */ + public function setChildren(Collection $children): self + { + $this->children = $children; + + return $this; + } + + /** + * Check weather menu item have children or not. + */ + public function haveChildren(): bool + { + return $this->children->isNotEmpty(); + } + + /** + * Get children of menu item. + */ + public function getChildren(): Collection + { + if (! $this->haveChildren()) { + return collect(); + } + + return $this->children; + } + + /** + * Check weather menu item is active or not. + */ + public function isActive(): bool + { + if (request()->fullUrlIs($this->getUrl().'*')) { + return true; + } + + if ($this->haveChildren()) { + foreach ($this->getChildren() as $child) { + if ($child->isActive()) { + return true; + } + } + } + + return false; + } +} diff --git a/packages/Webkul/Core/src/Models/CoreConfig.php b/packages/Webkul/Core/src/Models/CoreConfig.php new file mode 100755 index 0000000..dca3c85 --- /dev/null +++ b/packages/Webkul/Core/src/Models/CoreConfig.php @@ -0,0 +1,24 @@ +areMigrationsEnabled()) { + $this->registerMigrations(); + } + + if ($this->areModelsEnabled()) { + $this->registerModels(); + $this->registerEnums(); + $this->registerRequestTypes(); + } + + if ($this->areViewsEnabled()) { + $this->registerViews(); + } + + if ($routes = $this->config('routes', true)) { + $this->registerRoutes($routes); + } + } +} diff --git a/packages/Webkul/Core/src/Providers/CoreServiceProvider.php b/packages/Webkul/Core/src/Providers/CoreServiceProvider.php new file mode 100644 index 0000000..13695b7 --- /dev/null +++ b/packages/Webkul/Core/src/Providers/CoreServiceProvider.php @@ -0,0 +1,89 @@ +loadMigrationsFrom(__DIR__.'/../Database/Migrations'); + + $this->publishes([ + dirname(__DIR__).'/Config/concord.php' => config_path('concord.php'), + dirname(__DIR__).'/Config/cors.php' => config_path('cors.php'), + dirname(__DIR__).'/Config/sanctum.php' => config_path('sanctum.php'), + ]); + } + + /** + * Register services. + * + * @return void + */ + public function register() + { + $this->registerCommands(); + + $this->registerFacades(); + } + + /** + * Register Bouncer as a singleton. + * + * @return void + */ + protected function registerFacades() + { + $loader = AliasLoader::getInstance(); + + $loader->alias('acl', AclFacade::class); + + $loader->alias('core', CoreFacade::class); + + $loader->alias('system_config', SystemConfigFacade::class); + + $loader->alias('menu', MenuFacade::class); + + $this->app->singleton('acl', fn () => app(Acl::class)); + + $this->app->singleton('core', fn () => app(Core::class)); + + $this->app->singleton('system_config', fn () => app()->make(SystemConfig::class)); + + $this->app->singleton('menu', fn () => app()->make(Menu::class)); + } + + /** + * Register the console commands of this package. + */ + protected function registerCommands(): void + { + if ($this->app->runningInConsole()) { + $this->commands([ + Version::class, + ]); + } + } +} diff --git a/packages/Webkul/Core/src/Providers/ModuleServiceProvider.php b/packages/Webkul/Core/src/Providers/ModuleServiceProvider.php new file mode 100644 index 0000000..3f1caf4 --- /dev/null +++ b/packages/Webkul/Core/src/Providers/ModuleServiceProvider.php @@ -0,0 +1,16 @@ +getTitle()) + ) { + return trans($configuration->getTitle()); + } + + if ( + method_exists($configuration, 'getName') + && ! is_null($configuration->getName()) + ) { + return trans($configuration->getName()); + } + + return ''; + } + + /** + * Get children and fields. + */ + protected function getChildrenAndFields(mixed $configuration, string $searchTerm, array $path, array &$results): void + { + if ( + method_exists($configuration, 'getChildren') + || method_exists($configuration, 'getFields') + ) { + $children = $configuration->haveChildren() + ? $configuration->getChildren() + : $configuration->getFields(); + + $tempPath = array_merge($path, [[ + 'key' => $configuration->getKey() ?? null, + 'title' => $this->getTranslatedTitle($configuration), + ]]); + + $results = array_merge($results, $this->search($children, $searchTerm, $tempPath)); + } + } + + /** + * Search configuration. + * + * @param array $items + */ + public function search(Collection $items, string $searchTerm, array $path = []): array + { + $results = []; + + foreach ($items as $configuration) { + $title = $this->getTranslatedTitle($configuration); + + if ( + stripos($title, $searchTerm) !== false + && count($path) + ) { + $queryParam = $path[1]['key'] ?? $configuration->getKey(); + + $results[] = [ + 'title' => implode(' > ', [...Arr::pluck($path, 'title'), $title]), + 'url' => route('admin.configuration.index', Str::replace('.', '/', $queryParam)), + ]; + } + + $this->getChildrenAndFields($configuration, $searchTerm, $path, $results); + } + + return $results; + } + + /** + * Create core configuration. + */ + public function create(array $data): void + { + unset($data['_token']); + + $preparedData = []; + + foreach ($data as $method => $fieldData) { + $recursiveData = $this->recursiveArray($fieldData, $method); + + foreach ($recursiveData as $fieldName => $value) { + if ( + is_array($value) + && isset($value['delete']) + ) { + $coreConfigValues = $this->model->where('code', $fieldName)->get(); + + if ($coreConfigValues->isNotEmpty()) { + foreach ($coreConfigValues as $coreConfig) { + if (! empty($coreConfig['value'])) { + Storage::delete($coreConfig['value']); + } + + parent::delete($coreConfig['id']); + } + } + + continue; + } + } + + foreach ($recursiveData as $fieldName => $value) { + if (is_array($value)) { + foreach ($value as $key => $val) { + $fieldNameWithKey = $fieldName.'.'.$key; + + $coreConfigValues = $this->model->where('code', $fieldNameWithKey)->get(); + if (request()->hasFile($fieldNameWithKey)) { + $uploadedFile = request()->file($fieldNameWithKey); + + $val = $uploadedFile->store('configuration'); + + /** + * Nested configuration fields accept uploads on the same terms as the + * flat ones below, so they need the same SVG sanitization; without it an + * SVG carrying script was stored and later served untouched. + */ + $this->sanitizeSVG($val, $uploadedFile); + } else { + $val = $this->sanitizeConfigValue($fieldNameWithKey, $val); + } + + if ($coreConfigValues->isNotEmpty()) { + foreach ($coreConfigValues as $coreConfig) { + if (request()->hasFile($fieldNameWithKey)) { + Storage::delete($coreConfig['value']); + } + + parent::update(['code' => $fieldNameWithKey, 'value' => $val], $coreConfig->id); + } + } else { + parent::create(['code' => $fieldNameWithKey, 'value' => $val]); + } + } + } else { + /** + * Reset per field: $path persists across iterations otherwise, and a later field + * with no upload would be saved with the previous field's file path as its value. + */ + $path = null; + + if (request()->hasFile($fieldName)) { + $file = request()->file($fieldName); + + $filename = md5($file->getClientOriginalName().time()).'.'.$file->getClientOriginalExtension(); + + $path = $file->storeAs('configuration', $filename); + + $this->sanitizeSVG($path, $file); + } + + $preparedData[] = [ + 'code' => $fieldName, + 'value' => $path ?? $this->sanitizeConfigValue($fieldName, $value), + ]; + } + } + } + + if (! empty($preparedData)) { + foreach ($preparedData as $dataItem) { + $coreConfigValues = $this->model->where('code', $dataItem['code'])->get(); + + if ($coreConfigValues->isNotEmpty()) { + foreach ($coreConfigValues as $coreConfig) { + parent::update($dataItem, $coreConfig->id); + } + } else { + parent::create($dataItem); + } + } + } + + Event::dispatch('core.configuration.save.after'); + } + + /** + * Sanitize a configuration value before it is persisted. + * + * Rich-text (`editor`/TinyMCE) fields legitimately hold HTML and are rendered unescaped, so their + * value is passed through an allowlist HTML sanitizer to strip any active content. This is the + * server-side boundary that client-side TinyMCE filtering does not provide. + */ + protected function sanitizeConfigValue(string $code, mixed $value): mixed + { + if (! is_string($value)) { + return $value; + } + + $field = core()->getConfigField($code); + + if ( + $field + && (($field['type'] ?? null) === 'editor' || ! empty($field['tinymce'])) + ) { + return $this->sanitizeHtml($value); + } + + return $value; + } + + /** + * Recursive array. + */ + public function recursiveArray(array $formData, string $method): array + { + static $data = []; + + static $recursiveArrayData = []; + + foreach ($formData as $form => $formValue) { + $value = $method.'.'.$form; + + if (is_array($formValue)) { + $dim = $this->countDim($formValue); + + if ($dim > 1) { + $this->recursiveArray($formValue, $value); + } elseif ($dim == 1) { + $data[$value] = $formValue; + } + } + } + + foreach ($data as $key => $value) { + $field = core()->getConfigField($key); + + if ($field) { + $recursiveArrayData[$key] = $value; + } else { + foreach ($value as $key1 => $val) { + $recursiveArrayData[$key.'.'.$key1] = $val; + } + } + } + + return $recursiveArrayData; + } + + /** + * Return dimension of the array. + */ + public function countDim(array|string $array): int + { + if (is_array(reset($array))) { + $return = $this->countDim(reset($array)) + 1; + } else { + $return = 1; + } + + return $return; + } +} diff --git a/packages/Webkul/Core/src/Repositories/CountryRepository.php b/packages/Webkul/Core/src/Repositories/CountryRepository.php new file mode 100755 index 0000000..17b52c3 --- /dev/null +++ b/packages/Webkul/Core/src/Repositories/CountryRepository.php @@ -0,0 +1,21 @@ +items[] = $item; + } + + /** + * Get all configuration items. + */ + public function getItems(): Collection + { + if (! $this->items) { + $this->prepareConfigurationItems(); + } + + return collect($this->items) + ->sortBy('sort'); + } + + /** + * Retrieve Core Config + */ + private function retrieveCoreConfig(): array + { + static $items; + + if ($items) { + return $items; + } + + return $items = config('core_config'); + } + + /** + * Prepare configuration items. + */ + public function prepareConfigurationItems() + { + $configWithDotNotation = []; + + foreach ($this->retrieveCoreConfig() as $item) { + $configWithDotNotation[$item['key']] = $item; + } + + $configs = Arr::undot(Arr::dot($configWithDotNotation)); + + foreach ($configs as $configItem) { + $subConfigItems = $this->processSubConfigItems($configItem); + + $this->addItem(new Item( + children: $subConfigItems, + fields: $configItem['fields'] ?? null, + icon: $configItem['icon'] ?? null, + key: $configItem['key'], + name: trans($configItem['name']), + route: $configItem['route'] ?? null, + info: trans($configItem['info']) ?? null, + sort: $configItem['sort'], + )); + } + } + + /** + * Process sub config items. + */ + private function processSubConfigItems($configItem): Collection + { + return collect($configItem) + ->sortBy('sort') + ->filter(fn ($value) => is_array($value) && isset($value['name'])) + ->map(function ($subConfigItem) { + $configItemChildren = $this->processSubConfigItems($subConfigItem); + + return new Item( + children: $configItemChildren, + fields: $subConfigItem['fields'] ?? null, + icon: $subConfigItem['icon'] ?? null, + key: $subConfigItem['key'], + name: trans($subConfigItem['name']), + info: trans($subConfigItem['info']) ?? null, + route: $subConfigItem['route'] ?? null, + sort: $subConfigItem['sort'] ?? null, + ); + }); + } + + /** + * Get active configuration item. + */ + public function getActiveConfigurationItem(): ?Item + { + if (! $slug = request()->route('slug')) { + return null; + } + + $activeItem = $this->getItems()->where('key', $slug)->first() ?? null; + + if (! $activeItem) { + return null; + } + + if ($slug2 = request()->route('slug2')) { + $activeItem = $activeItem->getChildren()[$slug2]; + } + + return $activeItem; + } + + /** + * Get config field. + */ + public function getConfigField(string $fieldName): ?array + { + foreach ($this->retrieveCoreConfig() as $coreData) { + if (! isset($coreData['fields'])) { + continue; + } + + foreach ($coreData['fields'] as $field) { + $name = $coreData['key'].'.'.$field['name']; + + if ($name == $fieldName) { + return $field; + } + } + } + + return null; + } + + /** + * Get default config. + */ + private function getDefaultConfig(string $field): mixed + { + $configFieldInfo = $this->getConfigField($field); + + $fields = explode('.', $field); + + array_shift($fields); + + $field = implode('.', $fields); + + return Config::get($field, $configFieldInfo['default'] ?? null); + } + + /** + * Retrieve information for configuration + */ + public function getConfigData(string $field): mixed + { + $coreConfigValue = $this->coreConfigRepository->findOneWhere([ + 'code' => $field, + ]); + + if (! $coreConfigValue) { + return $this->getDefaultConfig($field); + } + + return $coreConfigValue->value; + } +} diff --git a/packages/Webkul/Core/src/SystemConfig/Item.php b/packages/Webkul/Core/src/SystemConfig/Item.php new file mode 100644 index 0000000..5cc096c --- /dev/null +++ b/packages/Webkul/Core/src/SystemConfig/Item.php @@ -0,0 +1,123 @@ +name ?? ''; + } + + /** + * Format options. + */ + private function formatOptions($options) + { + return is_array($options) ? $options : (is_string($options) ? $options : []); + } + + /** + * Get fields of config item. + */ + public function getFields(): Collection + { + return collect($this->fields)->map(function ($field) { + return new ItemField( + item_key: $this->key, + name: $field['name'], + title: $field['title'], + info: $field['info'] ?? null, + type: $field['type'], + depends: $field['depends'] ?? null, + path: $field['path'] ?? null, + validation: $field['validation'] ?? null, + default: $field['default'] ?? null, + channel_based: $field['channel_based'] ?? null, + locale_based: $field['locale_based'] ?? null, + options: $this->formatOptions($field['options'] ?? null), + tinymce: $field['tinymce'] ?? false, + is_visible: true, + ); + }); + } + + /** + * Get name of config item. + */ + public function getInfo(): ?string + { + return $this->info; + } + + /** + * Get current route. + */ + public function getRoute(): string + { + return $this->route; + } + + /** + * Get the url of the config item. + */ + public function getUrl(): string + { + return route($this->getRoute()); + } + + /** + * Get the key of the config item. + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Get Icon. + */ + public function getIcon(): ?string + { + return $this->icon; + } + + /** + * Check weather config item have children or not. + */ + public function haveChildren(): bool + { + return $this->children->isNotEmpty(); + } + + /** + * Get children of config item. + */ + public function getChildren(): Collection + { + if (! $this->haveChildren()) { + return collect(); + } + + return $this->children; + } +} diff --git a/packages/Webkul/Core/src/SystemConfig/ItemField.php b/packages/Webkul/Core/src/SystemConfig/ItemField.php new file mode 100644 index 0000000..cad44f8 --- /dev/null +++ b/packages/Webkul/Core/src/SystemConfig/ItemField.php @@ -0,0 +1,244 @@ + 'min_value', + ]; + + /** + * Create a new ItemField instance. + */ + public function __construct( + public string $item_key, + public string $name, + public string $title, + public ?string $info, + public string $type, + public ?string $path, + public ?string $validation, + public ?string $depends, + public ?string $default, + public ?bool $channel_based, + public ?bool $locale_based, + public array|string $options, + public bool $is_visible = true, + public bool $tinymce = false, + ) { + $this->options = $this->getOptions(); + } + + /** + * Get name of config item. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Get info of config item. + */ + public function getInfo(): ?string + { + return $this->info ?? ''; + } + + /** + * Get title of config item. + */ + public function getTitle(): ?string + { + return $this->title ?? ''; + } + + /** + * Determine if the field should use TinyMCE. + */ + public function getTinymce(): bool + { + return $this->tinymce; + } + + /** + * Get type of config item. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get path of config item. + */ + public function getPath(): ?string + { + return $this->path; + } + + /** + * Get item key of config item. + */ + public function getItemKey(): string + { + return $this->item_key; + } + + /** + * Get validation of config item. + */ + public function getValidations(): ?string + { + if (empty($this->validation)) { + return ''; + } + + foreach ($this->veeValidateMappings as $laravelRule => $veeValidateRule) { + $this->validation = str_replace($laravelRule, $veeValidateRule, $this->validation); + } + + return $this->validation; + } + + /** + * Get depends of config item. + */ + public function getDepends(): ?string + { + return $this->depends; + } + + /** + * Get default value of config item. + */ + public function getDefault(): ?string + { + return $this->default; + } + + /** + * Get channel based of config item. + */ + public function getChannelBased(): ?bool + { + return $this->channel_based; + } + + /** + * Get locale based of config item. + */ + public function getLocaleBased(): ?bool + { + return $this->locale_based; + } + + /** + * Get name field for forms in configuration page. + */ + public function getNameKey(): string + { + return $this->item_key.'.'.$this->name; + } + + /** + * Check if the field is required. + */ + public function isRequired(): string + { + return Str::contains($this->getValidations(), 'required') ? 'required' : ''; + } + + /** + * Get options of config item. + */ + public function getOptions(): array + { + if (is_array($this->options)) { + return collect($this->options)->map(fn ($option) => [ + 'title' => trans($option['title']), + 'value' => $option['value'], + ])->toArray(); + } + + return collect($this->getFieldOptions($this->options))->map(fn ($option) => [ + 'title' => trans($option['title']), + 'value' => $option['value'], + ])->toArray(); + } + + /** + * Convert the field to an array. + */ + public function toArray() + { + return [ + 'name' => $this->getName(), + 'title' => $this->getTitle(), + 'info' => $this->getInfo(), + 'type' => $this->getType(), + 'path' => $this->getPath(), + 'depends' => $this->getDepends(), + 'validation' => $this->getValidations(), + 'default' => $this->getDefault(), + 'channel_based' => $this->getChannelBased(), + 'locale_based' => $this->getLocaleBased(), + 'options' => $this->getOptions(), + 'item_key' => $this->getItemKey(), + 'tinymce' => $this->getTinymce(), + ]; + } + + /** + * Get name field for forms in configuration page. + * + * @param string $key + * @return string + */ + public function getNameField($key = null) + { + if (! $key) { + $key = $this->item_key.'.'.$this->name; + } + + $nameField = ''; + + foreach (explode('.', $key) as $key => $field) { + $nameField .= $key === 0 ? $field : '['.$field.']'; + } + + return $nameField; + } + + /** + * Get depend the field name. + */ + public function getDependFieldName(): string + { + if (empty($depends = $this->getDepends())) { + return ''; + } + + $dependNameKey = $this->getItemKey().'.'.collect(explode(':', $depends))->first(); + + return $this->getNameField($dependNameKey); + } + + /** + * Returns the select options for the field. + */ + protected function getFieldOptions(string $options): array + { + [$class, $method] = Str::parseCallback($options); + + return app($class)->$method(); + } +} diff --git a/packages/Webkul/Core/src/Traits/PDFHandler.php b/packages/Webkul/Core/src/Traits/PDFHandler.php new file mode 100644 index 0000000..bae2695 --- /dev/null +++ b/packages/Webkul/Core/src/Traits/PDFHandler.php @@ -0,0 +1,67 @@ +getLocale(), ['ar', 'he'])) { + $mPDF = new Mpdf([ + 'margin_left' => 0, + 'margin_right' => 0, + 'margin_top' => 0, + 'margin_bottom' => 0, + ]); + + $mPDF->SetDirectionality($direction); + + $mPDF->SetDisplayMode('fullpage'); + + $mPDF->WriteHTML($this->adjustArabicAndPersianContent($html)); + + return response()->streamDownload(fn () => print ($mPDF->Output('', 'S')), $fileName.'.pdf'); + } + + return Pdf::loadHTML($this->adjustArabicAndPersianContent($html)) + ->setPaper('A4', 'portrait') + ->set_option('defaultFont', 'Courier') + ->download($fileName.'.pdf'); + } + + /** + * Adjust arabic and persian content. + * + * @return string + */ + protected function adjustArabicAndPersianContent(string $html) + { + $arabic = new Arabic; + + $p = $arabic->arIdentify($html); + + for ($i = count($p) - 1; $i >= 0; $i -= 2) { + $utf8ar = $arabic->utf8Glyphs(substr($html, $p[$i - 1], $p[$i] - $p[$i - 1])); + $html = substr_replace($html, $utf8ar, $p[$i - 1], $p[$i] - $p[$i - 1]); + } + + return $html; + } +} diff --git a/packages/Webkul/Core/src/Traits/Sanitizer.php b/packages/Webkul/Core/src/Traits/Sanitizer.php new file mode 100644 index 0000000..ad3ddc1 --- /dev/null +++ b/packages/Webkul/Core/src/Traits/Sanitizer.php @@ -0,0 +1,140 @@ +set('Cache.DefinitionImpl', null); + + /** + * Preserve `target="_blank"` links (used by the default footer) while neutralising tab-nabbing. + */ + $config->set('Attr.AllowedFrameTargets', ['_blank']); + $config->set('HTML.TargetBlank', true); + + return (new HTMLPurifier($config))->purify($html); + } + + /** + * Sanitize an SVG file to remove potentially malicious content. + */ + public function sanitizeSvg(string $path, UploadedFile $file): void + { + try { + $svgContent = Storage::get($path); + + if (! $svgContent) { + return; + } + + /** + * The decision to sanitize must not rest on the client-supplied file name alone. An SVG + * uploaded as `payload.png` passes a name-based check while still being stored — and + * later served — as an SVG, so the markup itself and the stored path are consulted too. + */ + if ( + ! $this->isSvgFile($file) + && ! $this->isSvgPath($path) + && ! $this->hasSvgMarkup($svgContent) + ) { + return; + } + + $sanitizer = new MainSanitizer; + $sanitizer->setAllowedAttrs(new AllowedAttributes); + $sanitizer->setAllowedTags(new AllowedTags); + + $sanitizer->minify(true); + $sanitizer->removeRemoteReferences(true); + $sanitizer->removeXMLTag(true); + + $sanitizer->setXMLOptions(LIBXML_NONET | LIBXML_NOBLANKS); + + $sanitizedContent = $sanitizer->sanitize($svgContent); + + if ($sanitizedContent === false) { + $patterns = [ + '/]*>(.*?)<\/script>/is', + '/\bon\w+\s*=\s*["\'][^"\']*["\']/i', + '/javascript\s*:/i', + '/data\s*:[^,]*base64/i', + ]; + + $sanitizedContent = $svgContent; + + foreach ($patterns as $pattern) { + $sanitizedContent = preg_replace($pattern, '', $sanitizedContent); + } + + Storage::put($path, $sanitizedContent); + + return; + } + + $sanitizedContent = preg_replace('/(.*?<\/script>)|(\son\w+\s*=\s*["\'][^"\']*["\'])/is', '', $sanitizedContent); + + Storage::put($path, $sanitizedContent); + } catch (Exception $e) { + report($e->getMessage()); + + Storage::delete($path); + } + } + + /** + * Whether the client named the upload as an SVG. This is only one of the signals used, because + * the name is attacker-controlled; see isSvgPath() and hasSvgMarkup(). + */ + public function isSvgFile(UploadedFile $file): bool + { + return str_contains(strtolower((string) $file->getClientOriginalExtension()), 'svg'); + } + + /** + * Whether the path the file was actually stored under is an SVG. Upload handlers that derive the + * stored extension from the file's own content land here even when the client named it otherwise. + */ + public function isSvgPath(string $path): bool + { + return str_contains(strtolower((string) pathinfo($path, PATHINFO_EXTENSION)), 'svg'); + } + + /** + * Whether the stored bytes actually contain SVG markup, whatever the file is called. + */ + public function hasSvgMarkup(string $contents): bool + { + return (bool) preg_match('/<\s*svg\b/i', $contents); + } +} diff --git a/packages/Webkul/Core/src/ViewRenderEventManager.php b/packages/Webkul/Core/src/ViewRenderEventManager.php new file mode 100755 index 0000000..4c646b6 --- /dev/null +++ b/packages/Webkul/Core/src/ViewRenderEventManager.php @@ -0,0 +1,89 @@ +params = $params ?? []; + + Event::dispatch($eventName, $this); + + return $this->templates; + } + + /** + * get params + * + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * get param + * + * @return mixed + */ + public function getParam($name) + { + return optional($this->params)[$name]; + } + + /** + * Add templates for render + * + * @param string $template + * @return void + */ + public function addTemplate($template) + { + array_push($this->templates, $template); + } + + /** + * Renders templates + * + * @return string + */ + public function render() + { + $string = ''; + + foreach ($this->templates as $template) { + if (view()->exists($template)) { + $string .= view($template, $this->params)->render(); + } elseif (is_string($template)) { + $string .= $template; + } + } + + return $string; + } +} diff --git a/packages/Webkul/Core/src/Vite.php b/packages/Webkul/Core/src/Vite.php new file mode 100644 index 0000000..9df887a --- /dev/null +++ b/packages/Webkul/Core/src/Vite.php @@ -0,0 +1,49 @@ +useBuildDirectory($viters[$namespace]['build_directory']) + ->asset($viteUrl); + } + + /** + * Set krayin vite. + * + * @return mixed + */ + public function set(mixed $entryPoints, string $namespace = 'admin') + { + $viters = config('krayin-vite.viters'); + + if (empty($viters[$namespace])) { + throw new ViterNotFound($namespace); + } + + return BaseVite::useHotFile($viters[$namespace]['hot_file']) + ->useBuildDirectory($viters[$namespace]['build_directory']) + ->withEntryPoints($entryPoints); + } +} diff --git a/packages/Webkul/DataGrid/src/Action.php b/packages/Webkul/DataGrid/src/Action.php new file mode 100644 index 0000000..801eef2 --- /dev/null +++ b/packages/Webkul/DataGrid/src/Action.php @@ -0,0 +1,34 @@ + $this->index, + 'icon' => $this->icon, + 'title' => $this->title, + 'method' => $this->method, + 'url' => $this->url, + ]; + } +} diff --git a/packages/Webkul/DataGrid/src/Column.php b/packages/Webkul/DataGrid/src/Column.php new file mode 100644 index 0000000..75c6c3b --- /dev/null +++ b/packages/Webkul/DataGrid/src/Column.php @@ -0,0 +1,374 @@ +init($column); + } + + /** + * Initialize all necessary settings for the columns. + */ + public function init(array $column): void + { + $this->setIndex($column['index']); + + $this->setLabel($column['label']); + + $this->setType($column['type']); + + $this->setSearchable($column['searchable'] ?? $this->searchable); + + $this->setFilterable($column['filterable'] ?? $this->filterable); + + $this->setFilterableType($column['filterable_type'] ?? $this->filterableType); + + $this->setFilterableOptions($column['filterable_options'] ?? $this->filterableOptions); + + $this->setAllowMultipleValues($column['allow_multiple_values'] ?? $this->allowMultipleValues); + + $this->setSortable($column['sortable'] ?? $this->sortable); + + $this->setVisibility($column['visibility'] ?? $this->visibility); + + $this->setClosure($column['closure'] ?? $this->closure); + + $this->setColumnName($this->index); + } + + /** + * Set index. + */ + public function setIndex(string $index): void + { + $this->index = $index; + } + + /** + * Get index. + */ + public function getIndex(): string + { + return $this->index; + } + + /** + * Set label. + */ + public function setLabel(string $label): void + { + $this->label = $label; + } + + /** + * Get label. + */ + public function getLabel(): string + { + return $this->label; + } + + /** + * Set type. + */ + public function setType(string $type): void + { + $this->type = $type; + } + + /** + * Get type. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Set searchable. + */ + public function setSearchable(bool $searchable): void + { + $this->searchable = $searchable; + } + + /** + * Get searchable. + */ + public function getSearchable(): bool + { + return $this->searchable; + } + + /** + * Set filterable. + */ + public function setFilterable(bool $filterable): void + { + $this->filterable = $filterable; + } + + /** + * Get filterable. + */ + public function getFilterable(): bool + { + return $this->filterable; + } + + /** + * Set filterable type. + */ + public function setFilterableType(?string $filterableType): void + { + $this->filterableType = $filterableType; + } + + /** + * Get filterable type. + */ + public function getFilterableType(): ?string + { + return $this->filterableType; + } + + /** + * Set filterable options. + */ + public function setFilterableOptions(mixed $filterableOptions): void + { + if ($filterableOptions instanceof \Closure) { + $filterableOptions = $filterableOptions(); + } + + $this->filterableOptions = $filterableOptions; + } + + /** + * Get filterable options. + */ + public function getFilterableOptions(): array + { + return $this->filterableOptions; + } + + /** + * Set allow multiple values. + */ + public function setAllowMultipleValues(bool $allowMultipleValues): void + { + $this->allowMultipleValues = $allowMultipleValues; + } + + /** + * Get allow multiple values. + */ + public function getAllowMultipleValues(): bool + { + return $this->allowMultipleValues; + } + + /** + * Set sortable. + */ + public function setSortable(?bool $sortable = null): void + { + $this->sortable = $sortable; + } + + /** + * Get sortable. + */ + public function getSortable(): bool + { + return $this->sortable; + } + + /** + * Set exportable. + */ + public function setExportable(bool $exportable): void + { + $this->exportable = $exportable; + } + + /** + * Get exportable. + */ + public function getExportable(): bool + { + return $this->exportable; + } + + /** + * Set visibility. + */ + public function setVisibility(bool $visibility): void + { + $this->visibility = $visibility; + } + + /** + * Get visibility. + */ + public function getVisibility(): bool + { + return $this->visibility; + } + + /** + * Set closure. + */ + public function setClosure(mixed $closure): void + { + $this->closure = $closure; + } + + /** + * Get closure. + */ + public function getClosure(): mixed + { + return $this->closure; + } + + /** + * Define the table's column name. Initially, it will match the index. However, after adding an alias, + * the column name may change. + */ + public function setColumnName(mixed $columnName): void + { + $this->columnName = $columnName; + } + + /** + * Get the table's column name. + */ + public function getColumnName(): mixed + { + return $this->columnName; + } + + /** + * To array. + */ + public function toArray(): array + { + return [ + 'index' => $this->index, + 'label' => $this->label, + 'type' => $this->type, + 'searchable' => $this->searchable, + 'filterable' => $this->filterable, + 'filterable_type' => $this->filterableType, + 'filterable_options' => $this->filterableOptions, + 'allow_multiple_values' => $this->allowMultipleValues, + 'sortable' => $this->sortable, + 'visibility' => $this->visibility, + ]; + } + + /** + * Validate the column. + */ + public static function validate(array $column): void + { + if (empty($column['index'])) { + throw new InvalidColumnException('The `index` key is required. Ensure that the `index` key is present in all calls to the `addColumn` method.'); + } + + if (empty($column['label'])) { + throw new InvalidColumnException('The `label` key is required. Ensure that the `label` key is present in all calls to the `addColumn` method.'); + } + + if (empty($column['type'])) { + throw new InvalidColumnException('The `type` key is required. Ensure that the `type` key is present in all calls to the `addColumn` method.'); + } + } + + /** + * Resolve the column type class. + */ + public static function resolveType(array $column): self + { + self::validate($column); + + $columnTypeClass = ColumnTypeEnum::getClassName($column['type']); + + return new $columnTypeClass($column); + } +} diff --git a/packages/Webkul/DataGrid/src/ColumnTypes/Aggregate.php b/packages/Webkul/DataGrid/src/ColumnTypes/Aggregate.php new file mode 100644 index 0000000..9ceb9c0 --- /dev/null +++ b/packages/Webkul/DataGrid/src/ColumnTypes/Aggregate.php @@ -0,0 +1,41 @@ +filterableType === FilterTypeEnum::DROPDOWN->value) { + return $queryBuilder->having(function ($scopeQueryBuilder) use ($requestedValues) { + if (is_string($requestedValues)) { + $scopeQueryBuilder->orHaving($this->columnName, $requestedValues); + + return; + } + + foreach ($requestedValues as $value) { + $scopeQueryBuilder->orHaving($this->columnName, $value); + } + }); + } + + return $queryBuilder->having(function ($scopeQueryBuilder) use ($requestedValues) { + if (is_string($requestedValues)) { + $scopeQueryBuilder->orHaving($this->columnName, 'LIKE', '%'.$requestedValues.'%'); + + return; + } + + foreach ($requestedValues as $value) { + $scopeQueryBuilder->orHaving($this->columnName, 'LIKE', '%'.$value.'%'); + } + }); + } +} diff --git a/packages/Webkul/DataGrid/src/ColumnTypes/Boolean.php b/packages/Webkul/DataGrid/src/ColumnTypes/Boolean.php new file mode 100644 index 0000000..f48dba6 --- /dev/null +++ b/packages/Webkul/DataGrid/src/ColumnTypes/Boolean.php @@ -0,0 +1,68 @@ +value) + ) { + throw new InvalidColumnException('Boolean filters will only work with `dropdown` type. Either remove the `filterable_type` or set it to `dropdown`.'); + } + + if (! $filterableType) { + $filterableType = FilterTypeEnum::DROPDOWN->value; + } + + parent::setFilterableType($filterableType); + } + + /** + * Set filterable options. + */ + public function setFilterableOptions(mixed $filterableOptions): void + { + if (empty($filterableOptions)) { + $filterableOptions = [ + [ + 'label' => trans('admin::app.components.datagrid.filters.boolean-options.true'), + 'value' => 1, + ], + [ + 'label' => trans('admin::app.components.datagrid.filters.boolean-options.false'), + 'value' => 0, + ], + ]; + } + + parent::setFilterableOptions($filterableOptions); + } + + /** + * Process filter. + */ + public function processFilter($queryBuilder, $requestedValues): mixed + { + return $queryBuilder->where(function ($scopeQueryBuilder) use ($requestedValues) { + if (is_string($requestedValues)) { + $scopeQueryBuilder->orWhere($this->columnName, $requestedValues); + + return; + } + + foreach ($requestedValues as $value) { + $scopeQueryBuilder->orWhere($this->columnName, $value); + } + }); + } +} diff --git a/packages/Webkul/DataGrid/src/ColumnTypes/Date.php b/packages/Webkul/DataGrid/src/ColumnTypes/Date.php new file mode 100644 index 0000000..3bcb049 --- /dev/null +++ b/packages/Webkul/DataGrid/src/ColumnTypes/Date.php @@ -0,0 +1,70 @@ +value) + ) { + throw new InvalidColumnException('Date filters will only work with `date_range` type. Either remove the `filterable_type` or set it to `date_range`.'); + } + + parent::setFilterableType($filterableType); + } + + /** + * Set filterable options. + */ + public function setFilterableOptions(mixed $filterableOptions): void + { + if (empty($filterableOptions)) { + $filterableOptions = DateRangeOptionEnum::options(); + } + + parent::setFilterableOptions($filterableOptions); + } + + /** + * Process filter. + */ + public function processFilter($queryBuilder, $requestedDates) + { + return $queryBuilder->where(function ($scopeQueryBuilder) use ($requestedDates) { + if (is_string($requestedDates)) { + $rangeOption = collect($this->filterableOptions)->firstWhere('name', $requestedDates); + + $requestedDates = ! $rangeOption + ? [[$requestedDates, $requestedDates]] + : [[$rangeOption['from'], $rangeOption['to']]]; + } + + foreach ($requestedDates as $value) { + $from = $value[0] ?? ''; + $to = $value[1] ?? ''; + + if ($from !== '' && $to !== '') { + $scopeQueryBuilder->whereBetween($this->columnName, [ + $from.' 00:00:01', + $to.' 23:59:59', + ]); + } elseif ($from !== '') { + $scopeQueryBuilder->where($this->columnName, '>=', $from.' 00:00:01'); + } elseif ($to !== '') { + $scopeQueryBuilder->where($this->columnName, '<=', $to.' 23:59:59'); + } + } + }); + } +} diff --git a/packages/Webkul/DataGrid/src/ColumnTypes/Datetime.php b/packages/Webkul/DataGrid/src/ColumnTypes/Datetime.php new file mode 100644 index 0000000..cb4c271 --- /dev/null +++ b/packages/Webkul/DataGrid/src/ColumnTypes/Datetime.php @@ -0,0 +1,58 @@ +value) + ) { + throw new InvalidColumnException('Datetime filters will only work with `datetime_range` type. Either remove the `filterable_type` or set it to `datetime_range`.'); + } + + parent::setFilterableType($filterableType); + } + + /** + * Set filterable options. + */ + public function setFilterableOptions(mixed $filterableOptions): void + { + if (empty($filterableOptions)) { + $filterableOptions = DateRangeOptionEnum::options(); + } + + parent::setFilterableOptions($filterableOptions); + } + + /** + * Process filter. + */ + public function processFilter($queryBuilder, $requestedDates) + { + return $queryBuilder->where(function ($scopeQueryBuilder) use ($requestedDates) { + if (is_string($requestedDates)) { + $rangeOption = collect($this->filterableOptions)->firstWhere('name', $requestedDates); + + $requestedDates = ! $rangeOption + ? [[$requestedDates, $requestedDates]] + : [[$rangeOption['from'], $rangeOption['to']]]; + } + + foreach ($requestedDates as $value) { + $scopeQueryBuilder->whereBetween($this->columnName, [$value[0] ?? '', $value[1] ?? '']); + } + }); + } +} diff --git a/packages/Webkul/DataGrid/src/ColumnTypes/Decimal.php b/packages/Webkul/DataGrid/src/ColumnTypes/Decimal.php new file mode 100644 index 0000000..d55deb0 --- /dev/null +++ b/packages/Webkul/DataGrid/src/ColumnTypes/Decimal.php @@ -0,0 +1,26 @@ +where(function ($scopeQueryBuilder) use ($requestedValues) { + if (is_string($requestedValues)) { + $scopeQueryBuilder->orWhere($this->columnName, $requestedValues); + + return; + } + + foreach ($requestedValues as $value) { + $scopeQueryBuilder->orWhere($this->columnName, $value); + } + }); + } +} diff --git a/packages/Webkul/DataGrid/src/ColumnTypes/Integer.php b/packages/Webkul/DataGrid/src/ColumnTypes/Integer.php new file mode 100644 index 0000000..f615445 --- /dev/null +++ b/packages/Webkul/DataGrid/src/ColumnTypes/Integer.php @@ -0,0 +1,26 @@ +where(function ($scopeQueryBuilder) use ($requestedValues) { + if (is_string($requestedValues)) { + $scopeQueryBuilder->orWhere($this->columnName, $requestedValues); + + return; + } + + foreach ($requestedValues as $value) { + $scopeQueryBuilder->orWhere($this->columnName, $value); + } + }); + } +} diff --git a/packages/Webkul/DataGrid/src/ColumnTypes/Text.php b/packages/Webkul/DataGrid/src/ColumnTypes/Text.php new file mode 100644 index 0000000..e413cdb --- /dev/null +++ b/packages/Webkul/DataGrid/src/ColumnTypes/Text.php @@ -0,0 +1,41 @@ +filterableType === FilterTypeEnum::DROPDOWN->value) { + return $queryBuilder->where(function ($scopeQueryBuilder) use ($requestedValues) { + if (is_string($requestedValues)) { + $scopeQueryBuilder->orWhere($this->columnName, $requestedValues); + + return; + } + + foreach ($requestedValues as $value) { + $scopeQueryBuilder->orWhere($this->columnName, $value); + } + }); + } + + return $queryBuilder->where(function ($scopeQueryBuilder) use ($requestedValues) { + if (is_string($requestedValues)) { + $scopeQueryBuilder->orWhere($this->columnName, 'LIKE', '%'.$requestedValues.'%'); + + return; + } + + foreach ($requestedValues as $value) { + $scopeQueryBuilder->orWhere($this->columnName, 'LIKE', '%'.$value.'%'); + } + }); + } +} diff --git a/packages/Webkul/DataGrid/src/Contracts/SavedFilter.php b/packages/Webkul/DataGrid/src/Contracts/SavedFilter.php new file mode 100644 index 0000000..9c4c4f8 --- /dev/null +++ b/packages/Webkul/DataGrid/src/Contracts/SavedFilter.php @@ -0,0 +1,5 @@ +columns; + } + + /** + * Get actions. + */ + public function getActions(): array + { + return $this->actions; + } + + /** + * Get mass actions. + */ + public function getMassActions(): array + { + return $this->massActions; + } + + /** + * Add column. + */ + public function addColumn(array $column): void + { + $this->dispatchEvent('columns.add.before', [$this, $column]); + + $this->columns[] = Column::resolveType($column); + + $this->dispatchEvent('columns.add.after', [$this, $this->columns[count($this->columns) - 1]]); + } + + /** + * Add action. + */ + public function addAction(array $action): void + { + $this->dispatchEvent('actions.add.before', [$this, $action]); + + $this->actions[] = new Action( + index: $action['index'] ?? '', + icon: $action['icon'] ?? '', + title: $action['title'], + method: $action['method'], + url: $action['url'], + ); + + $this->dispatchEvent('actions.add.after', [$this, $this->actions[count($this->actions) - 1]]); + } + + /** + * Add mass action. + */ + public function addMassAction(array $massAction): void + { + $this->dispatchEvent('mass_actions.add.before', [$this, $massAction]); + + $this->massActions[] = new MassAction( + icon: $massAction['icon'] ?? '', + title: $massAction['title'], + method: $massAction['method'], + url: $massAction['url'], + options: $massAction['options'] ?? [], + ); + + $this->dispatchEvent('mass_actions.add.after', [$this, $this->massActions[count($this->massActions) - 1]]); + } + + /** + * Set query builder. + * + * @param mixed $queryBuilder + */ + public function setQueryBuilder($queryBuilder = null): void + { + $this->dispatchEvent('query_builder.set.before', [$this, $queryBuilder]); + + $this->queryBuilder = $queryBuilder ?: $this->prepareQueryBuilder(); + + $this->dispatchEvent('query_builder.set.after', $this); + } + + /** + * Get query builder. + */ + public function getQueryBuilder(): mixed + { + return $this->queryBuilder; + } + + /** + * Prepare and return the query builder with the currently requested filters + * and sort applied, but without pagination or export side effects — e.g. + * for bulk actions that need every matching row rather than just the + * current page or a file download. + */ + public function getFilteredQueryBuilder(): mixed + { + $this->prepareColumns(); + + $this->setQueryBuilder(); + + $requestedParams = request()->only(['filters', 'sort']); + + $this->queryBuilder = $this->processRequestedFilters($requestedParams['filters'] ?? []); + + $this->queryBuilder = $this->processRequestedSorting($requestedParams['sort'] ?? []); + + return $this->queryBuilder; + } + + /** + * Map your filter. + */ + public function addFilter(string $datagridColumn, mixed $queryColumn): void + { + $this->dispatchEvent('filters.add.before', [$this, $datagridColumn, $queryColumn]); + + foreach ($this->columns as $column) { + if ($column->getIndex() === $datagridColumn) { + $column->setColumnName($queryColumn); + + break; + } + } + + $this->dispatchEvent('filters.add.after', [$this, $datagridColumn, $queryColumn]); + } + + /** + * Set exportable. + */ + public function setExportable(bool $exportable): void + { + $this->dispatchEvent('exportable.set.before', [$this, $exportable]); + + $this->exportable = $exportable; + + $this->dispatchEvent('exportable.set.after', $this); + } + + /** + * Get exportable. + */ + public function getExportable(): bool + { + return $this->exportable; + } + + /** + * Set export file. + * + * @param string $format + * @return void + */ + public function setExportFile($format = 'csv') + { + $this->dispatchEvent('export_file.set.before', [$this, $format]); + + $this->setExportable(true); + + $this->exportFile = Excel::download(new DataGridExport($this), Str::random(36).'.'.$format); + + $this->dispatchEvent('export_file.set.after', $this); + } + + /** + * Download export file. + * + * @return BinaryFileResponse + */ + public function downloadExportFile() + { + return $this->exportFile; + } + + /** + * Process the datagrid. + * + * @return BinaryFileResponse|JsonResponse + */ + public function process() + { + $this->prepare(); + + if ($this->getExportable()) { + return $this->downloadExportFile(); + } + + return response()->json($this->formatData()); + } + + /** + * To json. The reason for deprecation is that it is not an action returning JSON; instead, + * it is a process method which returns a download as well as a JSON response. + * + * @deprecated + * + * @return BinaryFileResponse|JsonResponse + */ + public function toJson() + { + $this->prepare(); + + if ($this->getExportable()) { + return $this->downloadExportFile(); + } + + return response()->json($this->formatData()); + } + + /** + * Validated request. + */ + protected function validatedRequest(): array + { + request()->validate([ + 'filters' => ['sometimes', 'required', 'array'], + 'sort' => ['sometimes', 'required', 'array'], + 'pagination' => ['sometimes', 'required', 'array'], + 'export' => ['sometimes', 'required', 'boolean'], + 'format' => ['sometimes', 'required', 'in:csv,xls,xlsx'], + ]); + + return request()->only(['filters', 'sort', 'pagination', 'export', 'format']); + } + + /** + * Process all requested filters. + * + * @return Builder + */ + protected function processRequestedFilters(array $requestedFilters) + { + foreach ($requestedFilters as $requestedColumn => $requestedValues) { + if ($requestedColumn === 'all') { + $this->queryBuilder->where(function ($scopeQueryBuilder) use ($requestedValues) { + foreach ($requestedValues as $value) { + collect($this->columns) + ->filter(fn ($column) => $column->getSearchable() && ! in_array($column->getType(), [ + ColumnTypeEnum::BOOLEAN->value, + ColumnTypeEnum::AGGREGATE->value, + ])) + ->each(fn ($column) => $scopeQueryBuilder->orWhere($column->getColumnName(), 'LIKE', '%'.$value.'%')); + } + }); + } else { + collect($this->columns) + ->first(fn ($column) => $column->getIndex() === $requestedColumn) + ->processFilter($this->queryBuilder, $requestedValues); + } + } + + return $this->queryBuilder; + } + + /** + * Process requested sorting. + * + * @return Builder + */ + protected function processRequestedSorting($requestedSort) + { + if (! $this->sortColumn) { + $this->sortColumn = $this->primaryColumn; + } + + return $this->queryBuilder->orderBy($requestedSort['column'] ?? $this->sortColumn, $requestedSort['order'] ?? $this->sortOrder); + } + + /** + * Process requested pagination. + */ + protected function processRequestedPagination($requestedPagination): LengthAwarePaginator + { + return $this->queryBuilder->paginate( + $requestedPagination['per_page'] ?? $this->itemsPerPage, + ['*'], + 'page', + $requestedPagination['page'] ?? 1 + ); + } + + /** + * Process paginated request. + */ + protected function processPaginatedRequest(array $requestedParams): void + { + $this->dispatchEvent('process_request.paginated.before', $this); + + $this->paginator = $this->processRequestedPagination($requestedParams['pagination'] ?? []); + + $this->dispatchEvent('process_request.paginated.after', $this); + } + + /** + * Process export request. + */ + protected function processExportRequest(array $requestedParams): void + { + $this->dispatchEvent('process_request.export.before', $this); + + $this->setExportFile($requestedParams['format']); + + $this->dispatchEvent('process_request.export.after', $this); + } + + /** + * Process request. + */ + protected function processRequest(): void + { + $this->dispatchEvent('process_request.before', $this); + + /** + * Store all request parameters in this variable; avoid using direct request helpers afterward. + */ + $requestedParams = $this->validatedRequest(); + + $this->queryBuilder = $this->processRequestedFilters($requestedParams['filters'] ?? []); + + $this->queryBuilder = $this->processRequestedSorting($requestedParams['sort'] ?? []); + + /** + * The `export` parameter is validated as a boolean in the `validatedRequest`. An `empty` function will not work, + * as it will always be treated as true because of "0" and "1". + */ + isset($requestedParams['export']) && (bool) $requestedParams['export'] + ? $this->processExportRequest($requestedParams) + : $this->processPaginatedRequest($requestedParams); + + $this->dispatchEvent('process_request.after', $this); + } + + /** + * Prepare all the setup for datagrid. + */ + protected function sanitizeRow($row): \stdClass + { + /** + * Convert stdClass to array. + */ + $tempRow = json_decode(json_encode($row), true); + + foreach ($tempRow as $column => $value) { + if (! is_string($tempRow[$column])) { + continue; + } + + if (is_array($value)) { + return $this->sanitizeRow($tempRow[$column]); + } else { + $row->{$column} = strip_tags($value); + } + } + + return $row; + } + + /** + * Format columns. + */ + protected function formatColumns(): array + { + return collect($this->columns) + ->map(fn ($column) => $column->toArray()) + ->toArray(); + } + + /** + * Format actions. + */ + protected function formatActions(): array + { + return collect($this->actions) + ->map(fn ($action) => $action->toArray()) + ->toArray(); + } + + /** + * Format mass actions. + */ + protected function formatMassActions(): array + { + return collect($this->massActions) + ->map(fn ($massAction) => $massAction->toArray()) + ->toArray(); + } + + /** + * Format records. + */ + protected function formatRecords($records): mixed + { + foreach ($records as $record) { + $record = $this->sanitizeRow($record); + + foreach ($this->columns as $column) { + if ($closure = $column->getClosure()) { + $record->{$column->getIndex()} = $closure($record); + } + } + + $record->actions = []; + + foreach ($this->actions as $index => $action) { + $getUrl = $action->url; + + $record->actions[] = [ + 'index' => ! empty($action->index) ? $action->index : 'action_'.$index + 1, + 'icon' => $action->icon, + 'title' => $action->title, + 'method' => $action->method, + 'url' => $getUrl($record), + ]; + } + } + + return $records; + } + + /** + * Format data. + */ + protected function formatData(): array + { + $paginator = $this->paginator->toArray(); + + return [ + 'id' => Crypt::encryptString(get_called_class()), + 'columns' => $this->formatColumns(), + 'actions' => $this->formatActions(), + 'mass_actions' => $this->formatMassActions(), + 'records' => $this->formatRecords($paginator['data']), + 'meta' => [ + 'primary_column' => $this->primaryColumn, + 'from' => $paginator['from'], + 'to' => $paginator['to'], + 'total' => $paginator['total'], + 'per_page_options' => $this->perPageOptions, + 'per_page' => $paginator['per_page'], + 'current_page' => $paginator['current_page'], + 'last_page' => $paginator['last_page'], + ], + ]; + } + + /** + * Dispatch event. + */ + protected function dispatchEvent(string $eventName, mixed $payload): void + { + $reflection = new \ReflectionClass($this); + + $datagridName = Str::snake($reflection->getShortName()); + + Event::dispatch("datagrid.{$datagridName}.{$eventName}", $payload); + } + + /** + * Prepare all the setup for datagrid. + */ + protected function prepare(): void + { + $this->dispatchEvent('prepare.before', $this); + + $this->prepareColumns(); + + $this->dispatchEvent('columns.prepare.after', $this); + + $this->prepareActions(); + + $this->dispatchEvent('actions.prepare.after', $this); + + $this->prepareMassActions(); + + $this->dispatchEvent('mass_actions.prepare.after', $this); + + $this->setQueryBuilder(); + + $this->dispatchEvent('query_builder.prepare.after', $this); + + $this->processRequest(); + + $this->dispatchEvent('prepare.after', $this); + } +} diff --git a/packages/Webkul/DataGrid/src/Database/Migrations/2024_05_10_152848_create_saved_filters_table.php b/packages/Webkul/DataGrid/src/Database/Migrations/2024_05_10_152848_create_saved_filters_table.php new file mode 100644 index 0000000..de5d20b --- /dev/null +++ b/packages/Webkul/DataGrid/src/Database/Migrations/2024_05_10_152848_create_saved_filters_table.php @@ -0,0 +1,33 @@ +id(); + $table->integer('user_id')->unsigned(); + $table->string('name'); + $table->string('src'); + $table->json('applied'); + $table->timestamps(); + + $table->unique(['user_id', 'name', 'src']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('datagrid_saved_filters'); + } +}; diff --git a/packages/Webkul/DataGrid/src/Enums/ColumnTypeEnum.php b/packages/Webkul/DataGrid/src/Enums/ColumnTypeEnum.php new file mode 100644 index 0000000..ae279e0 --- /dev/null +++ b/packages/Webkul/DataGrid/src/Enums/ColumnTypeEnum.php @@ -0,0 +1,67 @@ +value => Text::class, + self::INTEGER->value => Integer::class, + self::FLOAT->value => Decimal::class, + self::BOOLEAN->value => Boolean::class, + self::DATE->value => Date::class, + self::DATETIME->value => Datetime::class, + self::AGGREGATE->value => Aggregate::class, + default => throw new InvalidColumnTypeException("Invalid column type: {$type}"), + }; + } +} diff --git a/packages/Webkul/DataGrid/src/Enums/DateRangeOptionEnum.php b/packages/Webkul/DataGrid/src/Enums/DateRangeOptionEnum.php new file mode 100644 index 0000000..88cd36a --- /dev/null +++ b/packages/Webkul/DataGrid/src/Enums/DateRangeOptionEnum.php @@ -0,0 +1,103 @@ + self::TODAY->value, + 'label' => trans('admin::app.components.datagrid.filters.date-options.today'), + 'from' => now()->today()->format($format), + 'to' => now()->endOfDay()->format($format), + ], + [ + 'name' => self::YESTERDAY->value, + 'label' => trans('admin::app.components.datagrid.filters.date-options.yesterday'), + 'from' => now()->yesterday()->format($format), + 'to' => now()->today()->subSecond(1)->format($format), + ], + [ + 'name' => self::THIS_WEEK->value, + 'label' => trans('admin::app.components.datagrid.filters.date-options.this-week'), + 'from' => now()->startOfWeek()->format($format), + 'to' => now()->endOfWeek()->format($format), + ], + [ + 'name' => self::THIS_MONTH->value, + 'label' => trans('admin::app.components.datagrid.filters.date-options.this-month'), + 'from' => now()->startOfMonth()->format($format), + 'to' => now()->endOfMonth()->format($format), + ], + [ + 'name' => self::LAST_MONTH->value, + 'label' => trans('admin::app.components.datagrid.filters.date-options.last-month'), + 'from' => now()->subMonth(1)->startOfMonth()->format($format), + 'to' => now()->subMonth(1)->endOfMonth()->format($format), + ], + [ + 'name' => self::LAST_THREE_MONTHS->value, + 'label' => trans('admin::app.components.datagrid.filters.date-options.last-three-months'), + 'from' => now()->subMonth(3)->startOfMonth()->format($format), + 'to' => now()->subMonth(1)->endOfMonth()->format($format), + ], + [ + 'name' => self::LAST_SIX_MONTHS->value, + 'label' => trans('admin::app.components.datagrid.filters.date-options.last-six-months'), + 'from' => now()->subMonth(6)->startOfMonth()->format($format), + 'to' => now()->subMonth(1)->endOfMonth()->format($format), + ], + [ + 'name' => self::THIS_YEAR->value, + 'label' => trans('admin::app.components.datagrid.filters.date-options.this-year'), + 'from' => now()->startOfYear()->format($format), + 'to' => now()->endOfYear()->format($format), + ], + ]; + } +} diff --git a/packages/Webkul/DataGrid/src/Enums/FilterTypeEnum.php b/packages/Webkul/DataGrid/src/Enums/FilterTypeEnum.php new file mode 100644 index 0000000..df38a94 --- /dev/null +++ b/packages/Webkul/DataGrid/src/Enums/FilterTypeEnum.php @@ -0,0 +1,21 @@ +datagrid->getQueryBuilder(); + } + + /** + * Headings. + */ + public function headings(): array + { + return collect($this->datagrid->getColumns()) + ->filter(fn ($column) => $column->getExportable()) + ->map(fn ($column) => $column->getLabel()) + ->toArray(); + } + + /** + * Map each row for export. + */ + public function map(mixed $record): array + { + return collect($this->datagrid->getColumns()) + ->filter(fn ($column) => $column->getExportable()) + ->map(function ($column) use ($record) { + $index = $column->getIndex(); + $value = $record->{$index}; + + if ( + in_array($index, ['emails', 'contact_numbers']) + && is_string($value) + ) { + return $this->extractValuesFromJson($value); + } + + return $value; + }) + ->toArray(); + } + + /** + * Extract 'value' fields from a JSON string. + */ + protected function extractValuesFromJson(string $json): string + { + $items = json_decode($json, true); + + if ( + json_last_error() === JSON_ERROR_NONE + && is_array($items) + ) { + return collect($items)->map(fn ($item) => "{$item['value']} ({$item['label']})")->implode(', '); + } + + return $json; + } +} diff --git a/packages/Webkul/DataGrid/src/Http/helpers.php b/packages/Webkul/DataGrid/src/Http/helpers.php new file mode 100644 index 0000000..b174f1c --- /dev/null +++ b/packages/Webkul/DataGrid/src/Http/helpers.php @@ -0,0 +1,18 @@ + $this->icon, + 'title' => $this->title, + 'method' => $this->method, + 'url' => $this->url, + 'options' => $this->options, + ]; + } +} diff --git a/packages/Webkul/DataGrid/src/Models/SavedFilter.php b/packages/Webkul/DataGrid/src/Models/SavedFilter.php new file mode 100755 index 0000000..4eebe64 --- /dev/null +++ b/packages/Webkul/DataGrid/src/Models/SavedFilter.php @@ -0,0 +1,40 @@ + 'json', + ]; +} diff --git a/packages/Webkul/DataGrid/src/Models/SavedFilterProxy.php b/packages/Webkul/DataGrid/src/Models/SavedFilterProxy.php new file mode 100644 index 0000000..226823c --- /dev/null +++ b/packages/Webkul/DataGrid/src/Models/SavedFilterProxy.php @@ -0,0 +1,7 @@ +loadMigrationsFrom(__DIR__.'/../Database/Migrations'); + } + + /** + * Register any application services. + */ + public function register(): void {} +} diff --git a/packages/Webkul/DataGrid/src/Providers/ModuleServiceProvider.php b/packages/Webkul/DataGrid/src/Providers/ModuleServiceProvider.php new file mode 100644 index 0000000..531aaa1 --- /dev/null +++ b/packages/Webkul/DataGrid/src/Providers/ModuleServiceProvider.php @@ -0,0 +1,13 @@ + [ + 'title' => 'admin::app.settings.data-transfer.importers.persons.title', + 'importer' => 'Webkul\DataTransfer\Helpers\Importers\Persons\Importer', + 'sample_path' => 'data-transfer/samples/persons.csv', + ], + + 'products' => [ + 'title' => 'admin::app.settings.data-transfer.importers.products.title', + 'importer' => 'Webkul\DataTransfer\Helpers\Importers\Products\Importer', + 'sample_path' => 'data-transfer/samples/products.csv', + ], + + 'leads' => [ + 'title' => 'admin::app.settings.data-transfer.importers.leads.title', + 'importer' => 'Webkul\DataTransfer\Helpers\Importers\Leads\Importer', + 'sample_path' => 'data-transfer/samples/leads.csv', + ], +]; diff --git a/packages/Webkul/DataTransfer/src/Contracts/Import.php b/packages/Webkul/DataTransfer/src/Contracts/Import.php new file mode 100644 index 0000000..d9f02b8 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Contracts/Import.php @@ -0,0 +1,5 @@ +increments('id'); + $table->string('state')->default('pending'); + $table->boolean('process_in_queue')->default(1); + $table->string('type'); + $table->string('action'); + $table->string('validation_strategy'); + $table->integer('allowed_errors')->default(0); + $table->integer('processed_rows_count')->default(0); + $table->integer('invalid_rows_count')->default(0); + $table->integer('errors_count')->default(0); + $table->json('errors')->nullable(); + $table->string('field_separator'); + $table->string('file_path'); + $table->string('error_file_path')->nullable(); + $table->json('summary')->nullable(); + + $table->datetime('started_at')->nullable(); + $table->datetime('completed_at')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('imports'); + } +}; diff --git a/packages/Webkul/DataTransfer/src/Database/Migrations/2024_01_11_154741_create_import_batches_table.php b/packages/Webkul/DataTransfer/src/Database/Migrations/2024_01_11_154741_create_import_batches_table.php new file mode 100644 index 0000000..0139541 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Database/Migrations/2024_01_11_154741_create_import_batches_table.php @@ -0,0 +1,32 @@ +increments('id'); + $table->string('state')->default('pending'); + $table->json('data'); + $table->json('summary')->nullable(); + $table->integer('import_id')->unsigned(); + + $table->foreign('import_id')->references('id')->on('imports')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('import_batches'); + } +}; diff --git a/packages/Webkul/DataTransfer/src/Helpers/Error.php b/packages/Webkul/DataTransfer/src/Helpers/Error.php new file mode 100644 index 0000000..77470ae --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Error.php @@ -0,0 +1,186 @@ +messageTemplate[$code] = $template; + + return $this; + } + + /** + * Add error message. + */ + public function addError(string $code, ?int $rowNumber = null, ?string $columnName = null, ?string $message = null): self + { + if ($this->isErrorAlreadyAdded($rowNumber, $code, $columnName)) { + return $this; + } + + $this->addRowToInvalid($rowNumber); + + $message = $this->getErrorMessage($code, $message, $columnName); + + $this->items[$rowNumber][] = [ + 'code' => $code, + 'column' => $columnName, + 'message' => $message, + ]; + + $this->errorsCount++; + + return $this; + } + + /** + * Check if error is already added for the row, code and column. + */ + public function isErrorAlreadyAdded(?int $rowNumber, string $code, ?string $columnName): bool + { + return collect($this->items[$rowNumber] ?? []) + ->where('code', $code) + ->where('column', $columnName) + ->isNotEmpty(); + } + + /** + * Add specific row to invalid list via row number. + */ + protected function addRowToInvalid(?int $rowNumber): self + { + if (is_null($rowNumber)) { + return $this; + } + + if (! in_array($rowNumber, $this->invalidRows)) { + $this->invalidRows[] = $rowNumber; + } + + return $this; + } + + /** + * Add specific row to invalid list via row number. + */ + public function addRowToSkip(?int $rowNumber): self + { + if (is_null($rowNumber)) { + return $this; + } + + if (! in_array($rowNumber, $this->skippedRows)) { + $this->skippedRows[] = $rowNumber; + } + + return $this; + } + + /** + * Check if row is invalid by row number. + */ + public function isRowInvalid(int $rowNumber): bool + { + return in_array($rowNumber, array_merge($this->invalidRows, $this->skippedRows)); + } + + /** + * Build an error message via code, message and column name. + */ + protected function getErrorMessage(?string $code, ?string $message, ?string $columnName): string + { + if ( + empty($message) + && isset($this->messageTemplate[$code]) + ) { + $message = (string) $this->messageTemplate[$code]; + } + + if ( + $columnName + && $message + ) { + $message = sprintf($message, $columnName); + } + + if (! $message) { + $message = $code; + } + + return $message; + } + + /** + * Get number of invalid rows. + */ + public function getInvalidRowsCount(): int + { + return count($this->invalidRows); + } + + /** + * Get current error count. + */ + public function getErrorsCount(): int + { + return $this->errorsCount; + } + + /** + * Get all errors from an import process. + */ + public function getAllErrors(): array + { + return $this->items; + } + + /** + * Return all errors grouped by code. + */ + public function getAllErrorsGroupedByCode(): array + { + $errors = []; + + foreach ($this->items as $rowNumber => $rowErrors) { + foreach ($rowErrors as $error) { + if ($rowNumber === '') { + $errors[$error['code']][$error['message']] = null; + } else { + $errors[$error['code']][$error['message']][] = $rowNumber; + } + } + } + + return $errors; + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Import.php b/packages/Webkul/DataTransfer/src/Helpers/Import.php new file mode 100644 index 0000000..f7ba9e2 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Import.php @@ -0,0 +1,617 @@ +import = $import; + + return $this; + } + + /** + * Returns import instance. + */ + public function getImport(): ImportContract + { + return $this->import; + } + + /** + * Returns error helper instance. + * + * @return Error + */ + public function getErrorHelper() + { + return $this->errorHelper; + } + + /** + * Returns source helper instance. + */ + public function getSource(): AbstractSource + { + if (Str::contains($this->import->file_path, '.csv')) { + $source = new CSVSource( + $this->import->file_path, + $this->import->field_separator, + ); + } else { + $source = new ExcelSource( + $this->import->file_path, + $this->import->field_separator, + ); + } + + return $source; + } + + /** + * Validates import and returns validation result. + */ + public function validate(): bool + { + try { + $source = $this->getSource(); + + $typeImporter = $this->getTypeImporter()->setSource($source); + + $typeImporter->validateData(); + } catch (\Exception $e) { + $this->errorHelper->addError( + AbstractImporter::ERROR_CODE_SYSTEM_EXCEPTION, + null, + null, + $e->getMessage() + ); + } + + $import = $this->importRepository->update([ + 'state' => self::STATE_VALIDATED, + 'processed_rows_count' => $this->getProcessedRowsCount(), + 'invalid_rows_count' => $this->errorHelper->getInvalidRowsCount(), + 'errors_count' => $this->errorHelper->getErrorsCount(), + 'errors' => $this->getFormattedErrors(), + 'error_file_path' => $this->uploadErrorReport(), + ], $this->import->id); + + $this->setImport($import); + + return $this->isValid(); + } + + /** + * Starts import process. + */ + public function isValid(): bool + { + if ($this->isErrorLimitExceeded()) { + return false; + } + + if ($this->import->processed_rows_count <= $this->import->invalid_rows_count) { + return false; + } + + return true; + } + + /** + * Check if error limit has been exceeded. + */ + public function isErrorLimitExceeded(): bool + { + if ( + $this->import->validation_strategy == self::VALIDATION_STRATEGY_STOP_ON_ERROR + && $this->import->errors_count > $this->import->allowed_errors + ) { + return true; + } + + return false; + } + + /** + * Starts import process. + */ + public function start(?ImportBatchContract $importBatch = null): bool + { + DB::beginTransaction(); + + try { + $typeImporter = $this->getTypeImporter(); + + $typeImporter->importData($importBatch); + } catch (\Exception $e) { + /** + * Rollback transaction. + */ + DB::rollBack(); + + throw $e; + } finally { + /** + * Commit transaction. + */ + DB::commit(); + } + + return true; + } + + /** + * Link import resources. + */ + public function link(ImportBatchContract $importBatch): bool + { + DB::beginTransaction(); + + try { + $typeImporter = $this->getTypeImporter(); + + $typeImporter->linkData($importBatch); + } catch (\Exception $e) { + /** + * Rollback transaction. + */ + DB::rollBack(); + + throw $e; + } finally { + /** + * Commit transaction. + */ + DB::commit(); + } + + return true; + } + + /** + * Index import resources. + */ + public function index(ImportBatchContract $importBatch): bool + { + DB::beginTransaction(); + + try { + $typeImporter = $this->getTypeImporter(); + + $typeImporter->indexData($importBatch); + } catch (\Exception $e) { + /** + * Rollback transaction. + */ + DB::rollBack(); + + throw $e; + } finally { + /** + * Commit transaction. + */ + DB::commit(); + } + + return true; + } + + /** + * Started the import process. + */ + public function started(): void + { + $import = $this->importRepository->update([ + 'state' => self::STATE_PROCESSING, + 'started_at' => now(), + 'summary' => [], + ], $this->import->id); + + $this->setImport($import); + + Event::dispatch('data_transfer.imports.started', $import); + } + + /** + * Started the import linking process. + */ + public function linking(): void + { + $import = $this->importRepository->update([ + 'state' => self::STATE_LINKING, + ], $this->import->id); + + $this->setImport($import); + + Event::dispatch('data_transfer.imports.linking', $import); + } + + /** + * Started the import indexing process. + */ + public function indexing(): void + { + $import = $this->importRepository->update([ + 'state' => self::STATE_INDEXING, + ], $this->import->id); + + $this->setImport($import); + + Event::dispatch('data_transfer.imports.indexing', $import); + } + + /** + * Start the import process. + */ + public function completed(): void + { + $summary = $this->importBatchRepository + ->select( + DB::raw('SUM(json_unquote(json_extract(summary, \'$."created"\'))) AS created'), + DB::raw('SUM(json_unquote(json_extract(summary, \'$."updated"\'))) AS updated'), + DB::raw('SUM(json_unquote(json_extract(summary, \'$."deleted"\'))) AS deleted'), + ) + ->where('import_id', $this->import->id) + ->groupBy('import_id') + ->first() + ->toArray(); + + $import = $this->importRepository->update([ + 'state' => self::STATE_COMPLETED, + 'summary' => $summary, + 'completed_at' => now(), + ], $this->import->id); + + $this->setImport($import); + + Event::dispatch('data_transfer.imports.completed', $import); + } + + /** + * Returns import stats. + */ + public function stats(string $state): array + { + $total = $this->import->batches->count(); + + $completed = $this->import->batches->where('state', $state)->count(); + + $progress = $total + ? round($completed / $total * 100) + : 0; + + $summary = $this->importBatchRepository + ->select( + DB::raw('SUM(json_unquote(json_extract(summary, \'$."created"\'))) AS created'), + DB::raw('SUM(json_unquote(json_extract(summary, \'$."updated"\'))) AS updated'), + DB::raw('SUM(json_unquote(json_extract(summary, \'$."deleted"\'))) AS deleted'), + ) + ->where('import_id', $this->import->id) + ->where('state', $state) + ->groupBy('import_id') + ->first() + ?->toArray(); + + return [ + 'batches' => [ + 'total' => $total, + 'completed' => $completed, + 'remaining' => $total - $completed, + ], + 'progress' => $progress, + 'summary' => $summary ?? [ + 'created' => 0, + 'updated' => 0, + 'deleted' => 0, + ], + ]; + } + + /** + * Determine whether a queued import has stalled because no queue worker is consuming it. + * + * The queued path dispatches the batch chain and returns immediately, so an install running + * the `database` or `redis` driver without a worker leaves the import in a processing state + * with every batch still pending. Reporting that lets the frontend stop polling and tell the + * user to start a worker, instead of sitting at 0% with no explanation. + */ + public function isStuck(): bool + { + if (! $this->import->process_in_queue) { + return false; + } + + if (! in_array($this->import->state, [ + self::STATE_PROCESSING, + self::STATE_LINKING, + self::STATE_INDEXING, + ])) { + return false; + } + + if ( + ! $this->import->started_at + || $this->import->started_at->gt(now()->subMinutes(self::STUCK_AFTER_MINUTES)) + ) { + return false; + } + + /** + * A worker that ran at all would have moved at least one batch out of `pending`, so no + * progress after the threshold means nothing is consuming the queue. + */ + return ! $this->import->batches() + ->where('state', '!=', self::STATE_PENDING) + ->exists(); + } + + /** + * Return all error grouped by error code. + */ + public function getFormattedErrors(): array + { + $errors = []; + + foreach ($this->errorHelper->getAllErrorsGroupedByCode() as $groupedErrors) { + foreach ($groupedErrors as $errorMessage => $rowNumbers) { + if (! empty($rowNumbers)) { + $errors[] = 'Row(s) '.implode(', ', $rowNumbers).': '.$errorMessage; + } else { + $errors[] = $errorMessage; + } + } + } + + return $errors; + } + + /** + * Uploads error report and save the path to the database. + */ + public function uploadErrorReport(): ?string + { + /** + * Return null if there are no errors. + */ + if (! $this->errorHelper->getErrorsCount()) { + return null; + } + + /** + * Return null if there are no invalid rows. + */ + if (! $this->errorHelper->getInvalidRowsCount()) { + return null; + } + + $errors = $this->errorHelper->getAllErrors(); + + $source = $this->getTypeImporter()->getSource(); + + $source->rewind(); + + $spreadsheet = new Spreadsheet; + + $sheet = $spreadsheet->getActiveSheet(); + + /** + * Add headers with extra error column. + */ + $sheet->fromArray( + [array_merge($source->getColumnNames(), [ + 'error', + ])], + null, + 'A1' + ); + + $rowNumber = 2; + + while ($source->valid()) { + try { + $rowData = $source->current(); + } catch (\InvalidArgumentException $e) { + $source->next(); + + continue; + } + + $rowErrors = $errors[$source->getCurrentRowNumber()] ?? []; + + if (! empty($rowErrors)) { + $rowErrors = Arr::pluck($rowErrors, 'message'); + } + + $rowData[] = implode('|', $rowErrors); + + $sheet->fromArray([$rowData], null, 'A'.$rowNumber++); + + $source->next(); + } + + $fileType = pathinfo($this->import->file_path, PATHINFO_EXTENSION); + + switch ($fileType) { + case 'csv': + $writer = new Csv($spreadsheet); + + $writer->setDelimiter($this->import->field_separator); + + break; + + case 'xls': + $writer = new Xls($spreadsheet); + + case 'xlsx': + $writer = new Xlsx($spreadsheet); + + break; + + default: + throw new \InvalidArgumentException("Unsupported file type: $fileType"); + } + + $errorFilePath = 'imports/'.time().'-error-report.'.$fileType; + + $writer->save(Storage::disk('public')->path($errorFilePath)); + + return $errorFilePath; + } + + /** + * Validates source file and returns validation result. + */ + public function getTypeImporter(): AbstractImporter + { + if (! $this->typeImporter) { + $importerConfig = config('importers.'.$this->import->type); + + $this->typeImporter = app()->make($importerConfig['importer']) + ->setImport($this->import) + ->setErrorHelper($this->errorHelper); + } + + return $this->typeImporter; + } + + /** + * Returns number of checked rows. + */ + public function getProcessedRowsCount(): int + { + return $this->getTypeImporter()->getProcessedRowsCount(); + } + + /** + * Is linking resource required for the import operation. + */ + public function isLinkingRequired(): bool + { + return $this->getTypeImporter()->isLinkingRequired(); + } + + /** + * Is indexing resource required for the import operation. + */ + public function isIndexingRequired(): bool + { + return $this->getTypeImporter()->isIndexingRequired(); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Importers/AbstractImporter.php b/packages/Webkul/DataTransfer/src/Helpers/Importers/AbstractImporter.php new file mode 100644 index 0000000..9ae9f27 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Importers/AbstractImporter.php @@ -0,0 +1,553 @@ + 'admin::app.settings.data-transfer.validation.errors.system', + self::ERROR_CODE_COLUMN_NOT_FOUND => 'admin::app.settings.data-transfer.validation.errors.column-not-found', + self::ERROR_CODE_COLUMN_EMPTY_HEADER => 'admin::app.settings.data-transfer.validation.errors.column-empty-headers', + self::ERROR_CODE_COLUMN_NAME_INVALID => 'admin::app.settings.data-transfer.validation.errors.column-name-invalid', + self::ERROR_CODE_INVALID_ATTRIBUTE => 'admin::app.settings.data-transfer.validation.errors.invalid-attribute', + self::ERROR_CODE_WRONG_QUOTES => 'admin::app.settings.data-transfer.validation.errors.wrong-quotes', + self::ERROR_CODE_COLUMNS_NUMBER => 'admin::app.settings.data-transfer.validation.errors.column-numbers', + ]; + + public const BATCH_SIZE = 100; + + /** + * Is linking required. + */ + protected bool $linkingRequired = false; + + /** + * Is indexing required. + */ + protected bool $indexingRequired = false; + + /** + * Error helper instance. + * + * @var Error + */ + protected $errorHelper; + + /** + * Import instance. + */ + protected ImportContract $import; + + /** + * Source instance. + * + * @var Source + */ + protected $source; + + /** + * Valid column names. + */ + protected array $validColumnNames = []; + + /** + * Array of numbers of validated rows as keys and boolean TRUE as values. + */ + protected array $validatedRows = []; + + /** + * Number of rows processed by validation. + */ + protected int $processedRowsCount = 0; + + /** + * Number of created items. + */ + protected int $createdItemsCount = 0; + + /** + * Number of updated items. + */ + protected int $updatedItemsCount = 0; + + /** + * Number of deleted items. + */ + protected int $deletedItemsCount = 0; + + /** + * Create a new helper instance. + * + * @return void + */ + public function __construct( + protected ImportBatchRepository $importBatchRepository, + protected AttributeRepository $attributeRepository, + protected AttributeValueRepository $attributeValueRepository + ) {} + + /** + * Validate data row. + */ + abstract public function validateRow(array $rowData, int $rowNumber): bool; + + /** + * Import data rows. + */ + abstract public function importBatch(ImportBatchContract $importBatchContract): bool; + + /** + * Initialize Product error messages. + */ + protected function initErrorMessages(): void + { + foreach ($this->errorMessages as $errorCode => $message) { + $this->errorHelper->addErrorMessage($errorCode, trans($message)); + } + } + + /** + * Import instance. + */ + public function setImport(ImportContract $import): self + { + $this->import = $import; + + return $this; + } + + /** + * Import instance. + * + * @param Source $errorHelper + */ + public function setSource($source) + { + $this->source = $source; + + return $this; + } + + /** + * Import instance. + * + * @param Error $errorHelper + */ + public function setErrorHelper($errorHelper): self + { + $this->errorHelper = $errorHelper; + + $this->initErrorMessages(); + + return $this; + } + + /** + * Import instance. + * + * @return Source + */ + public function getSource() + { + return $this->source; + } + + /** + * Retrieve valid column names. + */ + public function getValidColumnNames(): array + { + return $this->validColumnNames; + } + + /** + * Validate data. + */ + public function validateData(): void + { + Event::dispatch('data_transfer.imports.validate.before', $this->import); + + $errors = []; + + $absentColumns = array_diff($this->permanentAttributes, $columns = $this->getSource()->getColumnNames()); + + if (! empty($absentColumns)) { + $errors[self::ERROR_CODE_COLUMN_NOT_FOUND] = $absentColumns; + } + + foreach ($columns as $columnNumber => $columnName) { + if (empty($columnName)) { + $errors[self::ERROR_CODE_COLUMN_EMPTY_HEADER][] = $columnNumber + 1; + } elseif (! preg_match('/^[a-z][a-z0-9_]*$/', $columnName)) { + $errors[self::ERROR_CODE_COLUMN_NAME_INVALID][] = $columnName; + } elseif (! in_array($columnName, $this->getValidColumnNames())) { + $errors[self::ERROR_CODE_INVALID_ATTRIBUTE][] = $columnName; + } + } + + /** + * Add Columns Errors. + */ + foreach ($errors as $errorCode => $error) { + $this->addErrors($errorCode, $error); + } + + if (! $this->errorHelper->getErrorsCount()) { + $this->saveValidatedBatches(); + } + + Event::dispatch('data_transfer.imports.validate.after', $this->import); + } + + /** + * Save validated batches. + */ + protected function saveValidatedBatches(): self + { + $source = $this->getSource(); + + $batchRows = []; + + $source->rewind(); + + /** + * Clean previous saved batches. + */ + $this->importBatchRepository->deleteWhere([ + 'import_id' => $this->import->id, + ]); + + while ( + $source->valid() + || count($batchRows) + ) { + if ( + count($batchRows) == self::BATCH_SIZE + || ! $source->valid() + ) { + $this->importBatchRepository->create([ + 'import_id' => $this->import->id, + 'data' => $batchRows, + ]); + + $batchRows = []; + } + + if ($source->valid()) { + $rowData = $source->current(); + + if ($this->validateRow($rowData, $source->getCurrentRowNumber())) { + $batchRows[] = $this->prepareRowForDb($rowData); + } + + $this->processedRowsCount++; + + $source->next(); + } + } + + return $this; + } + + /** + * Prepare validation rules. + */ + public function getValidationRules(string $entityType, array $rowData): array + { + if (empty($entityType)) { + return []; + } + + $rules = []; + + $attributes = $this->attributeRepository->scopeQuery(fn ($query) => $query->whereIn('code', array_keys($rowData))->where('entity_type', $entityType))->get(); + + foreach ($attributes as $attribute) { + $validations = []; + + if ($attribute->type == 'boolean') { + continue; + } elseif ($attribute->type == 'address') { + if (! $attribute->is_required) { + continue; + } + + $validations = [ + $attribute->code.'.address' => 'required', + $attribute->code.'.country' => 'required', + $attribute->code.'.state' => 'required', + $attribute->code.'.city' => 'required', + $attribute->code.'.postcode' => 'required', + ]; + } elseif ($attribute->type == 'email') { + $validations = [ + $attribute->code => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable', 'email'], + $attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable', + ]; + } elseif ($attribute->type == 'phone') { + $validations = [ + $attribute->code => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable', + ]; + } else { + $validations[$attribute->code] = [$attribute->is_required ? 'required' : 'nullable']; + + if ($attribute->type == 'text' && $attribute->validation) { + array_push($validations[$attribute->code], + $attribute->validation == 'decimal' + ? new Decimal + : $attribute->validation + ); + } + + if ($attribute->type == 'price') { + array_push($validations[$attribute->code], new Decimal); + } + } + + if ($attribute->is_unique) { + array_push($validations[in_array($attribute->type, ['email', 'phone']) + ? $attribute->code.'.*.value' + : $attribute->code + ], function ($field, $value, $fail) use ($attribute) { + if (! $this->attributeValueRepository->isValueUnique(null, $attribute->entity_type, $attribute, $field)) { + $fail(trans('admin::app.settings.data-transfer.validation.errors.already-exists', ['attribute' => $attribute->name])); + } + }); + } + + $rules = [ + ...$rules, + ...$validations, + ]; + } + + return $rules; + } + + /** + * Start the import process. + */ + public function importData(?ImportBatchContract $importBatch = null): bool + { + if ($importBatch) { + $this->importBatch($importBatch); + + return true; + } + + $typeBatches = []; + + foreach ($this->import->batches as $batch) { + $typeBatches['import'][] = new ImportBatchJob($batch); + + if ($this->isLinkingRequired()) { + $typeBatches['link'][] = new LinkBatchJob($batch); + } + + if ($this->isIndexingRequired()) { + $typeBatches['index'][] = new IndexBatchJob($batch); + } + } + + $chain[] = Bus::batch($typeBatches['import']); + + if (! empty($typeBatches['link'])) { + $chain[] = new LinkingJob($this->import); + + $chain[] = Bus::batch($typeBatches['link']); + } + + if (! empty($typeBatches['index'])) { + $chain[] = new IndexingJob($this->import); + + $chain[] = Bus::batch($typeBatches['index']); + } + + $chain[] = new CompletedJob($this->import); + + Bus::chain($chain)->dispatch(); + + return true; + } + + /** + * Link resource data. + */ + public function linkData(ImportBatchContract $importBatch): bool + { + $this->linkBatch($importBatch); + + return true; + } + + /** + * Index resource data. + */ + public function indexData(ImportBatchContract $importBatch): bool + { + $this->indexBatch($importBatch); + + return true; + } + + /** + * Add errors to error aggregator. + */ + protected function addErrors(string $code, mixed $errors): void + { + $this->errorHelper->addError( + $code, + null, + implode('", "', $errors) + ); + } + + /** + * Add row as skipped. + * + * @param int|null $rowNumber + * @param string|null $columnName + * @param string|null $errorMessage + * @return $this + */ + protected function skipRow($rowNumber, string $errorCode, $columnName = null, $errorMessage = null): self + { + $this->errorHelper->addError( + $errorCode, + $rowNumber, + $columnName, + $errorMessage + ); + + $this->errorHelper->addRowToSkip($rowNumber); + + return $this; + } + + /** + * Prepare row data to save into the database. + */ + protected function prepareRowForDb(array $rowData): array + { + $rowData = array_map(function ($value) { + return $value === '' ? null : $value; + }, $rowData); + + return $rowData; + } + + /** + * Returns number of checked rows. + */ + public function getProcessedRowsCount(): int + { + return $this->processedRowsCount; + } + + /** + * Returns number of created items count. + */ + public function getCreatedItemsCount(): int + { + return $this->createdItemsCount; + } + + /** + * Returns number of updated items count. + */ + public function getUpdatedItemsCount(): int + { + return $this->updatedItemsCount; + } + + /** + * Returns number of deleted items count. + */ + public function getDeletedItemsCount(): int + { + return $this->deletedItemsCount; + } + + /** + * Is linking resource required for the import operation. + */ + public function isLinkingRequired(): bool + { + if ($this->import->action == Import::ACTION_DELETE) { + return false; + } + + return $this->linkingRequired; + } + + /** + * Is indexing resource required for the import operation. + */ + public function isIndexingRequired(): bool + { + if ($this->import->action == Import::ACTION_DELETE) { + return false; + } + + return $this->indexingRequired; + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Importers/Leads/Importer.php b/packages/Webkul/DataTransfer/src/Helpers/Importers/Leads/Importer.php new file mode 100644 index 0000000..b48c74b --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Importers/Leads/Importer.php @@ -0,0 +1,623 @@ + 'admin::app.settings.data-transfer.importers.leads.validation.errors.id-not-found', + ]; + + /** + * Permanent entity columns. + * + * @var string[] + */ + protected $permanentAttributes = ['title']; + + /** + * Permanent entity column. + */ + protected string $masterAttributeCode = 'id'; + + /** + * Is linking required + */ + protected bool $linkingRequired = true; + + /** + * Create a new helper instance. + * + * @return void + */ + public function __construct( + protected ImportBatchRepository $importBatchRepository, + protected LeadRepository $leadRepository, + protected LeadProductRepository $leadProductRepository, + protected AttributeRepository $attributeRepository, + protected AttributeValueRepository $attributeValueRepository, + protected Storage $leadsStorage, + ) { + parent::__construct( + $importBatchRepository, + $attributeRepository, + $attributeValueRepository, + ); + + $this->initAttributes(); + } + + /** + * Append the lead attribute codes (including user defined ones) to the list of valid columns + * so that custom attributes can be imported alongside the built-in fields. + */ + protected function initAttributes(): void + { + $attributes = $this->attributeRepository->findWhere(['entity_type' => 'leads']); + + foreach ($attributes as $attribute) { + $this->validColumnNames[] = $attribute->code; + } + } + + /** + * Initialize leads error templates. + */ + protected function initErrorMessages(): void + { + foreach ($this->messages as $errorCode => $message) { + $this->errorHelper->addErrorMessage($errorCode, trans($message)); + } + + parent::initErrorMessages(); + } + + /** + * Validate data. + */ + public function validateData(): void + { + $this->leadsStorage->init(); + + parent::validateData(); + } + + /** + * Validates row. + */ + public function validateRow(array $rowData, int $rowNumber): bool + { + /** + * If row is already validated than no need for further validation. + */ + if (isset($this->validatedRows[$rowNumber])) { + return ! $this->errorHelper->isRowInvalid($rowNumber); + } + + $this->validatedRows[$rowNumber] = true; + + /** + * If import action is delete than no need for further validation. + */ + if ($this->import->action == Import::ACTION_DELETE) { + if (! $this->isTitleExist($rowData['title'])) { + $this->skipRow($rowNumber, self::ERROR_ID_NOT_FOUND_FOR_DELETE, 'id'); + + return false; + } + + return true; + } + + if (! empty($rowData['product'])) { + $product = $this->parseProducts($rowData['product']); + + $validator = Validator::make($product, [ + 'id' => 'required|exists:products,id', + 'price' => 'required', + 'quantity' => 'required', + ]); + + if ($validator->fails()) { + $failedAttributes = $validator->failed(); + + foreach ($validator->errors()->getMessages() as $attributeCode => $message) { + $errorCode = array_key_first($failedAttributes[$attributeCode] ?? []); + + $this->skipRow($rowNumber, $errorCode, $attributeCode, current($message)); + } + } + } + + /** + * Validate leads attributes. + */ + $validator = Validator::make($rowData, [ + ...$this->getValidationRules('leads|persons', $rowData), + 'id' => 'numeric', + 'status' => 'sometimes|required|in:0,1', + 'user_id' => 'required|exists:users,id', + 'person_id' => 'required|exists:persons,id', + 'lead_source_id' => 'required|exists:lead_sources,id', + 'lead_type_id' => 'required|exists:lead_types,id', + 'lead_pipeline_id' => 'required|exists:lead_pipelines,id', + 'lead_pipeline_stage_id' => 'required|exists:lead_pipeline_stages,id', + ]); + + if ($validator->fails()) { + $failedAttributes = $validator->failed(); + + foreach ($validator->errors()->getMessages() as $attributeCode => $message) { + $errorCode = array_key_first($failedAttributes[$attributeCode] ?? []); + + $this->skipRow($rowNumber, $errorCode, $attributeCode, current($message)); + } + } + + return ! $this->errorHelper->isRowInvalid($rowNumber); + } + + /** + * Prepare row data for lead product. + */ + protected function parseProducts(?string $products): array + { + $productData = []; + + $productArray = explode(',', $products); + + foreach ($productArray as $product) { + if (empty($product)) { + continue; + } + + [$key, $value] = explode('=', $product); + + $productData[$key] = $value; + } + + if ( + isset($productData['price']) + && isset($productData['quantity']) + ) { + $productData['amount'] = $productData['price'] * $productData['quantity']; + } + + return $productData; + } + + /** + * Get validation rules. + */ + public function getValidationRules(string $entityTypes, array $rowData): array + { + $rules = []; + + foreach (explode('|', $entityTypes) as $entityType) { + $attributes = $this->attributeRepository->scopeQuery(fn ($query) => $query->whereIn('code', array_keys($rowData))->where('entity_type', $entityType))->get(); + + foreach ($attributes as $attribute) { + if ($entityType == 'persons') { + $attribute->code = 'person.'.$attribute->code; + } + + $validations = []; + + if ($attribute->type == 'boolean') { + continue; + } elseif ($attribute->type == 'address') { + if (! $attribute->is_required) { + continue; + } + + $validations = [ + $attribute->code.'.address' => 'required', + $attribute->code.'.country' => 'required', + $attribute->code.'.state' => 'required', + $attribute->code.'.city' => 'required', + $attribute->code.'.postcode' => 'required', + ]; + } elseif ($attribute->type == 'email') { + $validations = [ + $attribute->code => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable', 'email'], + $attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable', + ]; + } elseif ($attribute->type == 'phone') { + $validations = [ + $attribute->code => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable'], + $attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable', + ]; + } else { + $validations[$attribute->code] = [$attribute->is_required ? 'required' : 'nullable']; + + if ($attribute->type == 'text' && $attribute->validation) { + array_push($validations[$attribute->code], + $attribute->validation == 'decimal' + ? new Decimal + : $attribute->validation + ); + } + + if ($attribute->type == 'price') { + array_push($validations[$attribute->code], new Decimal); + } + } + + if ($attribute->is_unique) { + array_push($validations[in_array($attribute->type, ['email', 'phone']) + ? $attribute->code.'.*.value' + : $attribute->code + ], function ($field, $value, $fail) use ($attribute) { + if (! $this->attributeValueRepository->isValueUnique( + null, + $attribute->entity_type, + $attribute, + request($field) + ) + ) { + $fail(trans('admin::app.settings.data-transfer.validation.errors.already-exists', ['attribute' => $attribute->name])); + } + }); + } + + $rules = [ + ...$rules, + ...$validations, + ]; + } + } + + return $rules; + } + + /** + * Start the import process. + */ + public function importBatch(ImportBatchContract $batch): bool + { + Event::dispatch('data_transfer.imports.batch.import.before', $batch); + + if ($batch->import->action == Import::ACTION_DELETE) { + $this->deleteLeads($batch); + } else { + $this->saveLeads($batch); + } + + /** + * Update import batch summary. + */ + $batch = $this->importBatchRepository->update([ + 'state' => Import::STATE_PROCESSED, + + 'summary' => [ + 'created' => $this->getCreatedItemsCount(), + 'updated' => $this->getUpdatedItemsCount(), + 'deleted' => $this->getDeletedItemsCount(), + ], + ], $batch->id); + + Event::dispatch('data_transfer.imports.batch.import.after', $batch); + + return true; + } + + /** + * Start the products linking process + */ + public function linkBatch(ImportBatchContract $batch): bool + { + Event::dispatch('data_transfer.imports.batch.linking.before', $batch); + + /** + * Load leads storage with batch ids. + */ + $this->leadsStorage->load(Arr::pluck($batch->data, 'title')); + + $products = []; + + foreach ($batch->data as $rowData) { + /** + * Prepare products. + */ + $this->prepareProducts($rowData, $products); + } + + $this->saveProducts($products); + + /** + * Update import batch summary + */ + $this->importBatchRepository->update([ + 'state' => Import::STATE_LINKED, + ], $batch->id); + + Event::dispatch('data_transfer.imports.batch.linking.after', $batch); + + return true; + } + + /** + * Prepare products. + */ + public function prepareProducts($rowData, &$product): void + { + if (! empty($rowData['product'])) { + $product[$rowData['title']] = $this->parseProducts($rowData['product']); + } + } + + /** + * Save products. + */ + public function saveProducts(array $products): void + { + $leadProducts = []; + + foreach ($products as $title => $product) { + $lead = $this->leadsStorage->get($title); + + $leadProducts['insert'][] = [ + 'lead_id' => $lead['id'], + 'product_id' => $product['id'], + 'price' => $product['price'], + 'quantity' => $product['quantity'], + 'amount' => $product['amount'], + ]; + } + + /** + * Nothing to link when none of the rows in this batch reference a product. + */ + if (empty($leadProducts['insert'])) { + return; + } + + foreach ($leadProducts['insert'] as $key => $leadProduct) { + $this->leadProductRepository->deleteWhere([ + 'lead_id' => $leadProduct['lead_id'], + 'product_id' => $leadProduct['product_id'], + ]); + } + + $this->leadProductRepository->upsert($leadProducts['insert'], ['lead_id', 'product_id']); + } + + /** + * Delete leads from current batch. + */ + protected function deleteLeads(ImportBatchContract $batch): bool + { + /** + * Load leads storage with batch ids. + */ + $this->leadsStorage->load(Arr::pluck($batch->data, 'title')); + + $idsToDelete = []; + + foreach ($batch->data as $rowData) { + if (! $this->isTitleExist($rowData['title'])) { + continue; + } + + $idsToDelete[] = $this->leadsStorage->get($rowData['title']); + } + + $idsToDelete = array_unique($idsToDelete); + + $this->deletedItemsCount = count($idsToDelete); + + $this->leadRepository->deleteWhere([['id', 'IN', $idsToDelete]]); + + return true; + } + + /** + * Save leads from current batch. + */ + protected function saveLeads(ImportBatchContract $batch): bool + { + /** + * Load lead storage with batch unique title. + */ + $this->leadsStorage->load(Arr::pluck($batch->data, 'title')); + + $leads = []; + + $attributeValues = []; + + /** + * Prepare leads for import. + */ + foreach ($batch->data as $rowData) { + /** + * Custom (user defined) attribute values are not columns on the leads table; they are + * persisted separately via saveAttributeValues(). Only the native columns are written + * to the leads table here. + */ + $native = Arr::only($rowData, $this->getEntityColumns()); + + if (isset($rowData['id'])) { + $leads['update'][$rowData['id']] = $native; + } else { + $leads['insert'][$rowData['title']] = [ + ...Arr::except($native, ['id']), + 'created_at' => $rowData['created_at'] ?? now(), + 'updated_at' => $rowData['updated_at'] ?? now(), + ]; + } + + $this->prepareAttributeValues($rowData, $attributeValues); + } + + if (! empty($leads['update'])) { + $this->updatedItemsCount += count($leads['update']); + + $this->leadRepository->upsert( + $leads['update'], + $this->masterAttributeCode + ); + } + + if (! empty($leads['insert'])) { + $this->createdItemsCount += count($leads['insert']); + + $this->leadRepository->insert($leads['insert']); + + /** + * Update the sku storage with newly created products + */ + $newLeads = $this->leadRepository->findWhereIn( + 'title', + array_keys($leads['insert']), + [ + 'id', + 'title', + ] + ); + + foreach ($newLeads as $lead) { + $this->leadsStorage->set($lead->title, [ + 'id' => $lead->id, + 'title' => $lead->title, + ]); + } + } + + $this->saveAttributeValues($attributeValues); + + return true; + } + + /** + * The native columns of the leads table (custom attribute values are stored separately). + */ + protected function getEntityColumns(): array + { + static $columns; + + return $columns ??= Schema::getColumnListing('leads'); + } + + /** + * Map each row's lead attribute values (including user defined ones), keyed by lead title. + */ + public function prepareAttributeValues(array $rowData, array &$attributeValues): void + { + foreach ($rowData as $code => $value) { + if (is_null($value)) { + continue; + } + + $attribute = $this->attributeRepository->findOneWhere([ + 'code' => $code, + 'entity_type' => 'leads', + ]); + + if (! $attribute) { + continue; + } + + $attributeTypeValues = array_fill_keys(array_values(AttributeValue::$attributeTypeFields), null); + + $attributeValues[$rowData['title']][] = array_merge($attributeTypeValues, [ + 'attribute_id' => $attribute->id, + AttributeValue::$attributeTypeFields[$attribute->type] => $value, + ]); + } + } + + /** + * Upsert the collected lead attribute values into the EAV attribute_values table. + */ + public function saveAttributeValues(array $attributeValues): void + { + $leadAttributeValues = []; + + foreach ($attributeValues as $title => $values) { + $lead = $this->leadsStorage->get($title); + + if (! $lead) { + continue; + } + + foreach ($values as $attribute) { + $attribute['entity_id'] = (int) $lead['id']; + + $attribute['unique_id'] = implode('|', array_filter([ + $attribute['entity_id'], + $attribute['attribute_id'], + ])); + + $attribute['entity_type'] = 'leads'; + + $leadAttributeValues[$attribute['unique_id']] = $attribute; + } + } + + if (! empty($leadAttributeValues)) { + $this->attributeValueRepository->upsert($leadAttributeValues, 'unique_id'); + } + } + + /** + * Check if title exists. + */ + public function isTitleExist(string $title): bool + { + return $this->leadsStorage->has($title); + } + + /** + * Prepare row data to save into the database. + */ + protected function prepareRowForDb(array $rowData): array + { + return parent::prepareRowForDb($rowData); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Importers/Leads/Storage.php b/packages/Webkul/DataTransfer/src/Helpers/Importers/Leads/Storage.php new file mode 100644 index 0000000..dd2d369 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Importers/Leads/Storage.php @@ -0,0 +1,97 @@ +items = []; + + $this->load(); + } + + /** + * Load the leads. + */ + public function load(array $titles = []): void + { + if (empty($titles)) { + $leads = $this->leadRepository->all($this->selectColumns); + } else { + $leads = $this->leadRepository->findWhereIn('title', $titles, $this->selectColumns); + } + + foreach ($leads as $lead) { + $this->set($lead->title, [ + 'id' => $lead->id, + 'title' => $lead->title, + ]); + } + } + + /** + * Get Ids and Unique Id. + */ + public function set(string $title, array $data): self + { + $this->items[$title] = $data; + + return $this; + } + + /** + * Check if unique id exists. + */ + public function has(string $title): bool + { + return isset($this->items[$title]); + } + + /** + * Get unique id information. + */ + public function get(string $title): ?array + { + if (! $this->has($title)) { + return null; + } + + return $this->items[$title]; + } + + public function getItems(): array + { + return $this->items; + } + + /** + * Is storage is empty. + */ + public function isEmpty(): bool + { + return empty($this->items); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Importers/Persons/Importer.php b/packages/Webkul/DataTransfer/src/Helpers/Importers/Persons/Importer.php new file mode 100644 index 0000000..4d15e7f --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Importers/Persons/Importer.php @@ -0,0 +1,704 @@ + 'admin::app.settings.data-transfer.importers.persons.validation.errors.email-not-found', + self::ERROR_DUPLICATE_EMAIL => 'admin::app.settings.data-transfer.importers.persons.validation.errors.duplicate-email', + self::ERROR_DUPLICATE_PHONE => 'admin::app.settings.data-transfer.importers.persons.validation.errors.duplicate-phone', + self::ERROR_INVALID_DATE => 'admin::app.settings.data-transfer.importers.persons.validation.errors.invalid-date', + ]; + + /** + * Permanent entity columns. + * + * @var string[] + */ + protected $permanentAttributes = ['emails']; + + /** + * Column used to match existing persons when upserting. The person's primary key is used (like + * the leads importer) so a re-imported row updates the existing record by id, rather than the + * composite `unique_id` which embeds the phone and organization and breaks the match when they + * change. + */ + protected string $masterAttributeCode = 'id'; + + /** + * Emails storage. + */ + protected array $emails = []; + + /** + * Phones storage. + */ + protected array $phones = []; + + /** + * Cache of resolved person attributes, keyed by attribute code. + */ + protected array $attributeCache = []; + + /** + * Cache of person date/datetime attribute codes mapped to their type. + */ + protected ?array $dateAttributeCodes = null; + + /** + * Create a new helper instance. + * + * @return void + */ + public function __construct( + protected ImportBatchRepository $importBatchRepository, + protected PersonRepository $personRepository, + protected AttributeRepository $attributeRepository, + protected AttributeValueRepository $attributeValueRepository, + protected Storage $personStorage, + ) { + parent::__construct( + $importBatchRepository, + $attributeRepository, + $attributeValueRepository, + ); + + $this->initAttributes(); + } + + /** + * Append the person attribute codes (including user defined ones) to the list of valid columns + * so that custom attributes can be imported alongside the built-in fields. + */ + protected function initAttributes(): void + { + $attributes = $this->attributeRepository->findWhere(['entity_type' => 'persons']); + + foreach ($attributes as $attribute) { + $this->validColumnNames[] = $attribute->code; + } + } + + /** + * The native columns of the persons table (custom attribute values are stored separately). + */ + protected function getEntityColumns(): array + { + static $columns; + + return $columns ??= Schema::getColumnListing('persons'); + } + + /** + * Initialize Product error templates. + */ + protected function initErrorMessages(): void + { + foreach ($this->messages as $errorCode => $message) { + $this->errorHelper->addErrorMessage($errorCode, trans($message)); + } + + parent::initErrorMessages(); + } + + /** + * Validate data. + */ + public function validateData(): void + { + $this->personStorage->init(); + + parent::validateData(); + } + + /** + * Validates row. + */ + public function validateRow(array $rowData, int $rowNumber): bool + { + $rowData = $this->parsedRowData($rowData); + + /** + * If row is already validated than no need for further validation. + */ + if (isset($this->validatedRows[$rowNumber])) { + return ! $this->errorHelper->isRowInvalid($rowNumber); + } + + $this->validatedRows[$rowNumber] = true; + + /** + * If import action is delete than no need for further validation. + */ + if ($this->import->action == Import::ACTION_DELETE) { + foreach ($rowData['emails'] as $email) { + if (! $this->isEmailExist($email['value'])) { + $this->skipRow($rowNumber, self::ERROR_EMAIL_NOT_FOUND_FOR_DELETE, 'email'); + + return false; + } + + return true; + } + } + + /** + * Validate row data. + */ + $validator = Validator::make($rowData, [ + ...$this->getValidationRules('persons', $rowData), + 'organization_id' => 'nullable|exists:organizations,id', + 'user_id' => 'required|exists:users,id', + 'contact_numbers' => 'nullable|array', + 'contact_numbers.*.value' => 'required|numeric', + 'contact_numbers.*.label' => 'required|in:home,work', + 'emails' => 'required|array', + 'emails.*.value' => 'required|email', + 'emails.*.label' => 'required|in:home,work', + ]); + + if ($validator->fails()) { + $failedAttributes = $validator->failed(); + + foreach ($validator->errors()->getMessages() as $attributeCode => $message) { + $errorCode = array_key_first($failedAttributes[$attributeCode] ?? []); + + $this->skipRow($rowNumber, $errorCode, $attributeCode, current($message)); + } + } + + /** + * Check if email is unique. + */ + if (! empty($emails = $rowData['emails'])) { + foreach ($emails as $email) { + if (! in_array($email['value'], $this->emails)) { + $this->emails[] = $email['value']; + } else { + $message = sprintf( + trans($this->messages[self::ERROR_DUPLICATE_EMAIL]), + $email['value'] + ); + + $this->skipRow($rowNumber, self::ERROR_DUPLICATE_EMAIL, 'email', $message); + } + } + } + + /** + * Check if phone(s) are unique. + */ + if (! empty($rowData['contact_numbers'])) { + foreach ($rowData['contact_numbers'] as $phone) { + if (! in_array($phone['value'], $this->phones)) { + if (! empty($phone['value'])) { + $this->phones[] = $phone['value']; + } + } else { + $message = sprintf( + trans($this->messages[self::ERROR_DUPLICATE_PHONE]), + $phone['value'] + ); + + $this->skipRow($rowNumber, self::ERROR_DUPLICATE_PHONE, 'phone', $message); + } + } + } + + /** + * Reject any date/datetime attribute value that cannot be parsed, so a malformed date is + * flagged as a row error instead of silently persisting as a zero date (0000-00-00). + */ + foreach ($this->getDateAttributeCodes() as $code => $type) { + $value = $rowData[$code] ?? null; + + if (! is_null($value) && $value !== '' && is_null($this->normalizeDate($value, $type))) { + $message = sprintf( + trans($this->messages[self::ERROR_INVALID_DATE]), + $value + ); + + $this->skipRow($rowNumber, self::ERROR_INVALID_DATE, $code, $message); + } + } + + return ! $this->errorHelper->isRowInvalid($rowNumber); + } + + /** + * Start the import process. + */ + public function importBatch(ImportBatchContract $batch): bool + { + Event::dispatch('data_transfer.imports.batch.import.before', $batch); + + if ($batch->import->action == Import::ACTION_DELETE) { + $this->deletePersons($batch); + } else { + $this->savePersonData($batch); + } + + /** + * Update import batch summary. + */ + $batch = $this->importBatchRepository->update([ + 'state' => Import::STATE_PROCESSED, + + 'summary' => [ + 'created' => $this->getCreatedItemsCount(), + 'updated' => $this->getUpdatedItemsCount(), + 'deleted' => $this->getDeletedItemsCount(), + ], + ], $batch->id); + + Event::dispatch('data_transfer.imports.batch.import.after', $batch); + + return true; + } + + /** + * Delete persons from current batch. + */ + protected function deletePersons(ImportBatchContract $batch): bool + { + /** + * Load person storage with batch emails. + */ + $emails = collect(Arr::pluck($batch->data, 'emails')) + ->map(function ($emails) { + $emails = json_decode($emails, true); + + foreach ($emails as $email) { + return $email['value']; + } + }); + + $this->personStorage->load($emails->toArray()); + + $idsToDelete = []; + + foreach ($batch->data as $rowData) { + $rowData = $this->parsedRowData($rowData); + + foreach ($rowData['emails'] as $email) { + if (! $this->isEmailExist($email['value'])) { + continue; + } + + $idsToDelete[] = $this->personStorage->get($email['value']); + } + } + + $idsToDelete = array_unique($idsToDelete); + + $this->deletedItemsCount = count($idsToDelete); + + $this->personRepository->deleteWhere([['id', 'IN', $idsToDelete]]); + + return true; + } + + /** + * Save person from current batch. + */ + protected function savePersonData(ImportBatchContract $batch): bool + { + /** + * Load person storage with batch email. + */ + $emails = collect(Arr::pluck($batch->data, 'emails')) + ->map(function ($emails) { + $emails = json_decode($emails, true); + + foreach ($emails as $email) { + return $email['value']; + } + }); + + $this->personStorage->load($emails->toArray()); + + $persons = []; + + $attributeValues = []; + + /** + * Prepare persons for import. + */ + foreach ($batch->data as $rowData) { + $this->preparePersons($rowData, $persons); + + $this->prepareAttributeValues($rowData, $attributeValues); + } + + $this->savePersons($persons); + + $this->saveAttributeValues($attributeValues); + + return true; + } + + /** + * Prepare persons from current batch. + */ + public function preparePersons(array $rowData, array &$persons): void + { + $emails = $this->prepareEmail($rowData['emails']); + + foreach ($emails as $email) { + $contactNumber = json_decode($rowData['contact_numbers'], true); + + $rowData['unique_id'] = "{$rowData['user_id']}|{$rowData['organization_id']}|{$email}|{$contactNumber[0]['value']}"; + + /** + * Custom (user defined) attribute values are not columns on the persons table; they are + * persisted separately via saveAttributeValues(). Only the native columns are written + * to the persons table here. + */ + $native = Arr::only($rowData, $this->getEntityColumns()); + + if ($this->isEmailExist($email)) { + /** + * Update the existing person by its primary key. Keying the update on the person id + * (resolved from the matched email) rather than the composite `unique_id` — which + * embeds the phone and organization — ensures a revised row whose phone or + * organization changed still updates the existing record instead of inserting a + * duplicate. + */ + $native['id'] = $this->personStorage->get($email); + + $persons['update'][$email] = $native; + } else { + $persons['insert'][$email] = [ + ...$native, + 'created_at' => $rowData['created_at'] ?? now(), + 'updated_at' => $rowData['updated_at'] ?? now(), + ]; + } + } + } + + /** + * Save persons from current batch. + */ + public function savePersons(array $persons): void + { + if (! empty($persons['update'])) { + $this->updatedItemsCount += count($persons['update']); + + $this->personRepository->upsert( + $persons['update'], + $this->masterAttributeCode, + ); + } + + if (! empty($persons['insert'])) { + $this->createdItemsCount += count($persons['insert']); + + $this->personRepository->insert($persons['insert']); + + /** + * Update the sku storage with newly created products + */ + $emails = array_keys($persons['insert']); + + $newPersons = $this->personRepository->where(function ($query) use ($emails) { + foreach ($emails as $email) { + $query->orWhereJsonContains('emails', [['value' => $email]]); + } + })->get(); + + foreach ($newPersons as $person) { + $this->personStorage->set($person->emails[0]['value'], $person->id); + } + } + } + + /** + * Save attribute values for the person. + */ + public function saveAttributeValues(array $attributeValues): void + { + $personAttributeValues = []; + + foreach ($attributeValues as $email => $attributeValue) { + foreach ($attributeValue as $attribute) { + $attribute['entity_id'] = (int) $this->personStorage->get($email); + + $attribute['unique_id'] = implode('|', array_filter([ + $attribute['entity_id'], + $attribute['attribute_id'], + ])); + + $attribute['entity_type'] = 'persons'; + + $personAttributeValues[$attribute['unique_id']] = $attribute; + } + } + + $this->attributeValueRepository->upsert($personAttributeValues, 'unique_id'); + } + + /** + * Check if email exists. + */ + public function isEmailExist(string $email): bool + { + return $this->personStorage->has($email); + } + + /** + * Prepare attribute values for the person. + */ + public function prepareAttributeValues(array $rowData, array &$attributeValues): void + { + foreach ($rowData as $code => $value) { + if (is_null($value) || $value === '') { + continue; + } + + $attribute = $this->getPersonAttribute($code); + + if (! $attribute) { + continue; + } + + /** + * Convert the raw CSV cell into the value stored for this attribute type — option + * labels (or ids) are resolved to their option ids, and date/datetime values (Excel + * serials and regional formats) are normalised to the ISO storage format. A value that + * cannot be resolved or parsed is skipped, so an existing value is never overwritten + * with an invalid one (the integer `0` a label used to become, or a zero date). + */ + $storedValue = $this->formatAttributeValue($attribute, $value); + + if ( + is_null($storedValue) + && in_array($attribute->type, ['select', 'multiselect', 'checkbox']) + ) { + continue; + } + + if (in_array($attribute->type, ['date', 'datetime'])) { + $storedValue = $this->normalizeDate($storedValue, $attribute->type); + + if (is_null($storedValue)) { + continue; + } + } + + $typeFields = $this->personRepository->getModel()::$attributeTypeFields; + + $attributeTypeValues = array_fill_keys(array_values($typeFields), null); + + $emails = $this->prepareEmail($rowData['emails']); + + foreach ($emails as $email) { + $attributeValues[$email][] = array_merge($attributeTypeValues, [ + 'attribute_id' => $attribute->id, + $typeFields[$attribute->type] => $storedValue, + ]); + } + } + } + + /** + * Resolve (and cache) a person attribute by its code. + */ + protected function getPersonAttribute(string $code) + { + if (array_key_exists($code, $this->attributeCache)) { + return $this->attributeCache[$code]; + } + + return $this->attributeCache[$code] = $this->attributeRepository->findOneWhere([ + 'code' => $code, + 'entity_type' => 'persons', + ]); + } + + /** + * Convert a raw imported value into the value persisted for the attribute's type. + * + * Choice attributes (select/multiselect/checkbox) are stored as option ids, so an imported + * option label is resolved to its id (a numeric id is accepted as-is). A null return means the + * value could not be resolved. + */ + protected function formatAttributeValue($attribute, $value) + { + if ($attribute->type === 'select') { + return $this->resolveOptionId($attribute, $value); + } + + if (in_array($attribute->type, ['multiselect', 'checkbox'])) { + $ids = collect(explode(',', (string) $value)) + ->map(fn ($label) => $this->resolveOptionId($attribute, trim($label))) + ->filter() + ->implode(','); + + return $ids !== '' ? $ids : null; + } + + return $value; + } + + /** + * Resolve an imported option label (case-insensitive) — or a raw option id — to its option id. + */ + protected function resolveOptionId($attribute, $label): ?int + { + if ($label === null || $label === '') { + return null; + } + + $option = $attribute->options->first(function ($option) use ($label) { + return strcasecmp((string) $option->name, (string) $label) === 0 + || (is_numeric($label) && (int) $option->id === (int) $label); + }); + + return $option?->id; + } + + /** + * The person date/datetime attribute codes, mapped to their type (cached). + */ + protected function getDateAttributeCodes(): array + { + if (! is_null($this->dateAttributeCodes)) { + return $this->dateAttributeCodes; + } + + return $this->dateAttributeCodes = $this->attributeRepository + ->findWhere(['entity_type' => 'persons']) + ->whereIn('type', ['date', 'datetime']) + ->pluck('type', 'code') + ->all(); + } + + /** + * Normalise an imported date to the storage format, accepting the ISO format, Excel/spreadsheet + * serial numbers and common regional (day-first) formats. Returns null when the value cannot be + * parsed so an invalid date is never stored as a zero date (0000-00-00). + */ + protected function normalizeDate($value, string $type): ?string + { + $value = trim((string) $value); + + if ($value === '') { + return null; + } + + /** + * A bare integer is a spreadsheet serial number — days since 1899-12-30, which also absorbs + * Excel's fictitious 1900 leap year. + */ + if (ctype_digit($value)) { + $date = Carbon::create(1899, 12, 30, 0, 0, 0)->addDays((int) $value); + } else { + $date = null; + + foreach (['Y-m-d', 'd/m/Y', 'd-m-Y', 'Y/m/d', 'm/d/Y'] as $format) { + if (Carbon::hasFormat($value, $format)) { + $date = Carbon::createFromFormat($format, $value); + + break; + } + } + + if (is_null($date)) { + try { + $date = Carbon::parse($value); + } catch (\Exception $e) { + return null; + } + } + } + + return $type === 'datetime' ? $date->format('Y-m-d H:i:s') : $date->format('Y-m-d'); + } + + /** + * Get parsed email and phone. + */ + private function parsedRowData(array $rowData): array + { + $rowData['emails'] = json_decode($rowData['emails'], true); + + $rowData['contact_numbers'] = json_decode($rowData['contact_numbers'], true); + + return $rowData; + } + + /** + * Prepare email from row data. + */ + private function prepareEmail(array|string $emails): Collection + { + static $cache = []; + + return collect($emails) + ->map(function ($emailString) use (&$cache) { + if (isset($cache[$emailString])) { + return $cache[$emailString]; + } + + $decoded = json_decode($emailString, true); + + $emailValue = is_array($decoded) + && isset($decoded[0]['value']) + ? $decoded[0]['value'] + : null; + + return $cache[$emailString] = $emailValue; + }); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Importers/Persons/Storage.php b/packages/Webkul/DataTransfer/src/Helpers/Importers/Persons/Storage.php new file mode 100644 index 0000000..f18761b --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Importers/Persons/Storage.php @@ -0,0 +1,99 @@ +items = []; + + $this->load(); + } + + /** + * Load the Emails. + */ + public function load(array $emails = []): void + { + if (empty($emails)) { + $persons = $this->personRepository->all($this->selectColumns); + } else { + $persons = $this->personRepository->scopeQuery(function ($query) use ($emails) { + return $query->where(function ($subQuery) use ($emails) { + foreach ($emails as $email) { + $subQuery->orWhereJsonContains('emails', ['value' => $email]); + } + }); + })->all($this->selectColumns); + } + + $persons->each(function ($person) { + collect($person->emails) + ->each(fn ($email) => $this->set($email['value'], $person->id)); + }); + } + + /** + * Get email information. + */ + public function set(string $email, int $id): self + { + $this->items[$email] = $id; + + return $this; + } + + /** + * Check if email exists. + */ + public function has(string $email): bool + { + return isset($this->items[$email]); + } + + /** + * Get email information. + */ + public function get(string $email): ?int + { + if (! $this->has($email)) { + return null; + } + + return $this->items[$email]; + } + + /** + * Is storage is empty. + */ + public function isEmpty(): int + { + return empty($this->items); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Importers/Products/Importer.php b/packages/Webkul/DataTransfer/src/Helpers/Importers/Products/Importer.php new file mode 100644 index 0000000..6974232 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Importers/Products/Importer.php @@ -0,0 +1,369 @@ + 'admin::app.settings.data-transfer.importers.products.validation.errors.sku-not-found', + ]; + + /** + * Permanent entity columns. + */ + protected array $permanentAttributes = ['sku']; + + /** + * Permanent entity column. + */ + protected string $masterAttributeCode = 'sku'; + + /** + * Cached attributes. + */ + protected mixed $attributes = []; + + /** + * Valid csv columns. + */ + protected array $validColumnNames = [ + 'sku', + 'name', + 'description', + 'quantity', + 'price', + ]; + + /** + * Create a new helper instance. + * + * @return void + */ + public function __construct( + protected ImportBatchRepository $importBatchRepository, + protected AttributeRepository $attributeRepository, + protected AttributeOptionRepository $attributeOptionRepository, + protected ProductRepository $productRepository, + protected ProductInventoryRepository $productInventoryRepository, + protected AttributeValueRepository $attributeValueRepository, + protected SKUStorage $skuStorage + ) { + parent::__construct( + $importBatchRepository, + $attributeRepository, + $attributeValueRepository + ); + + $this->initAttributes(); + } + + /** + * Load all attributes and families to use later. + */ + protected function initAttributes(): void + { + $this->attributes = $this->attributeRepository->findWhere(['entity_type' => 'products']); + + foreach ($this->attributes as $attribute) { + $this->validColumnNames[] = $attribute->code; + } + } + + /** + * Initialize Product error templates. + */ + protected function initErrorMessages(): void + { + foreach ($this->messages as $errorCode => $message) { + $this->errorHelper->addErrorMessage($errorCode, trans($message)); + } + + parent::initErrorMessages(); + } + + /** + * Save validated batches. + */ + protected function saveValidatedBatches(): self + { + $source = $this->getSource(); + + $source->rewind(); + + $this->skuStorage->init(); + + while ($source->valid()) { + try { + $rowData = $source->current(); + } catch (\InvalidArgumentException $e) { + $source->next(); + + continue; + } + + $this->validateRow($rowData, $source->getCurrentRowNumber()); + + $source->next(); + } + + parent::saveValidatedBatches(); + + return $this; + } + + /** + * Validates row. + */ + public function validateRow(array $rowData, int $rowNumber): bool + { + /** + * If row is already validated than no need for further validation. + */ + if (isset($this->validatedRows[$rowNumber])) { + return ! $this->errorHelper->isRowInvalid($rowNumber); + } + + $this->validatedRows[$rowNumber] = true; + + /** + * If import action is delete than no need for further validation. + */ + if ($this->import->action == Import::ACTION_DELETE) { + if (! $this->isSKUExist($rowData['sku'])) { + $this->skipRow($rowNumber, self::ERROR_SKU_NOT_FOUND_FOR_DELETE, 'sku'); + + return false; + } + + return true; + } + + /** + * Validate product attributes + */ + $validator = Validator::make($rowData, $this->getValidationRules('products', $rowData)); + + if ($validator->fails()) { + foreach ($validator->errors()->getMessages() as $attributeCode => $message) { + $failedAttributes = $validator->failed(); + + $errorCode = array_key_first($failedAttributes[$attributeCode] ?? []); + + $this->skipRow($rowNumber, $errorCode, $attributeCode, current($message)); + } + } + + return ! $this->errorHelper->isRowInvalid($rowNumber); + } + + /** + * Start the import process. + */ + public function importBatch(ImportBatchContract $batch): bool + { + Event::dispatch('data_transfer.imports.batch.import.before', $batch); + + if ($batch->import->action == Import::ACTION_DELETE) { + $this->deleteProducts($batch); + } else { + $this->saveProductsData($batch); + } + + /** + * Update import batch summary. + */ + $batch = $this->importBatchRepository->update([ + 'state' => Import::STATE_PROCESSED, + + 'summary' => [ + 'created' => $this->getCreatedItemsCount(), + 'updated' => $this->getUpdatedItemsCount(), + 'deleted' => $this->getDeletedItemsCount(), + ], + ], $batch->id); + + Event::dispatch('data_transfer.imports.batch.import.after', $batch); + + return true; + } + + /** + * Delete products from current batch. + */ + protected function deleteProducts(ImportBatchContract $batch): bool + { + /** + * Load SKU storage with batch skus. + */ + $this->skuStorage->load(Arr::pluck($batch->data, 'sku')); + + $idsToDelete = []; + + foreach ($batch->data as $rowData) { + if (! $this->isSKUExist($rowData['sku'])) { + continue; + } + + $product = $this->skuStorage->get($rowData['sku']); + + $idsToDelete[] = $product['id']; + } + + $idsToDelete = array_unique($idsToDelete); + + $this->deletedItemsCount = count($idsToDelete); + + $this->productRepository->deleteWhere([['id', 'IN', $idsToDelete]]); + + return true; + } + + /** + * Save products from current batch. + */ + protected function saveProductsData(ImportBatchContract $batch): bool + { + /** + * Load SKU storage with batch skus. + */ + $this->skuStorage->load(Arr::pluck($batch->data, 'sku')); + + $products = []; + + /** + * Prepare products for import. + */ + foreach ($batch->data as $rowData) { + $this->prepareProducts($rowData, $products); + } + + $this->saveProducts($products); + + return true; + } + + /** + * Prepare products from current batch. + */ + public function prepareProducts(array $rowData, array &$products): void + { + if ($this->isSKUExist($rowData['sku'])) { + $products['update'][$rowData['sku']] = $rowData; + } else { + $products['insert'][$rowData['sku']] = [ + ...$rowData, + 'created_at' => $rowData['created_at'] ?? now(), + 'updated_at' => $rowData['updated_at'] ?? now(), + ]; + } + } + + /** + * Save products from current batch. + */ + public function saveProducts(array $products): void + { + if (! empty($products['update'])) { + $this->updatedItemsCount += count($products['update']); + + $this->productRepository->upsert( + $products['update'], + $this->masterAttributeCode + ); + } + + if (! empty($products['insert'])) { + $this->createdItemsCount += count($products['insert']); + + $this->productRepository->insert($products['insert']); + } + } + + /** + * Save channels from current batch. + */ + public function saveChannels(array $channels): void + { + $productChannels = []; + + foreach ($channels as $sku => $channelIds) { + $product = $this->skuStorage->get($sku); + + foreach (array_unique($channelIds) as $channelId) { + $productChannels[] = [ + 'product_id' => $product['id'], + 'channel_id' => $channelId, + ]; + } + } + + DB::table('product_channels')->upsert( + $productChannels, + [ + 'product_id', + 'channel_id', + ], + ); + } + + /** + * Save links. + */ + public function loadUnloadedSKUs(array $skus): void + { + $notLoadedSkus = []; + + foreach ($skus as $sku) { + if ($this->skuStorage->has($sku)) { + continue; + } + + $notLoadedSkus[] = $sku; + } + + /** + * Load not loaded SKUs to the sku storage. + */ + if (! empty($notLoadedSkus)) { + $this->skuStorage->load($notLoadedSkus); + } + } + + /** + * Check if SKU exists. + */ + public function isSKUExist(string $sku): bool + { + return $this->skuStorage->has($sku); + } + + /** + * Prepare row data to save into the database. + */ + protected function prepareRowForDb(array $rowData): array + { + return parent::prepareRowForDb($rowData); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Importers/Products/SKUStorage.php b/packages/Webkul/DataTransfer/src/Helpers/Importers/Products/SKUStorage.php new file mode 100644 index 0000000..28ac974 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Importers/Products/SKUStorage.php @@ -0,0 +1,107 @@ +items = []; + + $this->load(); + } + + /** + * Load the SKU. + */ + public function load(array $skus = []): void + { + if (empty($skus)) { + $products = $this->productRepository->all($this->selectColumns); + } else { + $products = $this->productRepository->findWhereIn('sku', $skus, $this->selectColumns); + } + + foreach ($products as $product) { + $this->set($product->sku, [ + 'id' => $product->id, + 'sku' => $product->sku, + ]); + } + } + + /** + * Get SKU information. + */ + public function set(string $sku, array $data): self + { + $this->items[$sku] = implode(self::DELIMITER, [ + $data['id'], + $data['sku'], + ]); + + return $this; + } + + /** + * Check if SKU exists. + */ + public function has(string $sku): bool + { + return isset($this->items[$sku]); + } + + /** + * Get SKU information. + */ + public function get(string $sku): ?array + { + if (! $this->has($sku)) { + return null; + } + + $data = explode(self::DELIMITER, $this->items[$sku]); + + return [ + 'id' => $data[0], + ]; + } + + /** + * Is storage is empty. + */ + public function isEmpty(): int + { + return empty($this->items); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Sources/AbstractSource.php b/packages/Webkul/DataTransfer/src/Helpers/Sources/AbstractSource.php new file mode 100644 index 0000000..6d4689d --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Sources/AbstractSource.php @@ -0,0 +1,120 @@ +currentRowNumber; + } + + /** + * Checks if current position is valid. + */ + public function valid(): bool + { + return $this->currentRowNumber !== -1; + } + + /** + * Read next line from source. + */ + public function current(): array + { + $row = $this->currentRowData; + + if (count($row) != $this->totalColumns) { + if ($this->foundWrongQuoteFlag) { + throw new \InvalidArgumentException(AbstractImporter::ERROR_CODE_WRONG_QUOTES); + } else { + throw new \InvalidArgumentException(AbstractImporter::ERROR_CODE_COLUMNS_NUMBER); + } + } + + return array_combine($this->columnNames, $row); + } + + /** + * Read next line from source. + */ + public function next(): void + { + $this->currentRowNumber++; + + $row = $this->getNextRow(); + + if ($row === false || $row === []) { + $this->currentRowData = []; + + $this->currentRowNumber = -1; + } else { + $this->currentRowData = $row; + } + } + + /** + * Rewind the iterator to the first row. + */ + public function rewind(): void + { + $this->currentRowNumber = 0; + + $this->currentRowData = []; + + $this->getNextRow(); + + $this->next(); + } + + /** + * Column names getter. + */ + public function getColumnNames(): array + { + return $this->columnNames; + } + + /** + * Column names getter. + */ + public function getTotalColumns(): int + { + return count($this->columnNames); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Sources/CSV.php b/packages/Webkul/DataTransfer/src/Helpers/Sources/CSV.php new file mode 100644 index 0000000..eee5e88 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Sources/CSV.php @@ -0,0 +1,79 @@ +reader = fopen(Storage::disk('public')->path($filePath), 'r'); + + $this->columnNames = fgetcsv($this->reader, 4096, $delimiter); + + $this->totalColumns = count($this->columnNames); + } catch (\Exception $e) { + throw new \LogicException("Unable to open file: '{$filePath}'"); + } + } + + /** + * Close file handle. + * + * @return void + */ + public function __destruct() + { + if (! is_object($this->reader)) { + return; + } + + $this->reader->close(); + } + + /** + * Read next line from csv. + */ + protected function getNextRow(): array + { + $parsed = fgetcsv($this->reader, 4096, $this->delimiter); + + if (is_array($parsed) && count($parsed) != $this->totalColumns) { + foreach ($parsed as $element) { + if ($element && strpos($element, "'") !== false) { + $this->foundWrongQuoteFlag = true; + + break; + } + } + } else { + $this->foundWrongQuoteFlag = false; + } + + return is_array($parsed) ? $parsed : []; + } + + /** + * Rewind the iterator to the first row. + */ + public function rewind(): void + { + rewind($this->reader); + + parent::rewind(); + } +} diff --git a/packages/Webkul/DataTransfer/src/Helpers/Sources/Excel.php b/packages/Webkul/DataTransfer/src/Helpers/Sources/Excel.php new file mode 100644 index 0000000..1d9e66a --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Helpers/Sources/Excel.php @@ -0,0 +1,70 @@ +path($filePath)); + + $this->reader = $factory->getActiveSheet(); + + $highestColumn = $this->reader->getHighestColumn(); + + $this->totalColumns = Coordinate::columnIndexFromString($highestColumn); + + $this->columnNames = $this->getNextRow(); + } catch (\Exception $e) { + throw new \LogicException("Unable to open file: '{$filePath}'"); + } + } + + /** + * Read next line from csv. + */ + protected function getNextRow(): array|bool + { + for ($column = 1; $column <= $this->totalColumns; $column++) { + $rowData[] = $this->reader->getCellByColumnAndRow($column, $this->currentRowNumber)->getValue(); + } + + $filteredRowData = array_filter($rowData); + + if (empty($filteredRowData)) { + return false; + } + + return $rowData; + } + + /** + * Rewind the iterator to the first row. + */ + public function rewind(): void + { + $this->currentRowNumber = 1; + + $this->next(); + } +} diff --git a/packages/Webkul/DataTransfer/src/Jobs/Import/Completed.php b/packages/Webkul/DataTransfer/src/Jobs/Import/Completed.php new file mode 100644 index 0000000..75f466d --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Jobs/Import/Completed.php @@ -0,0 +1,38 @@ +import = $import; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + app(ImportHelper::class) + ->setImport($this->import) + ->completed(); + } +} diff --git a/packages/Webkul/DataTransfer/src/Jobs/Import/ImportBatch.php b/packages/Webkul/DataTransfer/src/Jobs/Import/ImportBatch.php new file mode 100644 index 0000000..595dcc7 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Jobs/Import/ImportBatch.php @@ -0,0 +1,41 @@ +importBatch = $importBatch; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $typeImported = app(ImportHelper::class) + ->setImport($this->importBatch->import) + ->getTypeImporter(); + + $typeImported->importBatch($this->importBatch); + } +} diff --git a/packages/Webkul/DataTransfer/src/Jobs/Import/IndexBatch.php b/packages/Webkul/DataTransfer/src/Jobs/Import/IndexBatch.php new file mode 100644 index 0000000..971893b --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Jobs/Import/IndexBatch.php @@ -0,0 +1,41 @@ +importBatch = $importBatch; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $typeImported = app(ImportHelper::class) + ->setImport($this->importBatch->import) + ->getTypeImporter(); + + $typeImported->indexBatch($this->importBatch); + } +} diff --git a/packages/Webkul/DataTransfer/src/Jobs/Import/Indexing.php b/packages/Webkul/DataTransfer/src/Jobs/Import/Indexing.php new file mode 100644 index 0000000..b4ed9aa --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Jobs/Import/Indexing.php @@ -0,0 +1,38 @@ +import = $import; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + app(ImportHelper::class) + ->setImport($this->import) + ->indexing(); + } +} diff --git a/packages/Webkul/DataTransfer/src/Jobs/Import/LinkBatch.php b/packages/Webkul/DataTransfer/src/Jobs/Import/LinkBatch.php new file mode 100644 index 0000000..c02f2cd --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Jobs/Import/LinkBatch.php @@ -0,0 +1,41 @@ +importBatch = $importBatch; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $typeImported = app(ImportHelper::class) + ->setImport($this->importBatch->import) + ->getTypeImporter(); + + $typeImported->linkBatch($this->importBatch); + } +} diff --git a/packages/Webkul/DataTransfer/src/Jobs/Import/Linking.php b/packages/Webkul/DataTransfer/src/Jobs/Import/Linking.php new file mode 100644 index 0000000..e826cf2 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Jobs/Import/Linking.php @@ -0,0 +1,38 @@ +import = $import; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + app(ImportHelper::class) + ->setImport($this->import) + ->linking(); + } +} diff --git a/packages/Webkul/DataTransfer/src/Models/Import.php b/packages/Webkul/DataTransfer/src/Models/Import.php new file mode 100755 index 0000000..c55772e --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Models/Import.php @@ -0,0 +1,63 @@ + 'array', + 'errors' => 'array', + 'started_at' => 'datetime', + 'completed_at' => 'datetime', + ]; + + /** + * Get the options. + */ + public function batches(): HasMany + { + return $this->hasMany(ImportBatchProxy::modelClass()); + } + + /** + * Get the file name. + */ + public function getFileNameAttribute(): string + { + return preg_replace('/^.*?\/\d+-/', '', $this->file_path); + } +} diff --git a/packages/Webkul/DataTransfer/src/Models/ImportBatch.php b/packages/Webkul/DataTransfer/src/Models/ImportBatch.php new file mode 100755 index 0000000..4b93e40 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Models/ImportBatch.php @@ -0,0 +1,49 @@ + 'array', + 'data' => 'array', + ]; + + /** + * Get the import that owns the import batch. + * + * @return BelongsTo + */ + public function import() + { + return $this->belongsTo(ImportProxy::modelClass()); + } +} diff --git a/packages/Webkul/DataTransfer/src/Models/ImportBatchProxy.php b/packages/Webkul/DataTransfer/src/Models/ImportBatchProxy.php new file mode 100644 index 0000000..47b0988 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Models/ImportBatchProxy.php @@ -0,0 +1,7 @@ +loadMigrationsFrom(__DIR__.'/../Database/Migrations'); + } + + /** + * Register any application services. + */ + public function register(): void + { + $this->mergeConfigFrom(dirname(__DIR__).'/Config/importers.php', 'importers'); + } +} diff --git a/packages/Webkul/DataTransfer/src/Providers/ModuleServiceProvider.php b/packages/Webkul/DataTransfer/src/Providers/ModuleServiceProvider.php new file mode 100644 index 0000000..27914c3 --- /dev/null +++ b/packages/Webkul/DataTransfer/src/Providers/ModuleServiceProvider.php @@ -0,0 +1,20 @@ +info('Processing the incoming emails.'); + + $this->inboundEmailProcessor->processMessagesFromAllFolders(); + + $this->info('Incoming emails processed successfully.'); + } +} diff --git a/packages/Webkul/Email/src/Contracts/Attachment.php b/packages/Webkul/Email/src/Contracts/Attachment.php new file mode 100644 index 0000000..7aa97f7 --- /dev/null +++ b/packages/Webkul/Email/src/Contracts/Attachment.php @@ -0,0 +1,5 @@ +increments('id'); + $table->string('subject')->nullable(); + $table->string('source'); + $table->string('user_type'); + $table->string('name')->nullable(); + $table->text('reply')->nullable(); + $table->boolean('is_read')->default(0); + $table->json('folders')->nullable(); + $table->json('from')->nullable(); + $table->json('sender')->nullable(); + $table->json('reply_to')->nullable(); + $table->json('cc')->nullable(); + $table->json('bcc')->nullable(); + $table->string('unique_id')->nullable()->unique(); + $table->string('message_id')->unique(); + $table->json('reference_ids')->nullable(); + + $table->integer('person_id')->unsigned()->nullable(); + $table->foreign('person_id')->references('id')->on('persons')->onDelete('set null'); + + $table->integer('lead_id')->unsigned()->nullable(); + $table->foreign('lead_id')->references('id')->on('leads')->onDelete('set null'); + + $table->timestamps(); + }); + + Schema::table('emails', function (Blueprint $table) { + $table->integer('parent_id')->unsigned()->nullable(); + $table->foreign('parent_id')->references('id')->on('emails')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('emails'); + } +}; diff --git a/packages/Webkul/Email/src/Database/Migrations/2021_05_25_072700_create_email_attachments_table.php b/packages/Webkul/Email/src/Database/Migrations/2021_05_25_072700_create_email_attachments_table.php new file mode 100644 index 0000000..93f2251 --- /dev/null +++ b/packages/Webkul/Email/src/Database/Migrations/2021_05_25_072700_create_email_attachments_table.php @@ -0,0 +1,40 @@ +increments('id'); + $table->string('name')->nullable(); + $table->string('path'); + $table->integer('size')->nullable(); + $table->string('content_type')->nullable(); + $table->string('content_id')->nullable(); + + $table->integer('email_id')->unsigned(); + $table->foreign('email_id')->references('id')->on('emails')->onDelete('cascade'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('email_attachments'); + } +}; diff --git a/packages/Webkul/Email/src/Database/Migrations/2024_08_27_091619_create_email_tags_table.php b/packages/Webkul/Email/src/Database/Migrations/2024_08_27_091619_create_email_tags_table.php new file mode 100644 index 0000000..45fd477 --- /dev/null +++ b/packages/Webkul/Email/src/Database/Migrations/2024_08_27_091619_create_email_tags_table.php @@ -0,0 +1,30 @@ +integer('tag_id')->unsigned(); + $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); + + $table->integer('email_id')->unsigned(); + $table->foreign('email_id')->references('id')->on('emails')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('email_tags'); + } +}; diff --git a/packages/Webkul/Email/src/Enums/SupportedFolderEnum.php b/packages/Webkul/Email/src/Enums/SupportedFolderEnum.php new file mode 100644 index 0000000..cf88a85 --- /dev/null +++ b/packages/Webkul/Email/src/Enums/SupportedFolderEnum.php @@ -0,0 +1,46 @@ +filename; + } + + /** + * Retrieve the attachment content type. + * + * @return string + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * Retrieve the attachment content disposition. + * + * @return string + */ + public function getContentDisposition() + { + return $this->contentDisposition; + } + + /** + * Retrieve the attachment content ID. + * + * @return string + */ + public function getContentID() + { + return $this->contentId; + } + + /** + * Retrieve the attachment headers. + * + * @return string + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Read the contents a few bytes at a time until completed. + * + * Once read to completion, it always returns false. + * + * @param int $bytes + * @return string + */ + public function read($bytes = 2082) + { + return feof($this->stream) ? false : fread($this->stream, $bytes); + } + + /** + * Retrieve the file content in one go. + * + * Once you retrieve the content you cannot use MimeMailParser_attachment::read(). + * + * @return string + */ + public function getContent() + { + if ($this->content === null) { + fseek($this->stream, 0); + + while (($buf = $this->read()) !== false) { + $this->content .= $buf; + } + } + + return $this->content; + } +} diff --git a/packages/Webkul/Email/src/Helpers/Charset.php b/packages/Webkul/Email/src/Helpers/Charset.php new file mode 100644 index 0000000..46e4ac2 --- /dev/null +++ b/packages/Webkul/Email/src/Helpers/Charset.php @@ -0,0 +1,353 @@ + 'us-ascii', + 'us-ascii' => 'us-ascii', + 'ansi_x3.4-1968' => 'us-ascii', + '646' => 'us-ascii', + 'iso-8859-1' => 'ISO-8859-1', + 'iso-8859-2' => 'ISO-8859-2', + 'iso-8859-3' => 'ISO-8859-3', + 'iso-8859-4' => 'ISO-8859-4', + 'iso-8859-5' => 'ISO-8859-5', + 'iso-8859-6' => 'ISO-8859-6', + 'iso-8859-6-i' => 'ISO-8859-6-I', + 'iso-8859-6-e' => 'ISO-8859-6-E', + 'iso-8859-7' => 'ISO-8859-7', + 'iso-8859-8' => 'ISO-8859-8', + 'iso-8859-8-i' => 'ISO-8859-8-I', + 'iso-8859-8-e' => 'ISO-8859-8-E', + 'iso-8859-9' => 'ISO-8859-9', + 'iso-8859-10' => 'ISO-8859-10', + 'iso-8859-11' => 'ISO-8859-11', + 'iso-8859-13' => 'ISO-8859-13', + 'iso-8859-14' => 'ISO-8859-14', + 'iso-8859-15' => 'ISO-8859-15', + 'iso-8859-16' => 'ISO-8859-16', + 'iso-ir-111' => 'ISO-IR-111', + 'iso-2022-cn' => 'ISO-2022-CN', + 'iso-2022-cn-ext' => 'ISO-2022-CN', + 'iso-2022-kr' => 'ISO-2022-KR', + 'iso-2022-jp' => 'ISO-2022-JP', + 'utf-16be' => 'UTF-16BE', + 'utf-16le' => 'UTF-16LE', + 'utf-16' => 'UTF-16', + 'windows-1250' => 'windows-1250', + 'windows-1251' => 'windows-1251', + 'windows-1252' => 'windows-1252', + 'windows-1253' => 'windows-1253', + 'windows-1254' => 'windows-1254', + 'windows-1255' => 'windows-1255', + 'windows-1256' => 'windows-1256', + 'windows-1257' => 'windows-1257', + 'windows-1258' => 'windows-1258', + 'ibm866' => 'IBM866', + 'ibm850' => 'IBM850', + 'ibm852' => 'IBM852', + 'ibm855' => 'IBM855', + 'ibm857' => 'IBM857', + 'ibm862' => 'IBM862', + 'ibm864' => 'IBM864', + 'utf-8' => 'UTF-8', + 'utf-7' => 'UTF-7', + 'shift_jis' => 'Shift_JIS', + 'big5' => 'Big5', + 'euc-jp' => 'EUC-JP', + 'euc-kr' => 'EUC-KR', + 'gb2312' => 'GB2312', + 'gb18030' => 'gb18030', + 'viscii' => 'VISCII', + 'koi8-r' => 'KOI8-R', + 'koi8_r' => 'KOI8-R', + 'cskoi8r' => 'KOI8-R', + 'koi' => 'KOI8-R', + 'koi8' => 'KOI8-R', + 'koi8-u' => 'KOI8-U', + 'tis-620' => 'TIS-620', + 't.61-8bit' => 'T.61-8bit', + 'hz-gb-2312' => 'HZ-GB-2312', + 'big5-hkscs' => 'Big5-HKSCS', + 'gbk' => 'gbk', + 'cns11643' => 'x-euc-tw', + 'x-imap4-modified-utf7' => 'x-imap4-modified-utf7', + 'x-euc-tw' => 'x-euc-tw', + 'x-mac-ce' => 'x-mac-ce', + 'x-mac-turkish' => 'x-mac-turkish', + 'x-mac-greek' => 'x-mac-greek', + 'x-mac-icelandic' => 'x-mac-icelandic', + 'x-mac-croatian' => 'x-mac-croatian', + 'x-mac-romanian' => 'x-mac-romanian', + 'x-mac-cyrillic' => 'x-mac-cyrillic', + 'x-mac-ukrainian' => 'x-mac-cyrillic', + 'x-mac-hebrew' => 'x-mac-hebrew', + 'x-mac-arabic' => 'x-mac-arabic', + 'x-mac-farsi' => 'x-mac-farsi', + 'x-mac-devanagari' => 'x-mac-devanagari', + 'x-mac-gujarati' => 'x-mac-gujarati', + 'x-mac-gurmukhi' => 'x-mac-gurmukhi', + 'armscii-8' => 'armscii-8', + 'x-viet-tcvn5712' => 'x-viet-tcvn5712', + 'x-viet-vps' => 'x-viet-vps', + 'iso-10646-ucs-2' => 'UTF-16BE', + 'x-iso-10646-ucs-2-be' => 'UTF-16BE', + 'x-iso-10646-ucs-2-le' => 'UTF-16LE', + 'x-user-defined' => 'x-user-defined', + 'x-johab' => 'x-johab', + 'latin1' => 'ISO-8859-1', + 'iso_8859-1' => 'ISO-8859-1', + 'iso8859-1' => 'ISO-8859-1', + 'iso8859-2' => 'ISO-8859-2', + 'iso8859-3' => 'ISO-8859-3', + 'iso8859-4' => 'ISO-8859-4', + 'iso8859-5' => 'ISO-8859-5', + 'iso8859-6' => 'ISO-8859-6', + 'iso8859-7' => 'ISO-8859-7', + 'iso8859-8' => 'ISO-8859-8', + 'iso8859-9' => 'ISO-8859-9', + 'iso8859-10' => 'ISO-8859-10', + 'iso8859-11' => 'ISO-8859-11', + 'iso8859-13' => 'ISO-8859-13', + 'iso8859-14' => 'ISO-8859-14', + 'iso8859-15' => 'ISO-8859-15', + 'iso_8859-1:1987' => 'ISO-8859-1', + 'iso-ir-100' => 'ISO-8859-1', + 'l1' => 'ISO-8859-1', + 'ibm819' => 'ISO-8859-1', + 'cp819' => 'ISO-8859-1', + 'csisolatin1' => 'ISO-8859-1', + 'latin2' => 'ISO-8859-2', + 'iso_8859-2' => 'ISO-8859-2', + 'iso_8859-2:1987' => 'ISO-8859-2', + 'iso-ir-101' => 'ISO-8859-2', + 'l2' => 'ISO-8859-2', + 'csisolatin2' => 'ISO-8859-2', + 'latin3' => 'ISO-8859-3', + 'iso_8859-3' => 'ISO-8859-3', + 'iso_8859-3:1988' => 'ISO-8859-3', + 'iso-ir-109' => 'ISO-8859-3', + 'l3' => 'ISO-8859-3', + 'csisolatin3' => 'ISO-8859-3', + 'latin4' => 'ISO-8859-4', + 'iso_8859-4' => 'ISO-8859-4', + 'iso_8859-4:1988' => 'ISO-8859-4', + 'iso-ir-110' => 'ISO-8859-4', + 'l4' => 'ISO-8859-4', + 'csisolatin4' => 'ISO-8859-4', + 'cyrillic' => 'ISO-8859-5', + 'iso_8859-5' => 'ISO-8859-5', + 'iso_8859-5:1988' => 'ISO-8859-5', + 'iso-ir-144' => 'ISO-8859-5', + 'csisolatincyrillic' => 'ISO-8859-5', + 'arabic' => 'ISO-8859-6', + 'iso_8859-6' => 'ISO-8859-6', + 'iso_8859-6:1987' => 'ISO-8859-6', + 'iso-ir-127' => 'ISO-8859-6', + 'ecma-114' => 'ISO-8859-6', + 'asmo-708' => 'ISO-8859-6', + 'csisolatinarabic' => 'ISO-8859-6', + 'csiso88596i' => 'ISO-8859-6-I', + 'csiso88596e' => 'ISO-8859-6-E', + 'greek' => 'ISO-8859-7', + 'greek8' => 'ISO-8859-7', + 'sun_eu_greek' => 'ISO-8859-7', + 'iso_8859-7' => 'ISO-8859-7', + 'iso_8859-7:1987' => 'ISO-8859-7', + 'iso-ir-126' => 'ISO-8859-7', + 'elot_928' => 'ISO-8859-7', + 'ecma-118' => 'ISO-8859-7', + 'csisolatingreek' => 'ISO-8859-7', + 'hebrew' => 'ISO-8859-8', + 'iso_8859-8' => 'ISO-8859-8', + 'visual' => 'ISO-8859-8', + 'iso_8859-8:1988' => 'ISO-8859-8', + 'iso-ir-138' => 'ISO-8859-8', + 'csisolatinhebrew' => 'ISO-8859-8', + 'csiso88598i' => 'ISO-8859-8-I', + 'iso-8859-8i' => 'ISO-8859-8-I', + 'logical' => 'ISO-8859-8-I', + 'csiso88598e' => 'ISO-8859-8-E', + 'latin5' => 'ISO-8859-9', + 'iso_8859-9' => 'ISO-8859-9', + 'iso_8859-9:1989' => 'ISO-8859-9', + 'iso-ir-148' => 'ISO-8859-9', + 'l5' => 'ISO-8859-9', + 'csisolatin5' => 'ISO-8859-9', + 'unicode-1-1-utf-8' => 'UTF-8', + 'utf8' => 'UTF-8', + 'x-sjis' => 'Shift_JIS', + 'shift-jis' => 'Shift_JIS', + 'ms_kanji' => 'Shift_JIS', + 'csshiftjis' => 'Shift_JIS', + 'windows-31j' => 'Shift_JIS', + 'cp932' => 'Shift_JIS', + 'sjis' => 'Shift_JIS', + 'cseucpkdfmtjapanese' => 'EUC-JP', + 'x-euc-jp' => 'EUC-JP', + 'csiso2022jp' => 'ISO-2022-JP', + 'iso-2022-jp-2' => 'ISO-2022-JP', + 'csiso2022jp2' => 'ISO-2022-JP', + 'csbig5' => 'Big5', + 'cn-big5' => 'Big5', + 'x-x-big5' => 'Big5', + 'zh_tw-big5' => 'Big5', + 'cseuckr' => 'EUC-KR', + 'ks_c_5601-1987' => 'EUC-KR', + 'iso-ir-149' => 'EUC-KR', + 'ks_c_5601-1989' => 'EUC-KR', + 'ksc_5601' => 'EUC-KR', + 'ksc5601' => 'EUC-KR', + 'korean' => 'EUC-KR', + 'csksc56011987' => 'EUC-KR', + '5601' => 'EUC-KR', + 'windows-949' => 'EUC-KR', + 'gb_2312-80' => 'GB2312', + 'iso-ir-58' => 'GB2312', + 'chinese' => 'GB2312', + 'csiso58gb231280' => 'GB2312', + 'csgb2312' => 'GB2312', + 'zh_cn.euc' => 'GB2312', + 'gb_2312' => 'GB2312', + 'x-cp1250' => 'windows-1250', + 'x-cp1251' => 'windows-1251', + 'x-cp1252' => 'windows-1252', + 'x-cp1253' => 'windows-1253', + 'x-cp1254' => 'windows-1254', + 'x-cp1255' => 'windows-1255', + 'x-cp1256' => 'windows-1256', + 'x-cp1257' => 'windows-1257', + 'x-cp1258' => 'windows-1258', + 'windows-874' => 'windows-874', + 'ibm874' => 'windows-874', + 'dos-874' => 'windows-874', + 'macintosh' => 'macintosh', + 'x-mac-roman' => 'macintosh', + 'mac' => 'macintosh', + 'csmacintosh' => 'macintosh', + 'cp866' => 'IBM866', + 'cp-866' => 'IBM866', + '866' => 'IBM866', + 'csibm866' => 'IBM866', + 'cp850' => 'IBM850', + '850' => 'IBM850', + 'csibm850' => 'IBM850', + 'cp852' => 'IBM852', + '852' => 'IBM852', + 'csibm852' => 'IBM852', + 'cp855' => 'IBM855', + '855' => 'IBM855', + 'csibm855' => 'IBM855', + 'cp857' => 'IBM857', + '857' => 'IBM857', + 'csibm857' => 'IBM857', + 'cp862' => 'IBM862', + '862' => 'IBM862', + 'csibm862' => 'IBM862', + 'cp864' => 'IBM864', + '864' => 'IBM864', + 'csibm864' => 'IBM864', + 'ibm-864' => 'IBM864', + 't.61' => 'T.61-8bit', + 'iso-ir-103' => 'T.61-8bit', + 'csiso103t618bit' => 'T.61-8bit', + 'x-unicode-2-0-utf-7' => 'UTF-7', + 'unicode-2-0-utf-7' => 'UTF-7', + 'unicode-1-1-utf-7' => 'UTF-7', + 'csunicode11utf7' => 'UTF-7', + 'csunicode' => 'UTF-16BE', + 'csunicode11' => 'UTF-16BE', + 'iso-10646-ucs-basic' => 'UTF-16BE', + 'csunicodeascii' => 'UTF-16BE', + 'iso-10646-unicode-latin1' => 'UTF-16BE', + 'csunicodelatin1' => 'UTF-16BE', + 'iso-10646' => 'UTF-16BE', + 'iso-10646-j-1' => 'UTF-16BE', + 'latin6' => 'ISO-8859-10', + 'iso-ir-157' => 'ISO-8859-10', + 'l6' => 'ISO-8859-10', + 'csisolatin6' => 'ISO-8859-10', + 'iso_8859-15' => 'ISO-8859-15', + 'csisolatin9' => 'ISO-8859-15', + 'l9' => 'ISO-8859-15', + 'ecma-cyrillic' => 'ISO-IR-111', + 'csiso111ecmacyrillic' => 'ISO-IR-111', + 'csiso2022kr' => 'ISO-2022-KR', + 'csviscii' => 'VISCII', + 'zh_tw-euc' => 'x-euc-tw', + 'iso88591' => 'ISO-8859-1', + 'iso88592' => 'ISO-8859-2', + 'iso88593' => 'ISO-8859-3', + 'iso88594' => 'ISO-8859-4', + 'iso88595' => 'ISO-8859-5', + 'iso88596' => 'ISO-8859-6', + 'iso88597' => 'ISO-8859-7', + 'iso88598' => 'ISO-8859-8', + 'iso88599' => 'ISO-8859-9', + 'iso885910' => 'ISO-8859-10', + 'iso885911' => 'ISO-8859-11', + 'iso885912' => 'ISO-8859-12', + 'iso885913' => 'ISO-8859-13', + 'iso885914' => 'ISO-8859-14', + 'iso885915' => 'ISO-8859-15', + 'tis620' => 'TIS-620', + 'cp1250' => 'windows-1250', + 'cp1251' => 'windows-1251', + 'cp1252' => 'windows-1252', + 'cp1253' => 'windows-1253', + 'cp1254' => 'windows-1254', + 'cp1255' => 'windows-1255', + 'cp1256' => 'windows-1256', + 'cp1257' => 'windows-1257', + 'cp1258' => 'windows-1258', + 'x-gbk' => 'gbk', + 'windows-936' => 'gbk', + 'ansi-1251' => 'windows-1251', + ]; + + /** + * Decode the string from charset. + * + * @param string $encodedString + * @param string $charset + * @return string + */ + public function decodeCharset($encodedString, $charset) + { + if (strtolower($charset) == 'utf-8' || strtolower($charset) == 'us-ascii') { + return $encodedString; + } + + try { + return iconv($this->getCharsetAlias($charset), 'UTF-8//TRANSLIT', $encodedString); + } catch (\Exception $e) { + return iconv($this->getCharsetAlias($charset), 'UTF-8//IGNORE', $encodedString); + } + } + + /** + * Get charset alias. + * + * @param string $charset. + * @return string + */ + public function getCharsetAlias($charset) + { + $charset = strtolower($charset); + + if (array_key_exists($charset, $this->charsetAlias)) { + return $this->charsetAlias[$charset]; + } + + return null; + } +} diff --git a/packages/Webkul/Email/src/Helpers/Contracts/CharsetManager.php b/packages/Webkul/Email/src/Helpers/Contracts/CharsetManager.php new file mode 100644 index 0000000..5fc1b3d --- /dev/null +++ b/packages/Webkul/Email/src/Helpers/Contracts/CharsetManager.php @@ -0,0 +1,20 @@ +'; + } else { + $fulltag = '<'.$tagname; + + if (is_array($attary) && count($attary)) { + $atts = []; + + foreach ($attary as $attname => $attvalue) { + array_push($atts, "$attname=$attvalue"); + } + + $fulltag .= ' '.implode(' ', $atts); + } + + if ($tagtype == 3) { + $fulltag .= ' /'; + } + + $fulltag .= '>'; + } + + return $fulltag; + } + + /** + * A small helper function to use with array_walk. Modifies a by-ref + * value and makes it lowercase. + * + * @param string $val + * @return void + */ + public function tln_casenormalize(&$val) + { + $val = strtolower($val); + } + + /** + * This function skips any whitespace from the current position within + * a string and to the next non-whitespace value. + * + * @param string $body + * @param int $offset + * @return int + */ + public function tln_skipspace($body, $offset) + { + preg_match('/^(\s*)/s', substr($body, $offset), $matches); + + try { + if (! empty($matches[1])) { + $count = strlen($matches[1]); + $offset += $count; + } + } catch (\Exception $e) { + } + + return $offset; + } + + /** + * This function looks for the next character within a string. It's + * really just a glorified "strpos", except it catches the failures + * nicely. + * + * @param string $body + * @param int $offset + * @param string $needle + * @return int + */ + public function tln_findnxstr($body, $offset, $needle) + { + $pos = strpos($body, $needle, $offset); + + if ($pos === false) { + $pos = strlen($body); + } + + return $pos; + } + + /** + * This function takes a PCRE-style regexp and tries to match it + * within the string. + * + * @param string $body + * @param int $offset + * @param string $reg + * @return array|bool + */ + public function tln_findnxreg($body, $offset, $reg) + { + $matches = $retarr = []; + + $preg_rule = '%^(.*?)('.$reg.')%s'; + + preg_match($preg_rule, substr($body, $offset), $matches); + + if (! isset($matches[0]) || ! $matches[0]) { + $retarr = false; + } else { + $retarr[0] = $offset + strlen($matches[1]); + + $retarr[1] = $matches[1]; + + $retarr[2] = $matches[2]; + } + + return $retarr; + } + + /** + * This function looks for the next tag. + * + * @param string $body + * @param int $offset + * @return array|bool + */ + public function tln_getnxtag($body, $offset) + { + if ($offset > strlen($body)) { + return false; + } + + $lt = $this->tln_findnxstr($body, $offset, '<'); + + if ($lt == strlen($body)) { + return false; + } + + /** + * We are here: + * blah blah + * \---------^ + */ + $pos = $this->tln_skipspace($body, $lt + 1); + + if ($pos >= strlen($body)) { + return [false, false, false, $lt, strlen($body)]; + } + + /** + * There are 3 kinds of tags: + * 1. Opening tag, e.g.: + * + * 2. Closing tag, e.g.: + * + * 3. XHTML-style content-less tag, e.g.: + * + */ + switch (substr($body, $pos, 1)) { + case '/': + $tagtype = 2; + + $pos++; + break; + + case '!': + /** + * A comment or an SGML declaration. + */ + if (substr($body, $pos + 1, 2) == '--') { + $gt = strpos($body, '-->', $pos); + + if ($gt === false) { + $gt = strlen($body); + } else { + $gt += 2; + } + + return [false, false, false, $lt, $gt]; + } else { + $gt = $this->tln_findnxstr($body, $pos, '>'); + + return [false, false, false, $lt, $gt]; + } + break; + + default: + $tagtype = 1; + break; + + } + + /** + * Look for next [\W-_], which will indicate the end of the tag name. + */ + $regary = $this->tln_findnxreg($body, $pos, '[^\w\-_]'); + + if ($regary == false) { + return [false, false, false, $lt, strlen($body)]; + } + + [$pos, $tagname, $match] = $regary; + + $tagname = strtolower($tagname); + + /** + * $match can be either of these: + * '>' indicating the end of the tag entirely. + * '\s' indicating the end of the tag name. + * '/' indicating that this is type-3 xhtml tag. + * + * Whatever else we find there indicates an invalid tag. + */ + switch ($match) { + case '/': + /** + * This is an xhtml-style tag with a closing / at the + * end, like so: . Check if it's followed + * by the closing bracket. If not, then this tag is invalid + */ + if (substr($body, $pos, 2) == '/>') { + $pos++; + + $tagtype = 3; + } else { + $gt = $this->tln_findnxstr($body, $pos, '>'); + + $retary = [false, false, false, $lt, $gt]; + + return $retary; + } + + // intentional fall-through + case '>': + return [$tagname, false, $tagtype, $lt, $pos]; + + default: + /** + * Check if it's whitespace. + */ + if (! preg_match('/\s/', $match)) { + /** + * This is an invalid tag! Look for the next closing ">". + */ + $gt = $this->tln_findnxstr($body, $lt, '>'); + + return [false, false, false, $lt, $gt]; + } + + break; + } + + /** + * At this point we're here: + * + * \-------^ + * + * At this point we loop in order to find all attributes. + */ + $attary = []; + + while ($pos <= strlen($body)) { + $pos = $this->tln_skipspace($body, $pos); + + if ($pos == strlen($body)) { + /** + * Non-closed tag. + */ + return [false, false, false, $lt, $pos]; + } + + /** + * See if we arrived at a ">" or "/>", which means that we reached + * the end of the tag. + */ + $matches = []; + + if (preg_match('%^(\s*)(>|/>)%s', substr($body, $pos), $matches)) { + /** + * Yep. So we did. + */ + $pos += strlen($matches[1]); + + if ($matches[2] == '/>') { + $tagtype = 3; + + $pos++; + } + + return [$tagname, $attary, $tagtype, $lt, $pos]; + } + + /** + * There are several types of attributes, with optional + * [:space:] between members. + * Type 1: + * attrname[:space:]=[:space:]'CDATA' + * Type 2: + * attrname[:space:]=[:space:]"CDATA" + * Type 3: + * attr[:space:]=[:space:]CDATA + * Type 4: + * attrname + * + * We leave types 1 and 2 the same, type 3 we check for + * '"' and convert to """ if needed, then wrap in + * double quotes. Type 4 we convert into: + * attrname="yes". + */ + $regary = $this->tln_findnxreg($body, $pos, '[^\w\-_]'); + + if ($regary == false) { + /** + * Looks like body ended before the end of tag. + */ + return [false, false, false, $lt, strlen($body)]; + } + + [$pos, $attname, $match] = $regary; + + $attname = strtolower($attname); + + /** + * We arrived at the end of attribute name. Several things possible + * here: + * '>' means the end of the tag and this is attribute type 4 + * '/' if followed by '>' means the same thing as above + * '\s' means a lot of things -- look what it's followed by. + * anything else means the attribute is invalid. + */ + switch ($match) { + case '/': + /** + * This is an xhtml-style tag with a closing / at the + * end, like so: . Check if it's followed + * by the closing bracket. If not, then this tag is invalid + */ + if (substr($body, $pos, 2) == '/>') { + $pos++; + $tagtype = 3; + } else { + $gt = $this->tln_findnxstr($body, $pos, '>'); + $retary = [false, false, false, $lt, $gt]; + + return $retary; + } + + // intentional fall-through + case '>': + $attary[$attname] = '"yes"'; + + return [$tagname, $attary, $tagtype, $lt, $pos]; + break; + + default: + /** + * Skip whitespace and see what we arrive at. + */ + $pos = $this->tln_skipspace($body, $pos); + + $char = substr($body, $pos, 1); + /** + * Two things are valid here: + * '=' means this is attribute type 1 2 or 3. + * \w means this was attribute type 4. + * anything else we ignore and re-loop. End of tag and + * invalid stuff will be caught by our checks at the beginning + * of the loop. + */ + if ($char == '=') { + $pos++; + + $pos = $this->tln_skipspace($body, $pos); + /** + * Here are 3 possibilities: + * "'" attribute type 1 + * '"' attribute type 2 + * everything else is the content of tag type 3 + */ + $quot = substr($body, $pos, 1); + + if ($quot == '\'') { + $regary = $this->tln_findnxreg($body, $pos + 1, '\''); + + if ($regary == false) { + return [false, false, false, $lt, strlen($body)]; + } + + [$pos, $attval, $match] = $regary; + + $pos++; + + $attary[$attname] = '\''.$attval.'\''; + } elseif ($quot == '"') { + $regary = $this->tln_findnxreg($body, $pos + 1, '\"'); + + if ($regary == false) { + return [false, false, false, $lt, strlen($body)]; + } + + [$pos, $attval, $match] = $regary; + + $pos++; + + $attary[$attname] = '"'.$attval.'"'; + } else { + /** + * These are hateful. Look for \s, or >. + */ + $regary = $this->tln_findnxreg($body, $pos, '[\s>]'); + + if ($regary == false) { + return [false, false, false, $lt, strlen($body)]; + } + + [$pos, $attval, $match] = $regary; + + $attval = preg_replace('/\"/s', '"', $attval); + + $attary[$attname] = '"'.$attval.'"'; + } + } elseif (preg_match('|[\w/>]|', $char)) { + $attary[$attname] = '"yes"'; + } else { + $gt = $this->tln_findnxstr($body, $pos, '>'); + + return [false, false, false, $lt, $gt]; + } + break; + } + } + + /** + * The fact that we got here indicates that the tag end was never + * found. Return invalid tag indication so it gets stripped. + */ + return [false, false, false, $lt, strlen($body)]; + } + + /** + * Translates entities into literal values so they can be checked. + * + * @param string $attvalue + * @param string $regex + * @param bool $hex + * @return bool + */ + public function tln_deent(&$attvalue, $regex, $hex = false) + { + preg_match_all($regex, $attvalue, $matches); + + if (is_array($matches) && count($matches[0]) > 0) { + $repl = []; + + for ($i = 0; $i < count($matches[0]); $i++) { + $numval = $matches[1][$i]; + + if ($hex) { + $numval = hexdec($numval); + } + + $repl[$matches[0][$i]] = chr($numval); + } + + $attvalue = strtr($attvalue, $repl); + + return true; + } else { + return false; + } + } + + /** + * This function checks attribute values for entity-encoded values + * and returns them translated into 8-bit strings so we can run + * checks on them. + * + * @param string $attvalue + * @return void + */ + public function tln_defang(&$attvalue) + { + /** + * Skip this if there aren't ampersands or backslashes. + */ + if (strpos($attvalue, '&') === false + && strpos($attvalue, '\\') === false + ) { + return; + } + + do { + $m = false; + $m = $m || $this->tln_deent($attvalue, '/\�*(\d+);*/s'); + $m = $m || $this->tln_deent($attvalue, '/\�*((\d|[a-f])+);*/si', true); + $m = $m || $this->tln_deent($attvalue, '/\\\\(\d+)/s', true); + } while ($m == true); + + $attvalue = stripslashes($attvalue); + } + + /** + * Kill any tabs, newlines, or carriage returns. Our friends the + * makers of the browser with 95% market value decided that it'd + * be funny to make "java[tab]script" be just as good as "javascript". + * + * @param string $attvalue + * @return void + */ + public function tln_unspace(&$attvalue) + { + if (strcspn($attvalue, "\t\r\n\0 ") != strlen($attvalue)) { + $attvalue = str_replace( + ["\t", "\r", "\n", "\0", ' '], + ['', '', '', '', ''], + $attvalue + ); + } + } + + /** + * This function runs various checks against the attributes. + * + * @param string $tagname + * @param array $attary + * @param array $rm_attnames + * @param array $bad_attvals + * @param array $add_attr_to_tag + * @param string $trans_image_path + * @param bool $block_external_images + * @return array with modified attributes. + */ + public function tln_fixatts( + $tagname, + $attary, + $rm_attnames, + $bad_attvals, + $add_attr_to_tag, + $trans_image_path, + $block_external_images + ) { + /** + * Convert to array if is not. + */ + $attary = is_array($attary) ? $attary : []; + + foreach ($attary as $attname => $attvalue) { + /** + * See if this attribute should be removed. + */ + foreach ($rm_attnames as $matchtag => $matchattrs) { + if (preg_match($matchtag, $tagname)) { + foreach ($matchattrs as $matchattr) { + if (preg_match($matchattr, $attname)) { + unset($attary[$attname]); + + continue 2; + } + } + } + } + + $this->tln_defang($attvalue); + + $this->tln_unspace($attvalue); + + /** + * Now let's run checks on the attvalues. + * I don't expect anyone to comprehend this. If you do, + * get in touch with me so I can drive to where you live and + * shake your hand personally. :) + */ + foreach ($bad_attvals as $matchtag => $matchattrs) { + if (preg_match($matchtag, $tagname)) { + foreach ($matchattrs as $matchattr => $valary) { + if (preg_match($matchattr, $attname)) { + [$valmatch, $valrepl] = $valary; + + $newvalue = preg_replace($valmatch, $valrepl, $attvalue); + + if ($newvalue != $attvalue) { + $attary[$attname] = $newvalue; + $attvalue = $newvalue; + } + } + } + } + } + } + + /** + * See if we need to append any attributes to this tag. + */ + foreach ($add_attr_to_tag as $matchtag => $addattary) { + if (preg_match($matchtag, $tagname)) { + $attary = array_merge($attary, $addattary); + } + } + + return $attary; + } + + /** + * Fix url. + * + * @return void + */ + public function tln_fixurl($attname, &$attvalue, $trans_image_path, $block_external_images) + { + $sQuote = '"'; + + $attvalue = trim($attvalue); + + if ($attvalue && ($attvalue[0] == '"' || $attvalue[0] == "'")) { + // remove the double quotes + $sQuote = $attvalue[0]; + + $attvalue = trim(substr($attvalue, 1, -1)); + } + + /** + * Replace empty src tags with the blank image. src is only used + * for frames, images, and image inputs. Doing a replace should + * not affect them working as should be, however it will stop + * IE from being kicked off when src for img tags are not set. + */ + if ($attvalue == '') { + $attvalue = $sQuote.$trans_image_path.$sQuote; + } else { + // first, disallow 8 bit characters and control characters + if (preg_match('/[\0-\37\200-\377]+/', $attvalue)) { + switch ($attname) { + case 'href': + $attvalue = $sQuote.'http://invalid-stuff-detected.example.com'.$sQuote; + break; + + default: + $attvalue = $sQuote.$trans_image_path.$sQuote; + break; + + } + } else { + $aUrl = parse_url($attvalue); + + if (isset($aUrl['scheme'])) { + switch (strtolower($aUrl['scheme'])) { + case 'mailto': + case 'http': + case 'https': + case 'ftp': + if ($attname != 'href') { + if ($block_external_images == true) { + $attvalue = $sQuote.$trans_image_path.$sQuote; + } else { + if (! isset($aUrl['path'])) { + $attvalue = $sQuote.$trans_image_path.$sQuote; + } + } + } else { + $attvalue = $sQuote.$attvalue.$sQuote; + } + break; + + case 'outbind': + $attvalue = $sQuote.$attvalue.$sQuote; + break; + + case 'cid': + $attvalue = $sQuote.$attvalue.$sQuote; + break; + + default: + $attvalue = $sQuote.$trans_image_path.$sQuote; + break; + } + } else { + if (! isset($aUrl['path']) || $aUrl['path'] != $trans_image_path) { + $$attvalue = $sQuote.$trans_image_path.$sQuote; + } + } + } + } + } + + /** + * Fix style. + * + * @return void + */ + public function tln_fixstyle($body, $pos, $trans_image_path, $block_external_images) + { + $me = 'tln_fixstyle'; + + $content = ''; + + $sToken = ''; + + $bSucces = false; + + $bEndTag = false; + + for ($i = $pos,$iCount = strlen($body); $i < $iCount; $i++) { + $char = $body[$i]; + + switch ($char) { + case '<': + $sToken = $char; + break; + case '/': + if ($sToken == '<') { + $sToken .= $char; + + $bEndTag = true; + } else { + $content .= $char; + } + + break; + + case '>': + if ($bEndTag) { + $sToken .= $char; + + if (preg_match('/\<\/\s*style\s*\>/i', $sToken, $aMatch)) { + $newpos = $i + 1; + + $bSucces = true; + + break 2; + + } else { + $content .= $sToken; + } + + $bEndTag = false; + } else { + $content .= $char; + } + break; + case '!': + if ($sToken == '<') { + if (isset($body[$i + 2]) && substr($body, $i, 3) == '!--') { + $i = strpos($body, '-->', $i + 3); + + if (! $i) { + $i = strlen($body); + } + + $sToken = ''; + } + } else { + $content .= $char; + } + break; + default: + if ($bEndTag) { + $sToken .= $char; + } else { + $content .= $char; + } + break; + } + } + + if (! $bSucces) { + return [false, strlen($body)]; + } + + /** + * First look for general BODY style declaration, which would be + * like so: + * body {background: blah-blah} + * and change it to .bodyclass so we can just assign it to a
    + */ + $content = preg_replace("|body(\s*\{.*?\})|si", '.bodyclass\\1', $content); + + $trans_image_path = $trans_image_path; + + // first check for 8bit sequences and disallowed control characters + if (preg_match('/[\16-\37\200-\377]+/', $content)) { + $content = ''; + + return [$content, $newpos]; + } + + // remove @import line + $content = preg_replace("/^\s*(@import.*)$/mi", "\n\n", $content); + + $content = preg_replace('/(\\\\)?u(\\\\)?r(\\\\)?l(\\\\)?/i', 'url', $content); + + preg_match_all("/url\s*\((.+)\)/si", $content, $aMatch); + + if (count($aMatch)) { + $aValue = $aReplace = []; + + foreach ($aMatch[1] as $sMatch) { + $urlvalue = $sMatch; + $this->tln_fixurl('style', $urlvalue, $trans_image_path, $block_external_images); + $aValue[] = $sMatch; + $aReplace[] = $urlvalue; + } + + $content = str_replace($aValue, $aReplace, $content); + } + + /** + * Remove any backslashes, entities, and extraneous whitespace. + */ + $contentTemp = $content; + $this->tln_defang($contentTemp); + $this->tln_unspace($contentTemp); + + $match = ['/\/\*.*\*\//', + '/expression/i', + '/behaviou*r/i', + '/binding/i', + '/include-source/i', + '/javascript/i', + '/script/i', + '/position/i']; + + $replace = ['', 'idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', '']; + + $contentNew = preg_replace($match, $replace, $contentTemp); + + if ($contentNew !== $contentTemp) { + $content = $contentNew; + } + + return [$content, $newpos]; + } + + /** + * Body to div. + * + * @return void + */ + public function tln_body2div($attary, $trans_image_path) + { + $me = 'tln_body2div'; + + $divattary = ['class' => "'bodyclass'"]; + + $has_bgc_stl = $has_txt_stl = false; + + $styledef = ''; + + if (is_array($attary) && count($attary) > 0) { + foreach ($attary as $attname => $attvalue) { + $quotchar = substr($attvalue, 0, 1); + + $attvalue = str_replace($quotchar, '', $attvalue); + + switch ($attname) { + case 'background': + $styledef .= "background-image: url('$trans_image_path'); "; + break; + + case 'bgcolor': + $has_bgc_stl = true; + + $styledef .= "background-color: $attvalue; "; + break; + + case 'text': + $has_txt_stl = true; + + $styledef .= "color: $attvalue; "; + break; + + } + } + + // Outlook defines a white bgcolor and no text color. This can lead to white text on a white bg with certain themes. + if ($has_bgc_stl && ! $has_txt_stl) { + $styledef .= 'color: #000000; '; + } + + if (strlen($styledef) > 0) { + $divattary['style'] = "\"$styledef\""; + } + } + + return $divattary; + } + + /** + * Sanitize. + * + * @param string $body + * @param array $tag_list + * @param array $rm_tags_with_content + * @param array $self_closing_tags + * @param bool $force_tag_closing + * @param array $rm_attnames + * @param array $bad_attvals + * @param array $add_attr_to_tag + * @param string $trans_image_path + * @param bool $block_external_images + * @return string + */ + public function tln_sanitize( + $body, + $tag_list, + $rm_tags_with_content, + $self_closing_tags, + $force_tag_closing, + $rm_attnames, + $bad_attvals, + $add_attr_to_tag, + $trans_image_path, + $block_external_images + ) { + /** + * Normalize rm_tags and rm_tags_with_content. + */ + $rm_tags = array_shift($tag_list); + + @array_walk($tag_list, [$this, 'tln_casenormalize']); + + @array_walk($rm_tags_with_content, [$this, 'tln_casenormalize']); + + @array_walk($self_closing_tags, [$this, 'tln_casenormalize']); + + /** + * See if tag_list is of tags to remove or tags to allow. + * false means remove these tags + * true means allow these tags + */ + $curpos = 0; + $open_tags = []; + $trusted = ''; + $skip_content = false; + + /** + * Take care of netscape's stupid javascript entities like + * &{alert('boo')}; + */ + $body = preg_replace('/&(\{.*?\};)/si', '&\\1', $body); + + while (($curtag = $this->tln_getnxtag($body, $curpos)) != false) { + [$tagname, $attary, $tagtype, $lt, $gt] = $curtag; + $free_content = substr($body, $curpos, $lt - $curpos); + + /** + * Take care of + + {!! view_render_event('webform.layout.head') !!} + + + + {!! view_render_event('webform.layout.body.before') !!} + +
    + + + + {!! view_render_event('webform.layout.content.before') !!} + + + {{ $slot }} + + {!! view_render_event('webform.layout.content.after') !!} +
    + + {!! view_render_event('webform.layout.body.after') !!} + + @stack('scripts') + + {!! view_render_event('webform.layout.vue-app-mount.before') !!} + + + + {!! view_render_event('webform.layout.vue-app-mount.after') !!} + + + + + diff --git a/packages/Webkul/WebForm/src/Resources/views/components/spinner/index.blade.php b/packages/Webkul/WebForm/src/Resources/views/components/spinner/index.blade.php new file mode 100644 index 0000000..e195748 --- /dev/null +++ b/packages/Webkul/WebForm/src/Resources/views/components/spinner/index.blade.php @@ -0,0 +1,27 @@ + +@props(['color' => 'currentColor']) + + \ No newline at end of file diff --git a/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/controls.blade.php b/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/controls.blade.php new file mode 100644 index 0000000..871f0c3 --- /dev/null +++ b/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/controls.blade.php @@ -0,0 +1,270 @@ +@foreach ($webForm->attributes as $attribute) + @php + $parentAttribute = $attribute->attribute; + + $fieldName = $parentAttribute->entity_type . '[' . $parentAttribute->code . ']'; + + $validations = $attribute->is_required ? 'required' : ''; + @endphp + + + + {{ $attribute->name ?? $parentAttribute->name }} + + + @switch($parentAttribute->type) + @case('text') + + + + + @break + + @case('textarea') + + + + @break + + @case('price') + + + + + @break + + @case('email') + + + + + + + @break + + @case('checkbox') + @php + $options = $parentAttribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($parentAttribute->lookup_type) + : $parentAttribute->options()->orderBy('sort_order')->get(); + @endphp + + @foreach ($options as $option) + + + + + + @endforeach + + @case('file') + @case('image') + + + + + @break; + + @case('phone') + + + + + + + @break + + @case('date') + + + + + @break + + @case('datetime') + + + + + @break + + @case('select') + @case('lookup') + @php + $options = $parentAttribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($parentAttribute->lookup_type) + : $parentAttribute->options()->orderBy('sort_order')->get(); + @endphp + + + @foreach ($options as $option) + + @endforeach + + + + + @break + + @case('multiselect') + @php + $options = $parentAttribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($parentAttribute->lookup_type) + : $parentAttribute->options()->orderBy('sort_order')->get(); + @endphp + + + @foreach ($options as $option) + + @endforeach + + + + + @break + + @case('checkbox') +
    + @php + $options = $parentAttribute->lookup_type + ? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($parentAttribute->lookup_type) + : $parentAttribute->options()->orderBy('sort_order')->get(); + @endphp + + @foreach ($options as $option) + + + + + {{ $option->name }} + + @endforeach +
    + +

    + + @break + + @case('boolean') + + + + + + + + @break + @endswitch +
    +@endforeach diff --git a/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/embed.blade.php b/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/embed.blade.php new file mode 100644 index 0000000..f726040 --- /dev/null +++ b/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/embed.blade.php @@ -0,0 +1,10 @@ +(function() { + /** + * The preview HTML is handed to document.write as a JSON-encoded string rather than + * interpolated into a template literal. Blade escapes the form's stored fields for HTML, which + * leaves dollar-brace sequences untouched - inert in markup, but evaluated as code by the + * engine once that markup sits between backticks. The replaceAll this replaces could never + * help, since interpolation resolves before the call runs. + */ + document.write(@json(view('web_form::settings.web-forms.preview', compact('webForm'))->render())); +})(); diff --git a/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/preview.blade.php b/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/preview.blade.php new file mode 100644 index 0000000..9881f5e --- /dev/null +++ b/packages/Webkul/WebForm/src/Resources/views/settings/web-forms/preview.blade.php @@ -0,0 +1,164 @@ + + + {{ strip_tags($webForm->title) }} + + + + +
    +
    + +
    +
    +
    + + @pushOnce('scripts') + + + + @endPushOnce +
    diff --git a/packages/Webkul/WebForm/src/Routes/routes.php b/packages/Webkul/WebForm/src/Routes/routes.php new file mode 100644 index 0000000..668f0bd --- /dev/null +++ b/packages/Webkul/WebForm/src/Routes/routes.php @@ -0,0 +1,16 @@ +middleware(['web', 'admin_locale'])->prefix('web-forms')->group(function () { + Route::get('forms/{id}/form.js', 'formJS')->name('admin.settings.web_forms.form_js'); + + Route::get('forms/{id}/form.html', 'preview')->name('admin.settings.web_forms.preview'); + + Route::post('forms/{id}', 'formStore')->name('admin.settings.web_forms.form_store'); + + Route::group(['middleware' => ['user']], function () { + Route::get('form/{id}/form.html', 'view')->name('admin.settings.web_forms.view'); + }); +}); diff --git a/packages/Webkul/WebForm/src/Rules/PhoneNumber.php b/packages/Webkul/WebForm/src/Rules/PhoneNumber.php new file mode 100644 index 0000000..be54fe8 --- /dev/null +++ b/packages/Webkul/WebForm/src/Rules/PhoneNumber.php @@ -0,0 +1,33 @@ + { + const envDir = "../../../"; + + Object.assign(process.env, loadEnv(mode, envDir)); + + return { + build: { + emptyOutDir: true, + }, + + envDir, + + server: { + host: process.env.VITE_HOST || "localhost", + port: process.env.VITE_PORT || 5174, + }, + + plugins: [ + vue(), + + laravel({ + hotFile: "../../../public/webform-vite.hot", + publicDirectory: "../../../public", + buildDirectory: "webform/build", + input: [ + "src/Resources/assets/css/app.css", + "src/Resources/assets/js/app.js", + ], + refresh: true, + }), + ], + + experimental: { + renderBuiltUrl(filename, { hostId, hostType, type }) { + if (hostType === "css") { + return path.basename(filename); + } + }, + }, + }; +}); \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..25d918c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,36 @@ + + + + + ./tests/Unit + + + + ./tests/Feature + + + + + + ./app + + + + + + + + + + + + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..a6f6d3e --- /dev/null +++ b/pint.json @@ -0,0 +1,10 @@ +{ + "preset": "laravel", + "rules": { + "binary_operator_spaces": { + "operators": { + "=>": "single_space" + } + } + } +} \ No newline at end of file diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b9db6a7 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,22 @@ +# Rewrite Module + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Handle Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/admin/build/assets/activities-DpB2XKqX.svg b/public/admin/build/assets/activities-DpB2XKqX.svg new file mode 100644 index 0000000..890d0ea --- /dev/null +++ b/public/admin/build/assets/activities-DpB2XKqX.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/app-CaTJYsX9.css b/public/admin/build/assets/app-CaTJYsX9.css new file mode 100644 index 0000000..6f7e5b2 --- /dev/null +++ b/public/admin/build/assets/app-CaTJYsX9.css @@ -0,0 +1 @@ +.flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,.08);box-shadow:1px 0 #e6e6e6,-1px 0 #e6e6e6,0 1px #e6e6e6,0 -1px #e6e6e6,0 3px 13px #00000014}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1);animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px)}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none!important;box-shadow:none!important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 #e6e6e6,5px 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:"";height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.arrowRight:after{left:auto;right:22px}.flatpickr-calendar.arrowCenter:before,.flatpickr-calendar.arrowCenter:after{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-months .flatpickr-month{background:transparent;color:#000000e6;fill:#000000e6;height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:#000000e6;fill:#000000e6}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{left:0}.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,.flatpickr-months .flatpickr-next-month.flatpickr-next-month{right:0}.flatpickr-months .flatpickr-prev-month:hover,.flatpickr-months .flatpickr-next-month:hover{color:#959ea9}.flatpickr-months .flatpickr-prev-month:hover svg,.flatpickr-months .flatpickr-next-month:hover svg{fill:#f64747}.flatpickr-months .flatpickr-prev-month svg,.flatpickr-months .flatpickr-next-month svg{width:14px;height:14px}.flatpickr-months .flatpickr-prev-month svg path,.flatpickr-months .flatpickr-next-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%}.numInputWrapper input::-ms-clear{display:none}.numInputWrapper input::-webkit-outer-spin-button,.numInputWrapper input::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,.15);-webkit-box-sizing:border-box;box-sizing:border-box}.numInputWrapper span:hover{background:#0000001a}.numInputWrapper span:active{background:#0003}.numInputWrapper span:after{display:block;content:"";position:absolute}.numInputWrapper span.arrowUp{top:0;border-bottom:0}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,.6);top:26%}.numInputWrapper span.arrowDown{top:50%}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,.6);top:40%}.numInputWrapper span svg{width:inherit;height:auto}.numInputWrapper span svg path{fill:#00000080}.numInputWrapper:hover{background:#0000000d}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{font-size:135%;line-height:inherit;font-weight:300;color:inherit;position:absolute;width:75%;left:12.5%;padding:7.48px 0 0;line-height:1;height:34px;display:inline-block;text-align:center;-webkit-transform:translate3d(0px,0px,0px);transform:translateZ(0)}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0}.flatpickr-current-month span.cur-month:hover{background:#0000000d}.flatpickr-current-month .numInputWrapper{width:6ch;width:7ch�;display:inline-block}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:#000000e6}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:#000000e6}.flatpickr-current-month input.cur-year{background:transparent;-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;cursor:text;padding:0 0 0 .5ch;margin:0;display:inline-block;font-size:inherit;font-family:inherit;font-weight:300;line-height:inherit;height:auto;border:0;border-radius:0;vertical-align:initial;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{font-size:100%;color:#00000080;background:transparent;pointer-events:none}.flatpickr-current-month .flatpickr-monthDropdown-months{appearance:menulist;background:transparent;border:none;border-radius:0;box-sizing:border-box;color:inherit;cursor:pointer;font-size:inherit;font-family:inherit;font-weight:300;height:auto;line-height:inherit;margin:-1px 0 0;outline:none;padding:0 0 0 .5ch;position:relative;vertical-align:initial;-webkit-box-sizing:border-box;-webkit-appearance:menulist;-moz-appearance:menulist;width:auto}.flatpickr-current-month .flatpickr-monthDropdown-months:focus,.flatpickr-current-month .flatpickr-monthDropdown-months:active{outline:none}.flatpickr-current-month .flatpickr-monthDropdown-months:hover{background:#0000000d}.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{background-color:transparent;outline:none;padding:0}.flatpickr-weekdays{background:transparent;text-align:center;overflow:hidden;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:28px}.flatpickr-weekdays .flatpickr-weekdaycontainer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}span.flatpickr-weekday{cursor:default;font-size:90%;background:transparent;color:#0000008a;line-height:1;margin:0;text-align:center;display:block;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;font-weight:bolder}.dayContainer,.flatpickr-weeks{padding:1px 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;width:307.875px}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:307.875px;min-width:307.875px;max-width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;display:inline-block;display:-ms-flexbox;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-wrap:wrap;-ms-flex-pack:justify;-webkit-justify-content:space-around;justify-content:space-around;-webkit-transform:translate3d(0px,0px,0px);transform:translateZ(0);opacity:1}.dayContainer+.dayContainer{-webkit-box-shadow:-1px 0 0 #e6e6e6;box-shadow:-1px 0 #e6e6e6}.flatpickr-day{background:none;border:1px solid transparent;border-radius:150px;-webkit-box-sizing:border-box;box-sizing:border-box;color:#393939;cursor:pointer;font-weight:400;width:14.2857143%;-webkit-flex-basis:14.2857143%;-ms-flex-preferred-size:14.2857143%;flex-basis:14.2857143%;max-width:39px;height:39px;line-height:39px;margin:0;display:inline-block;position:relative;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center}.flatpickr-day.inRange,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.today.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day:hover,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.nextMonthDay:hover,.flatpickr-day:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.nextMonthDay:focus{cursor:pointer;outline:0;background:#e6e6e6;border-color:#e6e6e6}.flatpickr-day.today{border-color:#959ea9}.flatpickr-day.today:hover,.flatpickr-day.today:focus{border-color:#959ea9;background:#959ea9;color:#fff}.flatpickr-day.selected,.flatpickr-day.startRange,.flatpickr-day.endRange,.flatpickr-day.selected.inRange,.flatpickr-day.startRange.inRange,.flatpickr-day.endRange.inRange,.flatpickr-day.selected:focus,.flatpickr-day.startRange:focus,.flatpickr-day.endRange:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange:hover,.flatpickr-day.endRange:hover,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.endRange.nextMonthDay{background:#569ff7;-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:#569ff7}.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange,.flatpickr-day.endRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange,.flatpickr-day.endRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.selected.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.endRange.startRange+.endRange:not(:nth-child(7n+1)){-webkit-box-shadow:-10px 0 0 #569ff7;box-shadow:-10px 0 #569ff7}.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange,.flatpickr-day.endRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;-webkit-box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-5px 0 #e6e6e6,5px 0 #e6e6e6}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover,.flatpickr-day.prevMonthDay,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.notAllowed.nextMonthDay{color:#3939394d;background:transparent;border-color:transparent;cursor:default}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{cursor:not-allowed;color:#3939391a}.flatpickr-day.week.selected{border-radius:0;-webkit-box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7;box-shadow:-5px 0 #569ff7,5px 0 #569ff7}.flatpickr-day.hidden{visibility:hidden}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{float:left}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 #e6e6e6;box-shadow:1px 0 #e6e6e6}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day,.flatpickr-weekwrapper span.flatpickr-day:hover{display:block;width:100%;max-width:none;color:#3939394d;background:transparent;cursor:default;border:none}.flatpickr-innerContainer{display:block;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden}.flatpickr-rContainer{display:inline-block;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-time:after{content:"";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;width:40%;height:40px;float:left}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:#393939}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:#393939}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:transparent;-webkit-box-shadow:none;box-shadow:none;border:0;border-radius:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;color:#393939;font-size:14px;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-time input.flatpickr-hour{font-weight:700}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-time-separator,.flatpickr-time .flatpickr-am-pm{height:inherit;float:left;line-height:inherit;color:#393939;font-weight:700;width:2%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time input:hover,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time .flatpickr-am-pm:focus{background:#eee}.flatpickr-input[readonly]{cursor:pointer}@-webkit-keyframes fpFadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translateZ(0)}}@keyframes fpFadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translateZ(0)}}.vuecal__weekdays-headings{border-bottom:1px solid #ddd;margin-bottom:-1px}.vuecal--week-numbers .vuecal__weekdays-headings,.vuecal--view-with-time .vuecal__weekdays-headings{padding-left:3em}.vuecal--view-with-time.vuecal--twelve-hour .vuecal__weekdays-headings{font-size:.9em;padding-left:4em}.vuecal--overflow-x.vuecal--view-with-time .vuecal__weekdays-headings{padding-left:0}.vuecal__heading{width:100%;height:2.8em;font-weight:400;justify-content:center;text-align:center;align-items:center;position:relative;overflow:hidden}.vuecal__heading>.vuecal__flex{width:100%;height:100%;align-items:initial!important}.vuecal--sticky-split-labels .vuecal__heading{height:3.4em}.vuecal--month-view .vuecal__heading,.vuecal--week-view .vuecal__heading,.vuecal--day-view .vuecal__heading{width:14.2857%}.vuecal--hide-weekends.vuecal--month-view .vuecal__heading,.vuecal--hide-weekends.vuecal--week-view .vuecal__heading,.vuecal--hide-weekends.vuecal--day-view .vuecal__heading,.vuecal--years-view .vuecal__heading{width:20%}.vuecal--year-view .vuecal__heading{width:33.33%}.vuecal__heading .weekday-label{flex-shrink:0;display:flex;justify-content:center;align-items:center}.vuecal--small .vuecal__heading .small,.vuecal--xsmall .vuecal__heading .xsmall{display:block}.vuecal__heading .small,.vuecal__heading .xsmall,.vuecal--small .vuecal__heading .full,.vuecal--small .vuecal__heading .xsmall,.vuecal--xsmall .vuecal__heading .full,.vuecal--xsmall .vuecal__heading .small{display:none}.vuecal .vuecal__split-days-headers{align-items:center}@media screen and (max-width:550px){.vuecal__heading{line-height:1.2}.vuecal__heading .small,.vuecal--small .vuecal__heading .small,.vuecal--xsmall .vuecal__heading .xsmall{display:block}.vuecal__heading .full,.vuecal__heading .xsmall,.vuecal--small .vuecal__heading .full,.vuecal--small .vuecal__heading .xsmall,.vuecal--xsmall .vuecal__heading .full,.vuecal--xsmall .vuecal__heading .small{display:none}.vuecal--overflow-x .vuecal__heading .full,.vuecal--small.vuecal--overflow-x .vuecal__heading .small,.vuecal--xsmall.vuecal--overflow-x .vuecal__heading .xsmall{display:block}.vuecal--overflow-x .vuecal__heading .small,.vuecal--overflow-x .vuecal__heading .xsmall,.vuecal--small.vuecal--overflow-x .vuecal__heading .full,.vuecal--small.vuecal--overflow-x .vuecal__heading .xsmall,.vuecal--xsmall.vuecal--overflow-x .vuecal__heading .full,.vuecal--xsmall.vuecal--overflow-x .vuecal__heading .small{display:none}}@media screen and (max-width:450px){.vuecal__heading .xsmall,.vuecal--small .vuecal__heading .xsmall,.vuecal--xsmall .vuecal__heading .xsmall{display:block}.vuecal__heading .full,.vuecal__heading .small,.vuecal--small .vuecal__heading .full,.vuecal--small .vuecal__heading .small,.vuecal--xsmall .vuecal__heading .full,.vuecal--xsmall .vuecal__heading .small{display:none}.vuecal--small.vuecal--overflow-x .vuecal__heading .small,.vuecal--xsmall.vuecal--overflow-x .vuecal__heading .xsmall{display:block}.vuecal--small.vuecal--overflow-x .vuecal__heading .full,.vuecal--small.vuecal--overflow-x .vuecal__heading .xsmall,.vuecal--xsmall.vuecal--overflow-x .vuecal__heading .full,.vuecal--xsmall.vuecal--overflow-x .vuecal__heading .small{display:none}}.vuecal__header button{outline:none;font-family:inherit}.vuecal__menu{padding:0;margin:0;list-style-type:none;justify-content:center;background-color:#00000005}.vuecal__view-btn{background:none;border:none;padding:.3em 1em;height:2.2em;font-size:1.3em;border-bottom:0 solid currentColor;cursor:pointer;color:inherit;box-sizing:border-box;transition:.2s}.vuecal__view-btn--active{border-bottom-width:2px;background:#ffffff26}.vuecal__title-bar{background-color:#0000001a;display:flex;align-items:center;text-align:center;justify-content:space-between;font-size:1.4em;line-height:1.3;min-height:2em}.vuecal--xsmall .vuecal__title-bar{font-size:1.3em}.vuecal__title{position:relative;justify-content:center}.vuecal__title button{cursor:pointer;background:none;border:none}.vuecal__title button.slide-fade--left-leave-active,.vuecal__title button.slide-fade--right-leave-active{width:100%}.vuecal__today-btn{position:relative;align-items:center;display:flex;font-size:.8em;background:none;border:none}.vuecal__today-btn span.default{font-size:.8em;padding:3px 6px;text-transform:uppercase;cursor:pointer}.vuecal__arrow{cursor:pointer;position:relative;z-index:1;background:none;border:none;white-space:nowrap}.vuecal__arrow--prev{margin-left:.6em}.vuecal__arrow--next{margin-right:.6em}.vuecal__arrow i.angle{display:inline-block;border:solid currentColor;border-width:0 2px 2px 0;padding:.25em;transform:rotate(-45deg)}.vuecal__arrow--prev i.angle{border-width:2px 0 0 2px}.vuecal__arrow--highlighted,.vuecal__today-btn--highlighted,.vuecal__view-btn--highlighted{position:relative;background-color:#0000000a}.vuecal__arrow--highlighted *,.vuecal__today-btn--highlighted *,.vuecal__view-btn--highlighted *{pointer-events:none}.vuecal__arrow--highlighted:before,.vuecal__arrow--highlighted:after,.vuecal__today-btn--highlighted:before,.vuecal__today-btn--highlighted:after,.vuecal__view-btn--highlighted:before,.vuecal__view-btn--highlighted:after{content:"";background-color:inherit;animation:sonar .8s infinite ease-out;position:absolute;top:50%;left:50%;pointer-events:none}.vuecal__arrow--highlighted:before,.vuecal__today-btn--highlighted:before,.vuecal__view-btn--highlighted:before{width:3em;height:3em;border-radius:3em;margin-top:-1.5em;margin-left:-1.5em}.vuecal__arrow--highlighted:after,.vuecal__today-btn--highlighted:after,.vuecal__view-btn--highlighted:after{animation-duration:1.5s;animation-delay:.1s;width:2.6em;height:2.6em;border-radius:2.6em;margin-top:-1.3em;margin-left:-1.3em}@keyframes sonar{0%,20%{opacity:1}to{transform:scale(2.5);opacity:0}}@media screen and (max-width:450px){.vuecal__title{font-size:.9em}.vuecal__view-btn{padding-left:.6em;padding-right:.6em}}@media screen and (max-width:350px){.vuecal__view-btn{font-size:1.1em}}.vuecal__event{color:#666;background-color:#f8f8f8cc;position:relative;box-sizing:border-box;left:0;width:100%;z-index:1;transition:box-shadow .3s,left .3s,width .3s;overflow:hidden}.vuecal--no-time .vuecal__event{min-height:8px}.vuecal:not(.vuecal--dragging-event) .vuecal__event:hover{z-index:2}.vuecal__cell .vuecal__event *{-webkit-user-select:text;-moz-user-select:text;user-select:text}.vuecal--view-with-time .vuecal__event:not(.vuecal__event--all-day){position:absolute}.vuecal--view-with-time .vuecal__bg .vuecal__event--all-day{position:absolute;top:0;bottom:0;z-index:0;opacity:.6;width:auto;right:0}.vuecal--view-with-time .vuecal__all-day .vuecal__event--all-day{position:relative;left:0}.vuecal__event--background{z-index:0}.vuecal__event--focus,.vuecal__event:focus{box-shadow:1px 1px 6px #0003;z-index:3;outline:none}.vuecal__event.vuecal__event--dragging{opacity:.7}.vuecal__event.vuecal__event--static{opacity:0;transition:opacity .1s}@-moz-document url-prefix(){.vuecal__event.vuecal__event--dragging{opacity:1}}.vuecal__event-resize-handle{position:absolute;bottom:0;left:0;right:0;height:1em;background-color:#ffffff4d;opacity:0;transform:translateY(110%);transition:.3s;cursor:ns-resize}.vuecal__event:hover .vuecal__event-resize-handle,.vuecal__event:focus .vuecal__event-resize-handle,.vuecal__event--focus .vuecal__event-resize-handle,.vuecal__event--resizing .vuecal__event-resize-handle{opacity:1;transform:translateY(0)}.vuecal__event--dragging .vuecal__event-resize-handle{display:none}.vuecal__event-delete{position:absolute;top:0;left:0;right:0;display:flex;flex-direction:column;align-items:center;justify-content:center;height:1.4em;line-height:1.4em;background-color:#dd3333d9;color:#fff;z-index:0;cursor:pointer;transform:translateY(-110%);transition:.3s}.vuecal__event .vuecal__event-delete{-webkit-user-select:none;-moz-user-select:none;user-select:none}.vuecal--full-height-delete .vuecal__event-delete{height:auto;bottom:0}.vuecal--full-height-delete .vuecal__event-delete:before{content:"";width:1.7em;height:1.8em;display:block;background-image:url('data:image/svg+xml;utf8,')}.vuecal__event--deletable .vuecal__event-delete{transform:translateY(0);z-index:1}.vuecal__event--deletable.vuecal__event--dragging .vuecal__event-delete{opacity:0;transition:none}.vuecal--month-view .vuecal__event-title{font-size:.85em}.vuecal--short-events .vuecal__event-title{text-align:left;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;padding:0 3px}.vuecal__event-title,.vuecal__event-content{-webkit-hyphens:auto;hyphens:auto}.vuecal__event-title--edit{border-bottom:1px solid transparent;text-align:center;transition:.3s;color:inherit;background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:120% .15em;background-size:.4em;outline:none;width:100%}.vuecal__event-title--edit:hover,.vuecal__event-title--edit:focus{border-color:#0006;background-position:99% .15em;background-size:1.2em}.vuecal__cell{position:relative;width:100%;display:flex;justify-content:center;align-items:center;text-align:center;transition:.15s ease-in-out background-color}.vuecal__cells.month-view .vuecal__cell,.vuecal__cells.week-view .vuecal__cell{width:14.2857%}.vuecal--hide-weekends .vuecal__cells.month-view .vuecal__cell,.vuecal--hide-weekends .vuecal__cells.week-view .vuecal__cell,.vuecal__cells.years-view .vuecal__cell{width:20%}.vuecal__cells.year-view .vuecal__cell{width:33.33%}.vuecal__cells.day-view .vuecal__cell{flex:1}.vuecal--overflow-x.vuecal--day-view .vuecal__cell{width:auto}.vuecal--click-to-navigate .vuecal__cell:not(.vuecal__cell--disabled){cursor:pointer}.vuecal--view-with-time .vuecal__cell,.vuecal--week-view.vuecal--no-time .vuecal__cell:not(.vuecal__cell--has-splits),.vuecal--day-view.vuecal--no-time .vuecal__cell:not(.vuecal__cell--has-splits){display:block}.vuecal__cell.vuecal__cell--has-splits{flex-direction:row;display:flex}.vuecal__cell:before{content:"";position:absolute;z-index:0;top:0;left:0;right:-1px;bottom:-1px;border:1px solid rgba(196,196,196,.25)}.vuecal--overflow-x.vuecal--day-view .vuecal__cell:before{bottom:0}.vuecal__cell--today,.vuecal__cell--current{background-color:#f0f0ff66;z-index:1}.vuecal__cell--selected{background-color:#ebfff566;z-index:2}.vuecal--day-view .vuecal__cell--selected{background:none}.vuecal__cell--out-of-scope{color:#00000040}.vuecal__cell--disabled{color:#00000040;cursor:not-allowed}.vuecal__cell--highlighted:not(.vuecal__cell--has-splits),.vuecal__cell-split.vuecal__cell-split--highlighted{background-color:#0000000a;transition-duration:5ms}.vuecal__cell-content{position:relative;width:100%;height:100%;outline:none}.vuecal--years-view .vuecal__cell-content,.vuecal--year-view .vuecal__cell-content,.vuecal--month-view .vuecal__cell-content{justify-content:center}.vuecal__cell .cell-time-labels{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column}.vuecal__cell .cell-time-label{flex-grow:1;font-size:.8em;opacity:.3;line-height:1.7}.vuecal__cell-split{display:flex;flex-grow:1;flex-direction:column;height:100%;position:relative;transition:.15s ease-in-out background-color}.vuecal__cell-events{width:100%}.vuecal__cell-events-count{position:absolute;left:50%;top:65%;transform:translate(-50%);min-width:12px;height:12px;line-height:12px;padding:0 3px;background:#999;color:#fff;border-radius:12px;font-size:10px;box-sizing:border-box}.vuecal__cell .vuecal__special-hours{position:absolute;left:0;right:0;box-sizing:border-box}.vuecal--overflow-x.vuecal--week-view .vuecal__cell,.vuecal__cell-split{overflow:hidden}.vuecal__no-event{padding-top:1em;color:#aaa;justify-self:flex-start;margin-bottom:auto}.vuecal__all-day .vuecal__no-event{display:none}.vuecal__now-line{position:absolute;left:0;width:100%;height:0;color:red;border-top:1px solid currentColor;opacity:.6;z-index:1}.vuecal__now-line:before{content:"";position:absolute;top:-6px;left:0;border:5px solid transparent;border-left-color:currentColor}.vuecal{height:100%;box-shadow:0 0 0 1px inset #00000014}.vuecal *{-webkit-user-select:none;-moz-user-select:none;user-select:none}.vuecal--has-touch *:not(.vuecal__event-title--edit){-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.vuecal .clickable{cursor:pointer}.vuecal--resizing-event,.vuecal--drag-creating-event{cursor:ns-resize}.vuecal--dragging-event{cursor:move;cursor:grabbing}.vuecal .dragging-helper{position:absolute;width:60px;height:40px;background:#8abee6cc;border:1px solid #61a9e0;z-index:10}.vuecal--xsmall{font-size:.9em}.vuecal__flex{display:flex;flex-direction:row}.vuecal__flex[column]{flex-direction:column;flex:1 1 auto}.vuecal__flex[grow]{flex:1 1 auto;width:100%}.vuecal__flex[wrap]{flex-wrap:wrap}.vuecal__split-days-headers.slide-fade--right-leave-active{display:none}.vuecal--week-numbers.vuecal--month-view .vuecal__split-days-headers{margin-left:3em}.vuecal--day-view:not(.vuecal--overflow-x) .vuecal__split-days-headers{margin-left:3em;height:2.2em}.vuecal--day-view.vuecal--twelve-hour:not(.vuecal--overflow-x) .vuecal__split-days-headers{margin-left:4em}.vuecal__split-days-headers .day-split-header{display:flex;flex-grow:1;flex-basis:0;justify-content:center;align-items:center;height:100%}.vuecal__split-days-headers .vuecal--day-view.vuecal--overflow-x.vuecal--sticky-split-labels .day-split-header{height:1.5em}.vuecal__body{position:relative;overflow:hidden}.vuecal__all-day{min-height:1.7em;margin-bottom:-1px;flex-shrink:0}.vuecal__all-day-text{width:3em;box-sizing:border-box;color:#999;padding-right:2px;display:flex;flex-shrink:0;align-items:center;justify-content:flex-end;border-bottom:1px solid #ddd;-webkit-hyphens:auto;hyphens:auto}.vuecal__all-day-text span{font-size:.85em;text-align:right;line-height:1.1}.vuecal--twelve-hour .vuecal__all-day>span{width:4em}.vuecal__bg{overflow:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;min-height:60px;position:relative;width:100%}.vuecal--no-time .vuecal__bg{display:flex;flex:1 1 auto;overflow:auto}.vuecal__week-numbers{width:3em;flex-shrink:0!important}.vuecal__week-numbers .vuecal__week-number-cell{opacity:.4;font-size:.9em;align-items:center;justify-items:center;justify-content:center}.vuecal__scrollbar-check{position:absolute;top:0;left:0;right:0;bottom:0;overflow:scroll;visibility:hidden;z-index:-1}.vuecal__scrollbar-check div{height:120%}.vuecal__time-column{width:3em;height:100%;flex-shrink:0}.vuecal--twelve-hour .vuecal__time-column{width:4em;font-size:.9em}.vuecal--overflow-x.vuecal--week-view .vuecal__time-column{margin-top:2.8em;box-shadow:0 1px 1px #0000004d}.vuecal--overflow-x.vuecal--week-view.vuecal--sticky-split-labels .vuecal__time-column{margin-top:3.4em}.vuecal--overflow-x.vuecal--day-view.vuecal--sticky-split-labels .vuecal__time-column{margin-top:1.5em}.vuecal__time-column .vuecal__time-cell{color:#999;text-align:right;padding-right:2px;font-size:.9em}.vuecal__time-column .vuecal__time-cell-line:before{content:"";position:absolute;left:0;right:0;border-top:1px solid rgba(196,196,196,.3)}.vuecal__cells{margin:0 1px 1px 0}.vuecal--overflow-x.vuecal--day-view .vuecal__cells{margin:0}.vuecal--events-on-month-view.vuecal--short-events .vuecal__cells{width:99.9%}.vuecal--overflow-x.vuecal--day-view .vuecal__cells,.vuecal--overflow-x.vuecal--week-view .vuecal__cells{flex-wrap:nowrap;overflow:auto}.slide-fade--left-enter-active,.slide-fade--left-leave-active,.slide-fade--right-enter-active,.slide-fade--right-leave-active{transition:.25s ease-out}.slide-fade--left-enter-from,.slide-fade--right-leave-to{transform:translate(-15px);opacity:0}.slide-fade--left-leave-to,.slide-fade--right-enter-from{transform:translate(15px);opacity:0}.slide-fade--left-leave-active,.slide-fade--right-leave-active{position:absolute!important;height:100%}.vuecal__title-bar .slide-fade--left-leave-active,.vuecal__title-bar .slide-fade--right-leave-active{left:0;right:0;height:auto}.vuecal__heading .slide-fade--left-leave-active,.vuecal__heading .slide-fade--right-leave-active{display:flex;align-items:center}.vuecal--green-theme .vuecal__menu,.vuecal--green-theme .vuecal__cell-events-count{background-color:#42b983;color:#fff}.vuecal--green-theme .vuecal__title-bar{background-color:#e4f5ef}.vuecal--green-theme .vuecal__cell--today,.vuecal--green-theme .vuecal__cell--current{background-color:#f0f0ff66}.vuecal--green-theme:not(.vuecal--day-view) .vuecal__cell--selected{background-color:#ebfff566}.vuecal--green-theme .vuecal__cell--selected:before{border-color:#42b98380}.vuecal--green-theme .vuecal__cell--highlighted:not(.vuecal__cell--has-splits),.vuecal--green-theme .vuecal__cell-split--highlighted{background-color:#c3ffe180}.vuecal--green-theme .vuecal__arrow--highlighted,.vuecal--green-theme .vuecal__today-btn--highlighted,.vuecal--green-theme .vuecal__view-btn--highlighted{background-color:#88ecbf40}.vuecal--blue-theme .vuecal__menu,.vuecal--blue-theme .vuecal__cell-events-count{background-color:#42a3b9cc;color:#fff}.vuecal--blue-theme .vuecal__title-bar{background-color:#00a5bc4d}.vuecal--blue-theme .vuecal__cell--today,.vuecal--blue-theme .vuecal__cell--current{background-color:#f0f0ff66}.vuecal--blue-theme:not(.vuecal--day-view) .vuecal__cell--selected{background-color:#ebfdff66}.vuecal--blue-theme .vuecal__cell--selected:before{border-color:#73bfcc80}.vuecal--blue-theme .vuecal__cell--highlighted:not(.vuecal__cell--has-splits),.vuecal--blue-theme .vuecal__cell-split--highlighted{background-color:#00a5bc0f}.vuecal--blue-theme .vuecal__arrow--highlighted,.vuecal--blue-theme .vuecal__today-btn--highlighted,.vuecal--blue-theme .vuecal__view-btn--highlighted{background-color:#42a3b933}.vuecal--rounded-theme .vuecal__weekdays-headings{border:none}.vuecal--rounded-theme .vuecal__cell,.vuecal--rounded-theme .vuecal__cell:before{background:none;border:none}.vuecal--rounded-theme .vuecal__cell--out-of-scope{opacity:.4}.vuecal--rounded-theme .vuecal__cell-content{width:30px;height:30px;flex-grow:0;border:1px solid transparent;border-radius:30px;color:#333}.vuecal--rounded-theme.vuecal--day-view .vuecal__cell-content{width:auto;background:none}.vuecal--rounded-theme.vuecal--year-view .vuecal__cell{width:33.33%}.vuecal--rounded-theme.vuecal--year-view .vuecal__cell-content{width:85px}.vuecal--rounded-theme.vuecal--years-view .vuecal__cell-content{width:52px}.vuecal--rounded-theme .vuecal__cell{background-color:transparent!important}.vuecal--rounded-theme.vuecal--green-theme:not(.vuecal--day-view) .vuecal__cell-content{background-color:#f1faf7}.vuecal--rounded-theme.vuecal--green-theme:not(.vuecal--day-view) .vuecal__cell--today .vuecal__cell-content{background-color:#42b983;color:#fff}.vuecal--rounded-theme.vuecal--green-theme .vuecal--day-view .vuecal__cell--today:before{background-color:#42b9830d}.vuecal--rounded-theme.vuecal--green-theme:not(.vuecal--day-view) .vuecal__cell--selected .vuecal__cell-content{border-color:#42b983}.vuecal--rounded-theme.vuecal--green-theme .vuecal__cell--highlighted:not(.vuecal__cell--has-splits),.vuecal--rounded-theme.vuecal--green-theme .vuecal__cell-split--highlighted{background-color:#c3ffe180}.vuecal--rounded-theme.vuecal--blue-theme:not(.vuecal--day-view) .vuecal__cell-content{background-color:#64b6ff33}.vuecal--rounded-theme.vuecal--blue-theme:not(.vuecal--day-view) .vuecal__cell--today .vuecal__cell-content{background-color:#8fb7e4;color:#fff}.vuecal--rounded-theme.vuecal--blue-theme .vuecal--day-view .vuecal__cell--today:before{background-color:#8fb7e41a}.vuecal--rounded-theme.vuecal--blue-theme:not(.vuecal--day-view) .vuecal__cell--selected .vuecal__cell-content{border-color:#61a9e0}.vuecal--rounded-theme.vuecal--blue-theme .vuecal__cell--highlighted:not(.vuecal__cell--has-splits),.vuecal--rounded-theme.vuecal--blue-theme .vuecal__cell-split--highlighted{background-color:#00a5bc0f}.vuecal--date-picker .vuecal__title-bar{font-size:1.2em}.vuecal--date-picker .vuecal__heading{height:2.2em;font-weight:500;opacity:.4}.vuecal--date-picker .vuecal__weekdays-headings{border:none}.vuecal--date-picker .vuecal__body{margin-left:1px}.vuecal--date-picker .vuecal__cell,.vuecal--date-picker .vuecal__cell:before{background:none;border:none}.vuecal--date-picker .vuecal__cell-content{height:26px;flex-grow:0;border:1px solid transparent;border-radius:25px;transition:background-color .2s cubic-bezier(.39,.58,.57,1)}.vuecal--date-picker.vuecal--years-view .vuecal__cell-content{flex:0;padding:0 4px;height:24px}.vuecal--date-picker.vuecal--year-view .vuecal__cell-content{flex:0;padding:0 15px}.vuecal--date-picker.vuecal--month-view .vuecal__cell-content{width:26px}.vuecal--date-picker:not(.vuecal--day-view) .vuecal__cell-content:hover{background-color:#0000001a}.vuecal--date-picker:not(.vuecal--day-view) .vuecal__cell--selected .vuecal__cell-content{background-color:#42b982;color:#fff}.vuecal--date-picker:not(.vuecal--day-view) .vuecal__cell--current .vuecal__cell-content,.vuecal--date-picker:not(.vuecal--day-view) .vuecal__cell--today .vuecal__cell-content{border-color:#42b982} diff --git a/public/admin/build/assets/app-oZri8xg_.css b/public/admin/build/assets/app-oZri8xg_.css new file mode 100644 index 0000000..a067001 --- /dev/null +++ b/public/admin/build/assets/app-oZri8xg_.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:16px;padding-left:16px}@media(min-width:1920px){.container{max-width:1920px}}.icon-image:before{content:""}.icon-bookmark:before{content:""}.icon-bookmark-active:before{content:""}.icon-error:before{content:""}.icon-info:before{content:""}.icon-success:before{content:""}.icon-warning:before{content:""}.icon-stats-down:before{content:""}.icon-stats-up:before{content:""}.icon-attribute:before{content:""}.icon-download:before{content:""}.icon-settings-warehouse:before{content:""}.icon-move:before{content:""}.icon-organization:before{content:""}.icon-role:before{content:""}.icon-user:before{content:""}.icon-system-generate:before{content:""}.icon-settings-attributes:before{content:""}.icon-settings-flow:before{content:""}.icon-settings-group:before{content:""}.icon-settings-mail:before{content:""}.icon-settings-pipeline:before{content:""}.icon-settings-roles:before{content:""}.icon-settings-sources:before{content:""}.icon-settings-tag:before{content:""}.icon-settings-type:before{content:""}.icon-settings-user:before{content:""}.icon-settings-webforms:before{content:""}.icon-settings-webhooks:before{content:""}.icon-attached-file:before{content:""}.icon-forward:before{content:""}.icon-location:before{content:""}.icon-pin:before{content:""}.icon-print:before{content:""}.icon-reply-all:before{content:""}.icon-reply:before{content:""}.icon-rotten:before{content:""}.icon-tag:before{content:""}.icon-list:before{content:""}.icon-enter:before{content:""}.icon-kanban:before{content:""}.icon-tick:before{content:""}.icon-eye-hide:before{content:""}.icon-percentage:before{content:""}.icon-dollar:before{content:""}.icon-radio-selected:before{content:""}.icon-radio-normal:before{content:""}.icon-folder:before{content:""}.icon-file:before{content:""}.icon-eye:before{content:""}.icon-notification:before{content:""}.icon-configuration:before{content:""}.icon-note:before{content:""}.icon-edit:before{content:""}.icon-menu:before{content:""}.icon-calendar:before{content:""}.icon-delete:before{content:""}.icon-more:before{content:""}.icon-checkbox-multiple:before{content:""}.icon-checkbox-select:before{content:""}.icon-checkbox-outline:before{content:""}.icon-message:before{content:""}.icon-video:before{content:""}.icon-attachment:before{content:""}.icon-sent:before{content:""}.icon-call:before{content:""}.icon-meeting:before{content:""}.icon-light:before{content:""}.icon-dark:before{content:""}.icon-mail:before{content:""}.icon-leads:before{content:""}.icon-filter:before{content:""}.icon-setting:before{content:""}.icon-product:before{content:""}.icon-contact:before{content:""}.icon-activity:before{content:""}.icon-perosnal:before{content:""}.icon-quote:before{content:""}.icon-dashboard:before{content:""}.icon-cross-large:before{content:""}.icon-left-arrow:before{content:""}.icon-right-arrow:before{content:""}.icon-up-arrow:before{content:""}.icon-down-arrow:before{content:""}.icon-search:before{content:""}.icon-add:before{content:""}.icon-add-2:before{content:""}.label-active{max-width:-moz-max-content;max-width:max-content;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(167 243 208 / var(--tw-bg-opacity, 1));padding:.375rem .75rem;font-size:.75rem;line-height:1rem;font-weight:500;--tw-text-opacity: 1;color:rgb(6 78 59 / var(--tw-text-opacity, 1))}.label-inactive{max-width:-moz-max-content;max-width:max-content;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(254 202 202 / var(--tw-bg-opacity, 1));padding:.375rem .75rem;font-size:.75rem;line-height:1rem;font-weight:500;--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}[dir=ltr] .sidebar-rounded:after{pointer-events:none;position:absolute;top:0;right:-30px;height:30px;width:30px;background-image:url(corner-clip-DsfWoKt7.svg);background-repeat:no-repeat;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;--tw-content: "";content:var(--tw-content)}[dir=rtl] .sidebar-rounded:before{pointer-events:none;position:absolute;top:0;left:-30px;height:30px;width:30px;--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));background-image:url(corner-clip-DsfWoKt7.svg);background-repeat:no-repeat;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;--tw-content: "";content:var(--tw-content)}.dark .sidebar-rounded:after{background-image:url(dark-corner-clip-Deux0RQO.svg)}.dark .sidebar-rounded:before{background-image:url(dark-corner-clip-Deux0RQO.svg)}.stage:after{content:"";position:absolute;top:50%;right:-10px;width:24px;height:24px;z-index:1;border-radius:0 0 0 25px;transform:translateY(-50%) rotate(45deg);border-right:4px solid #f3f4f6;border-top:4px solid #f3f4f6}.dark .stage:after{content:"";position:absolute;top:50%;right:-10px;width:24px;height:24px;z-index:1;border-radius:0 0 0 25px;transform:translateY(-50%) rotate(45deg);border-right:4px solid #030712;border-top:4px solid #030712}[dir=rtl] .stage:before{content:"";position:absolute;top:50%;left:-10px;width:24px;height:24px;z-index:1;border-radius:0 0 0 25px;transform:translateY(-50%) rotate(225deg);border-right:4px solid #f3f4f6;border-top:4px solid #f3f4f6}[dir=rtl] .stage:after{display:none}.dark\:bg-gray-400:is(.dark *){--tw-bg-opacity: 1 !important;background-color:#1f2937!important;border:#111827 solid!important;color:#e5e7eb!important}.dark\:bg-gray-800:is(.dark *){--tw-bg-opacity: 1;background-color:#101827}.shimmer{animation-duration:2.2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:skeleton;animation-timing-function:linear;background:linear-gradient(to right,#f6f6f6 8%,#f0f0f0 18%,#f6f6f6 33%);background-size:1250px 100%}.light-shimmer-bg{background:linear-gradient(to right,#fafafa 8%,#f5f5f5 18%,#fafafa 33%);background-size:1250px 100%}.dark .shimmer{background:linear-gradient(to right,#1f2937 8%,#1a2232 18%,#1f2937 33%)}.primary-button{display:flex;cursor:pointer;place-content:center;align-items:center;-moz-column-gap:.25rem;column-gap:.25rem;border-radius:.375rem;border-width:1px;border-color:var(--brand-color);background-color:var(--brand-color);padding:.375rem .75rem;font-weight:600;--tw-text-opacity: 1;color:rgb(249 250 251 / var(--tw-text-opacity, 1));transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.primary-button:hover{opacity:.9}.primary-button:focus{opacity:.9}.secondary-button{display:flex;cursor:pointer;place-content:center;align-items:center;-moz-column-gap:.25rem;column-gap:.25rem;white-space:nowrap;border-radius:.375rem;border-width:2px;border-color:var(--brand-color);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:.375rem .75rem;font-weight:600;color:var(--brand-color);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.secondary-button:hover{background-color:#eff6ff61}.secondary-button:focus{background-color:#eff6ff61}.secondary-button:is(.dark *){--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.secondary-button:hover:is(.dark *){opacity:.8}.transparent-button{display:flex;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;place-content:center;align-items:center;-moz-column-gap:.25rem;column-gap:.25rem;white-space:nowrap;border-radius:.375rem;border-width:2px;border-color:transparent;padding:.375rem .75rem;font-weight:600;--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1));transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transparent-button *::marker{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.transparent-button::marker{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.transparent-button:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.transparent-button:focus{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.transparent-button:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.transparent-button:focus:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar{width:12px}::-webkit-scrollbar-track{background:#f1f1f1}::-webkit-scrollbar-thumb{background:#888;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#555}*{scrollbar-width:thin;scrollbar-color:#888 #f1f1f1}::-moz-selection{background-color:#0044f233}::selection{background-color:#0044f233}body{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1));font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}button:disabled{cursor:not-allowed;opacity:.5}button:disabled:hover{cursor:not-allowed;opacity:.5}.draggable-ghost{opacity:.5;background:#e0e7ff}html.dark [class^=icon-],html.dark [class*=" icon-"]{color:#d1d5db}p{font-size:14px;line-height:17px!important}input,textarea,select{outline:2px solid transparent;outline-offset:2px}.journal-scroll::-webkit-scrollbar{width:14px;cursor:pointer;display:none}.journal-scroll::-webkit-scrollbar-track{background-color:#fff;cursor:pointer;border-radius:12px;border:1px solid #e9e9e9}.journal-scroll::-webkit-scrollbar-thumb{cursor:pointer;background-color:#e9e9e9;border-radius:12px;border:3px solid transparent;background-clip:content-box}.custom-select{-webkit-appearance:none;-moz-appearance:none;background:transparent;background-image:url("data:image/svg+xml;utf8,");background-repeat:no-repeat;background-position-x:calc(100% - 10px);background-position-y:50%}.dark .custom-select{background-image:url("data:image/svg+xml;utf8,")}[dir=rtl] .custom-select{background-position-x:calc(100% - (100% - 10px))}.draggable-ghost{border-width:1px;border-style:dashed;--tw-border-opacity: 1 !important;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))!important}@keyframes skeleton{0%{background-position:-1250px 0}to{background-position:1250px 0}}.required:after{--tw-content: "*";content:var(--tw-content)}body.is-scrolling-down .scroll-reactive-sticky{width:calc(100% + 2rem);margin-left:-1rem;border-radius:0;padding-left:2rem;padding-right:2rem;border-right:none!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.\!bottom-12{bottom:3rem!important}.\!top-7{top:1.75rem!important}.\!top-auto{top:auto!important}.-bottom-0\.5{bottom:-.125rem}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-0{bottom:0}.bottom-10{bottom:2.5rem}.bottom-4{bottom:1rem}.bottom-5{bottom:1.25rem}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.left-\[6px\]{left:6px}.right-0{right:0}.right-1{right:.25rem}.right-1\.5{right:.375rem}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.right-7{right:1.75rem}.right-8{right:2rem}.right-\[6px\]{right:6px}.top-0{top:0}.top-1\.5{top:.375rem}.top-1\/2{top:50%}.top-10{top:2.5rem}.top-11{top:2.75rem}.top-2{top:.5rem}.top-3{top:.75rem}.top-4{top:1rem}.top-5{top:1.25rem}.top-\[60px\]{top:60px}.top-\[6px\]{top:6px}.top-\[9px\]{top:9px}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-\[10001\]{z-index:10001}.z-\[10002\]{z-index:10002}.z-\[10003\]{z-index:10003}.z-\[10004\]{z-index:10004}.z-\[1000\]{z-index:1000}.z-\[999\]{z-index:999}.float-right{float:right}.m-2{margin:.5rem}.m-3{margin:.75rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-2\.5{margin-top:.625rem;margin-bottom:.625rem}.my-4{margin-top:1rem;margin-bottom:1rem}.\!mb-0{margin-bottom:0!important}.\!mb-2{margin-bottom:.5rem!important}.\!mb-4{margin-bottom:1rem!important}.\!mt-6{margin-top:1.5rem!important}.-mb-px{margin-bottom:-1px}.-mt-2{margin-top:-.5rem}.-mt-px{margin-top:-1px}.mb-0{margin-bottom:0}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-2\.5{margin-bottom:.625rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-7{margin-bottom:1.75rem}.ml-1{margin-left:.25rem}.ml-4{margin-left:1rem}.mr-1{margin-right:.25rem}.ms-2{margin-inline-start:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-3\.5{margin-top:.875rem}.mt-4{margin-top:1rem}.mt-auto{margin-top:auto}.line-clamp-1{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.\!flex{display:flex!important}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-\[3\.23\/1\]{aspect-ratio:3.23/1}.\!h-10{height:2.5rem!important}.\!h-\[34px\]{height:34px!important}.h-0{height:0px}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-56{height:14rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[100vh\]{height:100vh}.h-\[120px\]{height:120px}.h-\[15px\]{height:15px}.h-\[17px\]{height:17px}.h-\[18px\]{height:18px}.h-\[19px\]{height:19px}.h-\[21px\]{height:21px}.h-\[223px\]{height:223px}.h-\[26px\]{height:26px}.h-\[271px\]{height:271px}.h-\[280px\]{height:280px}.h-\[28px\]{height:28px}.h-\[33px\]{height:33px}.h-\[34px\]{height:34px}.h-\[380px\]{height:380px}.h-\[38px\]{height:38px}.h-\[39px\]{height:39px}.h-\[40px\]{height:40px}.h-\[42px\]{height:42px}.h-\[44px\]{height:44px}.h-\[49px\]{height:49px}.h-\[59px\]{height:59px}.h-\[74px\]{height:74px}.h-\[calc\(100vh-100px\)\]{height:calc(100vh - 100px)}.h-\[calc\(100vh-317px\)\]{height:calc(100vh - 317px)}.h-fit{height:-moz-fit-content;height:fit-content}.h-full{height:100%}.max-h-0{max-height:0px}.max-h-40{max-height:10rem}.max-h-80{max-height:20rem}.max-h-9{max-height:2.25rem}.max-h-\[120px\]{max-height:120px}.max-h-\[200px\]{max-height:200px}.max-h-\[400px\]{max-height:400px}.max-h-\[500px\]{max-height:500px}.max-h-\[60vh\]{max-height:60vh}.max-h-\[80vh\]{max-height:80vh}.max-h-\[calc\(100vh-300px\)\]{max-height:calc(100vh - 300px)}.min-h-0{min-height:0px}.min-h-9{min-height:2.25rem}.min-h-\[110px\]{min-height:110px}.min-h-\[38px\]{min-height:38px}.min-h-\[39px\]{min-height:39px}.min-h-\[400px\]{min-height:400px}.min-h-\[47px\]{min-height:47px}.min-h-\[calc\(100vh-62px\)\]{min-height:calc(100vh - 62px)}.min-h-full{min-height:100%}.\!w-1\/2{width:50%!important}.\!w-1\/3{width:33.333333%!important}.\!w-1\/6{width:16.666667%!important}.\!w-12{width:3rem!important}.\!w-24{width:6rem!important}.\!w-56{width:14rem!important}.\!w-\[140px\]{width:140px!important}.\!w-\[150px\]{width:150px!important}.\!w-full{width:100%!important}.w-0{width:0px}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-1\/5{width:20%}.w-10{width:2.5rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-\[100px\]{width:100px}.w-\[101px\]{width:101px}.w-\[105px\]{width:105px}.w-\[110px\]{width:110px}.w-\[120px\]{width:120px}.w-\[125px\]{width:125px}.w-\[136px\]{width:136px}.w-\[140px\]{width:140px}.w-\[141px\]{width:141px}.w-\[150px\]{width:150px}.w-\[160px\]{width:160px}.w-\[16rem\]{width:16rem}.w-\[172px\]{width:172px}.w-\[200px\]{width:200px}.w-\[210px\]{width:210px}.w-\[250px\]{width:250px}.w-\[254px\]{width:254px}.w-\[264px\]{width:264px}.w-\[26px\]{width:26px}.w-\[280px\]{width:280px}.w-\[28px\]{width:28px}.w-\[33px\]{width:33px}.w-\[348px\]{width:348px}.w-\[350px\]{width:350px}.w-\[360px\]{width:360px}.w-\[378px\]{width:378px}.w-\[38px\]{width:38px}.w-\[39px\]{width:39px}.w-\[50px\]{width:50px}.w-\[525px\]{width:525px}.w-\[52px\]{width:52px}.w-\[550px\]{width:550px}.w-\[60px\]{width:60px}.w-\[62px\]{width:62px}.w-\[650px\]{width:650px}.w-\[66px\]{width:66px}.w-\[68px\]{width:68px}.w-\[70px\]{width:70px}.w-\[72px\]{width:72px}.w-\[75px\]{width:75px}.w-\[76px\]{width:76px}.w-\[80px\]{width:80px}.w-\[84px\]{width:84px}.w-\[926px\]{width:926px}.w-\[94px\]{width:94px}.w-\[calc\(100\%-62px\)\]{width:calc(100% - 62px)}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.\!min-w-40{min-width:10rem!important}.\!min-w-\[600px\]{min-width:600px!important}.min-w-0{min-width:0px}.min-w-24{min-width:6rem}.min-w-9{min-width:2.25rem}.min-w-\[110px\]{min-width:110px}.min-w-\[120px\]{min-width:120px}.min-w-\[140px\]{min-width:140px}.min-w-\[177px\]{min-width:177px}.min-w-\[210px\]{min-width:210px}.min-w-\[275px\]{min-width:275px}.min-w-\[300px\]{min-width:300px}.min-w-\[394px\]{min-width:394px}.min-w-\[800px\]{min-width:800px}.min-w-\[80px\]{min-width:80px}.max-w-3xl{max-width:48rem}.max-w-9{max-width:2.25rem}.max-w-\[110px\]{max-width:110px}.max-w-\[120px\]{max-width:120px}.max-w-\[200px\]{max-width:200px}.max-w-\[210px\]{max-width:210px}.max-w-\[250px\]{max-width:250px}.max-w-\[275px\]{max-width:275px}.max-w-\[360px\]{max-width:360px}.max-w-\[394px\]{max-width:394px}.max-w-\[400px\]{max-width:400px}.max-w-\[445px\]{max-width:445px}.max-w-\[525px\]{max-width:525px}.max-w-\[550px\]{max-width:550px}.max-w-\[70px\]{max-width:70px}.max-w-\[745px\]{max-width:745px}.max-w-\[768px\]{max-width:768px}.max-w-\[950px\]{max-width:950px}.max-w-full{max-width:100%}.max-w-max{max-width:-moz-max-content;max-width:max-content}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.\!table-auto{table-layout:auto!important}.table-fixed{table-layout:fixed}.border-collapse{border-collapse:collapse}.origin-top{transform-origin:top}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2,.-translate-y-2\/4{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-4{--tw-translate-y: -1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-full{--tw-translate-y: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-4{--tw-translate-y: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-45{--tw-rotate: 45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.animate-\[on-fade_0\.5s_ease-in-out\]{animation:on-fade .5s ease-in-out}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-grab{cursor:grab}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.resize{resize:both}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-\[\.1fr_\.2fr_\.2fr_\.2fr_\.2fr_\.2fr\]{grid-template-columns:.1fr .2fr .2fr .2fr .2fr .2fr}.grid-cols-\[\.3fr_\.1fr_\.3fr_\.5fr\]{grid-template-columns:.3fr .1fr .3fr .5fr}.grid-cols-\[1fr_2fr\]{grid-template-columns:1fr 2fr}.grid-cols-\[2fr_1fr_1fr\]{grid-template-columns:2fr 1fr 1fr}.grid-cols-\[2fr_7fr_\.0\.3fr\]{grid-template-columns:2fr 7fr 0.3fr}.grid-cols-\[2fr_7fr_\.1fr\]{grid-template-columns:2fr 7fr .1fr}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.place-content-start{place-content:start}.place-content-between{place-content:space-between}.place-items-start{place-items:start}.content-start{align-content:flex-start}.content-end{align-content:flex-end}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-end{justify-items:end}.justify-items-center{justify-items:center}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-10{gap:2.5rem}.gap-11{gap:2.75rem}.gap-16{gap:4rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-3\.5{gap:.875rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-\[100px\]{gap:100px}.gap-\[124px\]{gap:124px}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.gap-x-1\.5{-moz-column-gap:.375rem;column-gap:.375rem}.gap-x-12{-moz-column-gap:3rem;column-gap:3rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-2\.5{-moz-column-gap:.625rem;column-gap:.625rem}.gap-x-3\.5{-moz-column-gap:.875rem;column-gap:.875rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-5{-moz-column-gap:1.25rem;column-gap:1.25rem}.gap-y-2\.5{row-gap:.625rem}.gap-y-6{row-gap:1.5rem}.gap-y-8{row-gap:2rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(243 244 246 / var(--tw-divide-opacity, 1))}.divide-slate-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(226 232 240 / var(--tw-divide-opacity, 1))}.place-self-start{place-self:start}.place-self-end{place-self:end}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.text-wrap{text-wrap:wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.\!rounded-lg{border-radius:.5rem!important}.\!rounded-none{border-radius:0!important}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.\!rounded-l-none{border-top-left-radius:0!important;border-bottom-left-radius:0!important}.\!rounded-r-none{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-x-0{border-left-width:0px;border-right-width:0px}.border-y{border-top-width:1px;border-bottom-width:1px}.\!border-b-0{border-bottom-width:0px!important}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-t-0{border-top-width:0px}.border-t-2{border-top-width:2px}.border-solid{border-style:solid}.border-dashed{border-style:dashed}.\!border-none{border-style:none!important}.\!border-red-600{--tw-border-opacity: 1 !important;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))!important}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity, 1))}.border-brandColor{border-color:var(--brand-color)}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-gray-400{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.border-gray-500{--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity, 1))}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-orange-200{--tw-border-opacity: 1;border-color:rgb(254 215 170 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-b-gray-300{--tw-border-opacity: 1;border-bottom-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-l-brandColor{border-left-color:var(--brand-color)}.\!bg-green-500{--tw-bg-opacity: 1 !important;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))!important}.\!bg-red-500{--tw-bg-opacity: 1 !important;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))!important}.bg-\[\#63CFE5\]{--tw-bg-opacity: 1;background-color:rgb(99 207 229 / var(--tw-bg-opacity, 1))}.bg-\[\#8979FF\]{--tw-bg-opacity: 1;background-color:rgb(137 121 255 / var(--tw-bg-opacity, 1))}.bg-\[\#FFA8A1\]{--tw-bg-opacity: 1;background-color:rgb(255 168 161 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-brandColor{background-color:var(--brand-color)}.bg-cyan-200{--tw-bg-opacity: 1;background-color:rgb(165 243 252 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-200{--tw-bg-opacity: 1;background-color:rgb(187 247 208 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-lime-200{--tw-bg-opacity: 1;background-color:rgb(217 249 157 / var(--tw-bg-opacity, 1))}.bg-neutral-100{--tw-bg-opacity: 1;background-color:rgb(245 245 245 / var(--tw-bg-opacity, 1))}.bg-orange-100{--tw-bg-opacity: 1;background-color:rgb(255 237 213 / var(--tw-bg-opacity, 1))}.bg-orange-200{--tw-bg-opacity: 1;background-color:rgb(254 215 170 / var(--tw-bg-opacity, 1))}.bg-orange-50{--tw-bg-opacity: 1;background-color:rgb(255 247 237 / var(--tw-bg-opacity, 1))}.bg-pink-200{--tw-bg-opacity: 1;background-color:rgb(251 207 232 / var(--tw-bg-opacity, 1))}.bg-pink-400{--tw-bg-opacity: 1;background-color:rgb(244 114 182 / var(--tw-bg-opacity, 1))}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-200{--tw-bg-opacity: 1;background-color:rgb(254 202 202 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-rose-100{--tw-bg-opacity: 1;background-color:rgb(255 228 230 / var(--tw-bg-opacity, 1))}.bg-sky-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-sky-600{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-violet-50{--tw-bg-opacity: 1;background-color:rgb(245 243 255 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/20{background-color:#fff3}.bg-yellow-200{--tw-bg-opacity: 1;background-color:rgb(254 240 138 / var(--tw-bg-opacity, 1))}.bg-yellow-400{--tw-bg-opacity: 1;background-color:rgb(250 204 21 / var(--tw-bg-opacity, 1))}.bg-opacity-50{--tw-bg-opacity: .5}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-transparent{--tw-gradient-from: transparent var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-gray-50{--tw-gradient-to: rgb(249 250 251 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #f9fafb var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-gray-100{--tw-gradient-to: #f3f4f6 var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.\!p-0{padding:0!important}.\!p-2{padding:.5rem!important}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-3\.5{padding:.875rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.\!px-0{padding-left:0!important;padding-right:0!important}.\!px-2{padding-left:.5rem!important;padding-right:.5rem!important}.\!px-4{padding-left:1rem!important;padding-right:1rem!important}.\!py-0{padding-top:0!important;padding-bottom:0!important}.\!py-\[22px\]{padding-top:22px!important;padding-bottom:22px!important}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-\[11px\]{padding-top:11px;padding-bottom:11px}.py-\[7px\]{padding-top:7px;padding-bottom:7px}.py-\[9px\]{padding-top:9px;padding-bottom:9px}.py-px{padding-top:1px;padding-bottom:1px}.\!pb-0{padding-bottom:0!important}.\!pb-3{padding-bottom:.75rem!important}.pb-2\.5{padding-bottom:.625rem}.pb-3\.5{padding-bottom:.875rem}.pb-4{padding-bottom:1rem}.pb-8{padding-bottom:2rem}.pb-\[72px\]{padding-bottom:72px}.pb-\[9px\]{padding-bottom:9px}.pl-10{padding-left:2.5rem}.pl-2{padding-left:.5rem}.pl-2\.5{padding-left:.625rem}.pl-4{padding-left:1rem}.pl-7{padding-left:1.75rem}.pl-8{padding-left:2rem}.pl-\[2px\]{padding-left:2px}.pr-10{padding-right:2.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pr-6{padding-right:1.5rem}.pt-2{padding-top:.5rem}.pt-2\.5{padding-top:.625rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-end{text-align:end}.align-top{vertical-align:top}.align-text-bottom{vertical-align:text-bottom}.font-inter{font-family:Inter}.\!text-base{font-size:1rem!important;line-height:1.5rem!important}.\!text-sm{font-size:.875rem!important;line-height:1.25rem!important}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[26px\]{font-size:26px}.text-\[38px\]{font-size:38px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.\!font-bold{font-weight:700!important}.\!font-normal{font-weight:400!important}.\!font-semibold{font-weight:600!important}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-8{line-height:2rem}.leading-\[140\%\]{line-height:140%}.leading-none{line-height:1}.leading-normal{line-height:1.5}.leading-snug{line-height:1.375}.tracking-wider{letter-spacing:.05em}.\!text-brandColor{color:var(--brand-color)!important}.\!text-gray-500{--tw-text-opacity: 1 !important;color:rgb(107 114 128 / var(--tw-text-opacity, 1))!important}.\!text-gray-600{--tw-text-opacity: 1 !important;color:rgb(75 85 99 / var(--tw-text-opacity, 1))!important}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.text-brandColor{color:var(--brand-color)}.text-cyan-800{--tw-text-opacity: 1;color:rgb(21 94 117 / var(--tw-text-opacity, 1))}.text-cyan-900{--tw-text-opacity: 1;color:rgb(22 78 99 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-green-900{--tw-text-opacity: 1;color:rgb(20 83 45 / var(--tw-text-opacity, 1))}.text-lime-900{--tw-text-opacity: 1;color:rgb(54 83 20 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-orange-800{--tw-text-opacity: 1;color:rgb(154 52 18 / var(--tw-text-opacity, 1))}.text-orange-900{--tw-text-opacity: 1;color:rgb(124 45 18 / var(--tw-text-opacity, 1))}.text-pink-900{--tw-text-opacity: 1;color:rgb(131 24 67 / var(--tw-text-opacity, 1))}.text-purple-800{--tw-text-opacity: 1;color:rgb(107 33 168 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity, 1))}.text-rose-600{--tw-text-opacity: 1;color:rgb(225 29 72 / var(--tw-text-opacity, 1))}.text-sky-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-900{--tw-text-opacity: 1;color:rgb(113 63 18 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.\!shadow-none{--tw-shadow: 0 0 #0000 !important;--tw-shadow-colored: 0 0 #0000 !important;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)!important}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_5px_20px_rgba\(0\,0\,0\,0\.15\)\]{--tw-shadow: 0 5px 20px rgba(0,0,0,.15);--tw-shadow-colored: 0 5px 20px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0px_0px_0px_0px_rgba\(0\,0\,0\,0\.10\)\,0px_1px_3px_0px_rgba\(0\,0\,0\,0\.10\)\,0px_5px_5px_0px_rgba\(0\,0\,0\,0\.09\)\,0px_12px_7px_0px_rgba\(0\,0\,0\,0\.05\)\,0px_22px_9px_0px_rgba\(0\,0\,0\,0\.01\)\,0px_34px_9px_0px_rgba\(0\,0\,0\,0\.00\)\]{--tw-shadow: 0px 0px 0px 0px rgba(0,0,0,.1),0px 1px 3px 0px rgba(0,0,0,.1),0px 5px 5px 0px rgba(0,0,0,.09),0px 12px 7px 0px rgba(0,0,0,.05),0px 22px 9px 0px rgba(0,0,0,.01),0px 34px 9px 0px rgba(0,0,0,0);--tw-shadow-colored: 0px 0px 0px 0px var(--tw-shadow-color), 0px 1px 3px 0px var(--tw-shadow-color), 0px 5px 5px 0px var(--tw-shadow-color), 0px 12px 7px 0px var(--tw-shadow-color), 0px 22px 9px 0px var(--tw-shadow-color), 0px 34px 9px 0px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0px_10px_20px_0px_\#0000001F\]{--tw-shadow: 0px 10px 20px 0px #0000001F;--tw-shadow-colored: 0px 10px 20px 0px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0px_10px_20px_0px_rgba\(0\,0\,0\,0\.12\)\]{--tw-shadow: 0px 10px 20px 0px rgba(0,0,0,.12);--tw-shadow-colored: 0px 10px 20px 0px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-slate-200{--tw-shadow-color: #e2e8f0;--tw-shadow: var(--tw-shadow-colored)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-\[cubic-bezier\(\.4\,0\,\.2\,1\)\]{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}@font-face{font-family:icomoon;src:url(icomoon-BbY1GX-d.woff?w2trdd) format("woff");font-weight:400;font-style:normal;font-display:block}[class^=icon-],[class*=" icon-"]{font-family:icomoon!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-help{display:inline-block;width:1em;height:1em;background-color:currentColor;-webkit-mask:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iIzAwMCI+PHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNOS44NzkgNy41MTljMS4xNzEtMS4wMjUgMy4wNzEtMS4wMjUgNC4yNDIgMCAxLjE3MiAxLjAyNSAxLjE3MiAyLjY4NyAwIDMuNzEyLS4yMDMuMTc5LS40My4zMjYtLjY3LjQ0Mi0uNzQ1LjM2MS0xLjQ1Ljk5OS0xLjQ1IDEuODI3di43NU0yMSAxMmE5IDkgMCAxIDEtMTggMCA5IDkgMCAwIDEgMTggMFptLTkgNS4yNWguMDA4di4wMDhIMTJ2LS4wMDhaIi8+PC9zdmc+) no-repeat center / contain;mask:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iIzAwMCI+PHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNOS44NzkgNy41MTljMS4xNzEtMS4wMjUgMy4wNzEtMS4wMjUgNC4yNDIgMCAxLjE3MiAxLjAyNSAxLjE3MiAyLjY4NyAwIDMuNzEyLS4yMDMuMTc5LS40My4zMjYtLjY3LjQ0Mi0uNzQ1LjM2MS0xLjQ1Ljk5OS0xLjQ1IDEuODI3di43NU0yMSAxMmE5IDkgMCAxIDEtMTggMCA5IDkgMCAwIDEgMTggMFptLTkgNS4yNWguMDA4di4wMDhIMTJ2LS4wMDhaIi8+PC9zdmc+) no-repeat center / contain}.tox .tox-toolbar__group:last-child button{padding:6px 8px;background:#eff6ff;color:#2563eb}.tox .tox-toolbar__group:last-child button:hover{background:#dbeafe}.tox .tox-toolbar__group:last-child button[aria-disabled=true]{cursor:not-allowed;opacity:.5}.tox .tox-toolbar__group:last-child button{background-color:transparent!important}.tox .tox-toolbar__group:last-child button:hover{border-width:0px;--tw-bg-opacity: 1 !important;background-color:rgb(51 137 236 / var(--tw-bg-opacity, 1))!important;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1));--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.tox.tox-silver-sink.tox-tinymce-aux{z-index:99999}.peer:checked~.peer-checked\:icon-radio-selected:before{content:""}.peer:checked~.peer-checked\:icon-checkbox-multiple:before{content:""}.peer:checked~.peer-checked\:icon-checkbox-select:before{content:""}.dark\:shimmer:is(.dark *){animation-duration:2.2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:skeleton;animation-timing-function:linear;background:linear-gradient(to right,#f6f6f6 8%,#f0f0f0 18%,#f6f6f6 33%);background-size:1250px 100%}.dark .dark\:shimmer:is(.dark *){background:linear-gradient(to right,#1f2937 8%,#1a2232 18%,#1f2937 33%)}.rtl\:icon-left-arrow:where([dir=rtl],[dir=rtl] *):before{content:""}.rtl\:icon-right-arrow:where([dir=rtl],[dir=rtl] *):before{content:""}.marker\:shadow *::marker{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.marker\:shadow::marker{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:-right-\[30px\]:after{content:var(--tw-content);right:-30px}.after\:top-0\.5:after{content:var(--tw-content);top:.125rem}.after\:h-4:after{content:var(--tw-content);height:1rem}.after\:w-4:after{content:var(--tw-content);width:1rem}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:border:after{content:var(--tw-content);border-width:1px}.after\:border-gray-300:after{content:var(--tw-content);--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.after\:bg-green-500:after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.after\:bg-red-500:after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content: "";content:var(--tw-content)}.after\:content-\[\'\,_\'\]:after{--tw-content: ", ";content:var(--tw-content)}.after\:content-\[\'\/\'\]:after{--tw-content: "/";content:var(--tw-content)}.first\:rounded-l-lg:first-child{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.last\:\!mb-0:last-child{margin-bottom:0!important}.last\:mb-0:last-child{margin-bottom:0}.last\:cursor-default:last-child{cursor:default}.last\:border-b-0:last-child{border-bottom-width:0px}.last\:border-none:last-child{border-style:none}.after\:last\:hidden:last-child:after{content:var(--tw-content);display:none}.last\:after\:content-\[\'\'\]:last-child:after{--tw-content: "";content:var(--tw-content)}.focus-within\:border-gray-400:focus-within{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.hover\:rounded-lg:hover{border-radius:.5rem}.hover\:rounded-md:hover{border-radius:.375rem}.hover\:rounded-sm:hover{border-radius:.125rem}.hover\:border-blue-400:hover{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity, 1))}.hover\:border-cyan-400:hover{--tw-border-opacity: 1;border-color:rgb(34 211 238 / var(--tw-border-opacity, 1))}.hover\:border-gray-400:hover{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.hover\:border-green-400:hover{--tw-border-opacity: 1;border-color:rgb(74 222 128 / var(--tw-border-opacity, 1))}.hover\:border-orange-400:hover{--tw-border-opacity: 1;border-color:rgb(251 146 60 / var(--tw-border-opacity, 1))}.hover\:border-red-600:hover{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))}.hover\:bg-blue-100:hover{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.hover\:bg-brandColor:hover{background-color:var(--brand-color)}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-green-200:hover{--tw-bg-opacity: 1;background-color:rgb(187 247 208 / var(--tw-bg-opacity, 1))}.hover\:bg-red-200:hover{--tw-bg-opacity: 1;background-color:rgb(254 202 202 / var(--tw-bg-opacity, 1))}.hover\:text-brandColor:hover{color:var(--brand-color)}.hover\:text-gray-800:hover{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.hover\:text-sky-600:hover{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-90:hover{opacity:.9}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-gray-400:focus{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.focus\:opacity-80:focus{opacity:.8}.focus\:ring-black:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity, 1))}.active\:border-gray-300:active{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.group\/item:hover .group-hover\/item\:visible,.group:hover .group-hover\:visible{visibility:visible}.group:hover .group-hover\:flex{display:flex}.group:hover .group-hover\:text-brandColor{color:var(--brand-color)}.group:hover .group-hover\:text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:opacity-100{opacity:1}.group\/container.sidebar-collapsed .group-\[\.sidebar-collapsed\]\/container\:hidden{display:none}.group\/container.sidebar-collapsed .group-\[\.sidebar-collapsed\]\/container\:w-\[70px\]{width:70px}.group\/container.sidebar-collapsed .group-\[\.sidebar-collapsed\]\/container\:overflow-visible{overflow:visible}.peer:checked~.peer-checked\:border-2{border-width:2px}.peer:checked~.peer-checked\:border-4{border-width:4px}.peer:checked~.peer-checked\:border-solid{border-style:solid}.peer:checked~.peer-checked\:border-brandColor{border-color:var(--brand-color)}.peer:checked~.peer-checked\:border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.peer:checked~.peer-checked\:bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.peer:checked~.peer-checked\:bg-brandColor{background-color:var(--brand-color)}.peer:checked~.peer-checked\:\!text-brandColor{color:var(--brand-color)!important}.peer:checked~.peer-checked\:text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.peer:checked~.peer-checked\:text-brandColor{color:var(--brand-color)}.peer:checked~.peer-checked\:after\:border-white:after{content:var(--tw-content);--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.peer:focus~.peer-focus\:outline-none{outline:2px solid transparent;outline-offset:2px}.peer:focus~.peer-focus\:ring-blue-300{--tw-ring-opacity: 1;--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity, 1))}.dark\:divide-gray-700:is(.dark *)>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(55 65 81 / var(--tw-divide-opacity, 1))}.dark\:divide-gray-800:is(.dark *)>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(31 41 55 / var(--tw-divide-opacity, 1))}.dark\:border-gray-300:is(.dark *){--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.dark\:border-gray-400:is(.dark *){--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.dark\:border-gray-600:is(.dark *){--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.dark\:border-gray-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.dark\:border-gray-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.dark\:border-gray-900:is(.dark *){--tw-border-opacity: 1;border-color:rgb(17 24 39 / var(--tw-border-opacity, 1))}.dark\:\!bg-gray-900:is(.dark *){--tw-bg-opacity: 1 !important;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))!important}.dark\:bg-brandColor:is(.dark *){background-color:var(--brand-color)}.dark\:bg-gray-400:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.dark\:bg-green-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity, 1))}.dark\:bg-white:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.dark\:via-gray-900:is(.dark *){--tw-gradient-to: rgb(17 24 39 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #111827 var(--tw-gradient-via-position), var(--tw-gradient-to)}.dark\:to-gray-900:is(.dark *){--tw-gradient-to: #111827 var(--tw-gradient-to-position)}.dark\:\!text-blue-600:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(37 99 235 / var(--tw-text-opacity, 1))!important}.dark\:\!text-blue-800:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(30 64 175 / var(--tw-text-opacity, 1))!important}.dark\:\!text-cyan-800:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(21 94 117 / var(--tw-text-opacity, 1))!important}.dark\:\!text-cyan-900:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(22 78 99 / var(--tw-text-opacity, 1))!important}.dark\:\!text-gray-300:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(209 213 219 / var(--tw-text-opacity, 1))!important}.dark\:\!text-green-500:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(34 197 94 / var(--tw-text-opacity, 1))!important}.dark\:\!text-green-600:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(22 163 74 / var(--tw-text-opacity, 1))!important}.dark\:\!text-green-900:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(20 83 45 / var(--tw-text-opacity, 1))!important}.dark\:\!text-orange-600:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(234 88 12 / var(--tw-text-opacity, 1))!important}.dark\:\!text-orange-800:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(154 52 18 / var(--tw-text-opacity, 1))!important}.dark\:\!text-red-500:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(239 68 68 / var(--tw-text-opacity, 1))!important}.dark\:\!text-red-600:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(220 38 38 / var(--tw-text-opacity, 1))!important}.dark\:\!text-yellow-900:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(113 63 18 / var(--tw-text-opacity, 1))!important}.dark\:text-brandColor:is(.dark *){color:var(--brand-color)}.dark\:text-gray-100:is(.dark *){--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.dark\:text-gray-300:is(.dark *){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-gray-400:is(.dark *){--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.dark\:text-gray-600:is(.dark *){--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.dark\:text-gray-800:is(.dark *){--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.dark\:text-gray-900:is(.dark *){--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.dark\:text-white:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:mix-blend-exclusion:is(.dark *){mix-blend-mode:exclusion}.dark\:ring-offset-gray-800:is(.dark *){--tw-ring-offset-color: #1f2937}.dark\:invert:is(.dark *){--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.dark\:file\:bg-gray-800:is(.dark *)::file-selector-button{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:file\:dark\:text-white:is(.dark *):is(.dark *)::file-selector-button{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:after\:border-white:is(.dark *):after{content:var(--tw-content);--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.dark\:after\:bg-white:is(.dark *):after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.dark\:hover\:border-gray-400:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-gray-800:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-900:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-950:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.hover\:dark\:bg-gray-950:is(.dark *):hover{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.dark\:hover\:text-white:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:focus\:border-gray-400:focus:is(.dark *){--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.dark\:focus\:bg-gray-800:focus:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.peer:checked~.dark\:peer-checked\:bg-gray-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}@media not all and (min-width:1240px){.max-xl\:w-full{width:100%}.max-xl\:flex-auto{flex:1 1 auto}.max-xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.max-xl\:flex-wrap{flex-wrap:wrap}.max-xl\:flex-wrap-reverse{flex-wrap:wrap-reverse}}@media not all and (min-width:1024px){.max-lg\:mt-6{margin-top:1.5rem}.max-lg\:block{display:block}.max-lg\:hidden{display:none}.max-lg\:w-\[400px\]{width:400px}.max-lg\:min-w-full{min-width:100%}.max-lg\:max-w-full{max-width:100%}.max-lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.max-lg\:flex-wrap{flex-wrap:wrap}.max-lg\:gap-4{gap:1rem}.max-lg\:gap-y-4{row-gap:1rem}}@media not all and (min-width:768px){.max-md\:hidden{display:none}.max-md\:w-\[90\%\]{width:90%}.max-md\:w-full{width:100%}.max-md\:max-w-full{max-width:100%}.max-md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.max-md\:flex-wrap{flex-wrap:wrap}.max-md\:\!justify-between{justify-content:space-between!important}.max-md\:justify-between{justify-content:space-between}.max-md\:p-4{padding:1rem}.max-md\:py-3{padding-top:.75rem;padding-bottom:.75rem}}@media not all and (min-width:525px){.max-sm\:hidden{display:none}.max-sm\:\!w-\[calc\(100\%-24px\)\]{width:calc(100% - 24px)!important}.max-sm\:w-full{width:100%}.max-sm\:max-w-full{max-width:100%}.max-sm\:flex-1{flex:1 1 0%}.max-sm\:flex-auto{flex:1 1 auto}.max-sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.max-sm\:flex-wrap{flex-wrap:wrap}.max-sm\:gap-2\.5{gap:.625rem}.max-sm\:place-self-center{place-self:center}.max-sm\:px-4{padding-left:1rem;padding-right:1rem}}@media(min-width:525px){.sm\:absolute{position:absolute}.sm\:mt-0{margin-top:0}.sm\:hidden{display:none}.sm\:w-auto{width:auto}.sm\:min-w-\[12rem\]{min-width:12rem}.sm\:flex-1{flex:1 1 0%}.sm\:flex-row{flex-direction:row}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.sm\:rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.sm\:p-0{padding:0}.sm\:px-4{padding-left:1rem;padding-right:1rem}.sm\:py-3{padding-top:.75rem;padding-bottom:.75rem}.sm\:text-right{text-align:right}.sm\:text-base{font-size:1rem;line-height:1.5rem}}@media(min-width:768px){.md\:hidden{display:none}.md\:translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}}@media(min-width:1024px){.lg\:sticky{position:sticky}.lg\:top-\[73px\]{top:73px}.lg\:mt-6{margin-top:1.5rem}.lg\:hidden{display:none}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}.ltr\:left-0:where([dir=ltr],[dir=ltr] *){left:0}.ltr\:left-3:where([dir=ltr],[dir=ltr] *){left:.75rem}.ltr\:left-4:where([dir=ltr],[dir=ltr] *){left:1rem}.ltr\:left-\[200px\]:where([dir=ltr],[dir=ltr] *){left:200px}.ltr\:right-0:where([dir=ltr],[dir=ltr] *){right:0}.ltr\:right-1\.5:where([dir=ltr],[dir=ltr] *){right:.375rem}.ltr\:right-2:where([dir=ltr],[dir=ltr] *){right:.5rem}.ltr\:right-3:where([dir=ltr],[dir=ltr] *){right:.75rem}.ltr\:right-4:where([dir=ltr],[dir=ltr] *){right:1rem}.ltr\:ml-1\.5:where([dir=ltr],[dir=ltr] *){margin-left:.375rem}.ltr\:ml-11:where([dir=ltr],[dir=ltr] *){margin-left:2.75rem}.ltr\:ml-2\.5:where([dir=ltr],[dir=ltr] *){margin-left:.625rem}.ltr\:mr-1:where([dir=ltr],[dir=ltr] *){margin-right:.25rem}.ltr\:mr-11:where([dir=ltr],[dir=ltr] *){margin-right:2.75rem}.ltr\:mr-2:where([dir=ltr],[dir=ltr] *){margin-right:.5rem}.ltr\:mr-5:where([dir=ltr],[dir=ltr] *){margin-right:1.25rem}.ltr\:mr-6:where([dir=ltr],[dir=ltr] *){margin-right:1.5rem}.ltr\:-translate-x-full:where([dir=ltr],[dir=ltr] *){--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.ltr\:translate-x-full:where([dir=ltr],[dir=ltr] *){--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.ltr\:translate-y-0:where([dir=ltr],[dir=ltr] *){--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.ltr\:translate-y-full:where([dir=ltr],[dir=ltr] *){--tw-translate-y: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.ltr\:rotate-\[0\]:where([dir=ltr],[dir=ltr] *){--tw-rotate: 0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.ltr\:rotate-\[180deg\]:where([dir=ltr],[dir=ltr] *){--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.ltr\:rounded-l-md:where([dir=ltr],[dir=ltr] *){border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.ltr\:rounded-r-md:where([dir=ltr],[dir=ltr] *){border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.ltr\:border-r:where([dir=ltr],[dir=ltr] *){border-right-width:1px}.ltr\:\!pl-14:where([dir=ltr],[dir=ltr] *){padding-left:3.5rem!important}.ltr\:\!pl-5:where([dir=ltr],[dir=ltr] *){padding-left:1.25rem!important}.ltr\:pl-10:where([dir=ltr],[dir=ltr] *){padding-left:2.5rem}.ltr\:pl-2:where([dir=ltr],[dir=ltr] *){padding-left:.5rem}.ltr\:pl-2\.5:where([dir=ltr],[dir=ltr] *){padding-left:.625rem}.ltr\:pl-4:where([dir=ltr],[dir=ltr] *){padding-left:1rem}.ltr\:pr-1\.5:where([dir=ltr],[dir=ltr] *){padding-right:.375rem}.ltr\:pr-10:where([dir=ltr],[dir=ltr] *){padding-right:2.5rem}.ltr\:pr-14:where([dir=ltr],[dir=ltr] *){padding-right:3.5rem}.ltr\:pr-16:where([dir=ltr],[dir=ltr] *){padding-right:4rem}.ltr\:pr-20:where([dir=ltr],[dir=ltr] *){padding-right:5rem}.ltr\:pr-3:where([dir=ltr],[dir=ltr] *){padding-right:.75rem}.ltr\:pr-8:where([dir=ltr],[dir=ltr] *){padding-right:2rem}.ltr\:text-left:where([dir=ltr],[dir=ltr] *){text-align:left}.ltr\:text-right:where([dir=ltr],[dir=ltr] *){text-align:right}.after\:ltr\:left-0\.5:where([dir=ltr],[dir=ltr] *):after{content:var(--tw-content);left:.125rem}.ltr\:after\:bg-green-500:where([dir=ltr],[dir=ltr] *):after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.ltr\:after\:bg-red-500:where([dir=ltr],[dir=ltr] *):after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.ltr\:first\:rounded-l-lg:first-child:where([dir=ltr],[dir=ltr] *){border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.peer:checked~.peer-checked\:after\:ltr\:translate-x-full:where([dir=ltr],[dir=ltr] *):after{content:var(--tw-content);--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media(min-width:1024px){.lg\:ltr\:pl-\[215px\]:where([dir=ltr],[dir=ltr] *){padding-left:215px}.group\/container.sidebar-collapsed .lg\:group-\[\.sidebar-collapsed\]\/container\:ltr\:pl-\[85px\]:where([dir=ltr],[dir=ltr] *){padding-left:85px}}.rtl\:left-0:where([dir=rtl],[dir=rtl] *){left:0}.rtl\:left-1\.5:where([dir=rtl],[dir=rtl] *){left:.375rem}.rtl\:left-2:where([dir=rtl],[dir=rtl] *){left:.5rem}.rtl\:left-3:where([dir=rtl],[dir=rtl] *){left:.75rem}.rtl\:left-4:where([dir=rtl],[dir=rtl] *){left:1rem}.rtl\:right-0:where([dir=rtl],[dir=rtl] *){right:0}.rtl\:right-3:where([dir=rtl],[dir=rtl] *){right:.75rem}.rtl\:right-4:where([dir=rtl],[dir=rtl] *){right:1rem}.rtl\:right-\[199px\]:where([dir=rtl],[dir=rtl] *){right:199px}.rtl\:ml-1:where([dir=rtl],[dir=rtl] *){margin-left:.25rem}.rtl\:ml-11:where([dir=rtl],[dir=rtl] *){margin-left:2.75rem}.rtl\:ml-2:where([dir=rtl],[dir=rtl] *){margin-left:.5rem}.rtl\:ml-5:where([dir=rtl],[dir=rtl] *){margin-left:1.25rem}.rtl\:ml-6:where([dir=rtl],[dir=rtl] *){margin-left:1.5rem}.rtl\:mr-1\.5:where([dir=rtl],[dir=rtl] *){margin-right:.375rem}.rtl\:mr-11:where([dir=rtl],[dir=rtl] *){margin-right:2.75rem}.rtl\:mr-2\.5:where([dir=rtl],[dir=rtl] *){margin-right:.625rem}.rtl\:-translate-x-full:where([dir=rtl],[dir=rtl] *){--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:-translate-y-0:where([dir=rtl],[dir=rtl] *){--tw-translate-y: -0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:-translate-y-full:where([dir=rtl],[dir=rtl] *){--tw-translate-y: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:translate-x-full:where([dir=rtl],[dir=rtl] *){--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:rotate-\[0\]:where([dir=rtl],[dir=rtl] *){--tw-rotate: 0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:rotate-\[180deg\]:where([dir=rtl],[dir=rtl] *){--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:rounded-l-md:where([dir=rtl],[dir=rtl] *){border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.rtl\:rounded-r-md:where([dir=rtl],[dir=rtl] *){border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.rtl\:border-x:where([dir=rtl],[dir=rtl] *){border-left-width:1px;border-right-width:1px}.rtl\:border-l:where([dir=rtl],[dir=rtl] *){border-left-width:1px}.rtl\:\!pr-14:where([dir=rtl],[dir=rtl] *){padding-right:3.5rem!important}.rtl\:\!pr-5:where([dir=rtl],[dir=rtl] *){padding-right:1.25rem!important}.rtl\:pl-1\.5:where([dir=rtl],[dir=rtl] *){padding-left:.375rem}.rtl\:pl-10:where([dir=rtl],[dir=rtl] *){padding-left:2.5rem}.rtl\:pl-14:where([dir=rtl],[dir=rtl] *){padding-left:3.5rem}.rtl\:pl-16:where([dir=rtl],[dir=rtl] *){padding-left:4rem}.rtl\:pl-20:where([dir=rtl],[dir=rtl] *){padding-left:5rem}.rtl\:pl-3:where([dir=rtl],[dir=rtl] *){padding-left:.75rem}.rtl\:pl-8:where([dir=rtl],[dir=rtl] *){padding-left:2rem}.rtl\:pr-10:where([dir=rtl],[dir=rtl] *){padding-right:2.5rem}.rtl\:pr-2:where([dir=rtl],[dir=rtl] *){padding-right:.5rem}.rtl\:pr-2\.5:where([dir=rtl],[dir=rtl] *){padding-right:.625rem}.rtl\:pr-4:where([dir=rtl],[dir=rtl] *){padding-right:1rem}.rtl\:text-left:where([dir=rtl],[dir=rtl] *){text-align:left}.rtl\:text-right:where([dir=rtl],[dir=rtl] *){text-align:right}.rtl\:before\:bg-green-500:where([dir=rtl],[dir=rtl] *):before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.rtl\:before\:bg-red-500:where([dir=rtl],[dir=rtl] *):before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.after\:rtl\:right-0\.5:where([dir=rtl],[dir=rtl] *):after{content:var(--tw-content);right:.125rem}.rtl\:first\:rounded-r-lg:first-child:where([dir=rtl],[dir=rtl] *){border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.peer:checked~.peer-checked\:after\:rtl\:-translate-x-full:where([dir=rtl],[dir=rtl] *):after{content:var(--tw-content);--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media(min-width:1024px){.lg\:rtl\:pr-\[215px\]:where([dir=rtl],[dir=rtl] *){padding-right:215px}.group\/container.sidebar-collapsed .lg\:group-\[\.sidebar-collapsed\]\/container\:rtl\:pr-\[85px\]:where([dir=rtl],[dir=rtl] *){padding-right:85px}}.\[\&\.active\>\.v-tree-item\]\:block.active>.v-tree-item{display:block}@media not all and (min-width:1024px){.\[\&\>\*\>\*\:nth-child\(1\)\]\:max-lg\:\!flex-wrap>*>*:nth-child(1){flex-wrap:wrap!important}.\[\&\>\*\>\*\>\*\.toolbarRight\]\:max-lg\:w-full>*>*>*.toolbarRight{width:100%}.\[\&\>\*\>\*\>\*\.toolbarRight\]\:max-lg\:justify-between>*>*>*.toolbarRight{justify-content:space-between}}@media not all and (min-width:768px){.\[\&\>\*\>\*\>\*\.toolbarRight\]\:max-md\:flex-wrap>*>*>*.toolbarRight{flex-wrap:wrap}.\[\&\>\*\>\*\>\*\.toolbarRight\]\:max-md\:gap-y-2>*>*>*.toolbarRight{row-gap:.5rem}}.\[\&\>\*\]\:after\:content-\[\'_\/_\'\]>*:after{--tw-content: " / ";content:var(--tw-content)}.\[\&\>\.v-tree-item\]\:hidden>.v-tree-item{display:none}.\[\&\>\.v-tree-item\]\:ltr\:pl-6:where([dir=ltr],[dir=ltr] *)>.v-tree-item{padding-left:1.5rem}.\[\&\>\.v-tree-item\]\:rtl\:pr-6:where([dir=rtl],[dir=rtl] *)>.v-tree-item{padding-right:1.5rem}.\[\&\>\:nth-child\(3\)\]\:\!m-0>:nth-child(3){margin:0!important}.\[\&\>\:nth-child\(3\)\]\:\!rounded-l-none>:nth-child(3){border-top-left-radius:0!important;border-bottom-left-radius:0!important}@media not all and (min-width:525px){.\[\&\>\:nth-child\(3\)\]\:max-sm\:\!w-\[80\%\]>:nth-child(3){width:80%!important}}.\[\&\>div\:last-child\]\:border-b-0>div:last-child{border-bottom-width:0px}.ltr\:\[\&\>span\>i\]\:right-14>span>i:where([dir=ltr],[dir=ltr] *){right:3.5rem}.rtl\:\[\&\>span\>i\]\:left-14>span>i:where([dir=rtl],[dir=rtl] *){left:3.5rem} diff --git a/public/admin/build/assets/ar.es-bitOgb9K.js b/public/admin/build/assets/ar.es-bitOgb9K.js new file mode 100644 index 0000000..52146bf --- /dev/null +++ b/public/admin/build/assets/ar.es-bitOgb9K.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["الإثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],e=["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر"," ديسمبر"],n="سنوات",r="سنة",l="شهر",o="أسبوع",m="يوم",s="اليوم",a="لا حدث",c="طوال اليوم",v="حذف",d="إنشاء حدث",y="dddd D MMMM YYYY",E={weekDays:t,months:e,years:n,year:"سنة",month:"شهر",week:o,day:"يوم",today:s,noEvent:a,allDay:c,deleteEvent:"حذف",createEvent:d,dateFormat:y};export{c as allDay,d as createEvent,y as dateFormat,m as day,E as default,v as deleteEvent,l as month,e as months,a as noEvent,s as today,o as week,t as weekDays,r as year,n as years}; diff --git a/public/admin/build/assets/bg.es-5tp0KQRo.js b/public/admin/build/assets/bg.es-5tp0KQRo.js new file mode 100644 index 0000000..d57af17 --- /dev/null +++ b/public/admin/build/assets/bg.es-5tp0KQRo.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота","Неделя"],n=["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],o="Години",s="Година",e="Месец",c="Седмица",Y="Ден",a="Днес",d="Няма събития",y="Цял ден",r="Изтрий",l="Създай събитие",M="dddd D MMMM YYYY",m={weekDays:t,months:n,years:o,year:s,month:e,week:c,day:"Ден",today:a,noEvent:d,allDay:y,deleteEvent:r,createEvent:l,dateFormat:M};export{y as allDay,l as createEvent,M as dateFormat,Y as day,m as default,r as deleteEvent,e as month,n as months,d as noEvent,a as today,c as week,t as weekDays,s as year,o as years}; diff --git a/public/admin/build/assets/bn.es-C_qR0y0-.js b/public/admin/build/assets/bn.es-C_qR0y0-.js new file mode 100644 index 0000000..0794da9 --- /dev/null +++ b/public/admin/build/assets/bn.es-C_qR0y0-.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["সোম","মঙ্গল","বুধ","বৃহস্পতি","শুক্র","শনি","রবি"],n=["জানুয়ারি","ফেব্ুয়ারী","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],y="বছর",r="বছর",l="মাস",o="সপ্তাহ",m="দিন",M="আজ",e="কার্যসূচী",s="সারাদিন",a="মুছুন",c="কার্যসূচী তৈরি করুন",d="dddd D MMMM YYYY",Y={weekDays:t,months:n,years:"বছর",year:"বছর",month:"মাস",week:o,day:"দিন",today:"আজ",noEvent:e,allDay:s,deleteEvent:a,createEvent:c,dateFormat:d};export{s as allDay,c as createEvent,d as dateFormat,m as day,Y as default,a as deleteEvent,l as month,n as months,e as noEvent,M as today,o as week,t as weekDays,r as year,y as years}; diff --git a/public/admin/build/assets/bs.es-DduooXS8.js b/public/admin/build/assets/bs.es-DduooXS8.js new file mode 100644 index 0000000..48c99f1 --- /dev/null +++ b/public/admin/build/assets/bs.es-DduooXS8.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const a=["Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota","Nedjelja"],e=["Januar","Februar","Mart","April","Maj","Jun","Jul","Avgust","Septembar","Oktobar","Novembar","Decembar"],t="Godine",o="Godina",n="Mjesec",s="Sedmica",b="Dan",r="Danas",d="Nema događaja",c="Cijeli dan",j="Obriši",i="Kreiraj događaj",l="dddd D MMMM YYYY",m={weekDays:a,months:e,years:t,year:o,month:n,week:s,day:"Dan",today:r,noEvent:d,allDay:c,deleteEvent:j,createEvent:i,dateFormat:l};export{c as allDay,i as createEvent,l as dateFormat,b as day,m as default,j as deleteEvent,n as month,e as months,d as noEvent,r as today,s as week,a as weekDays,o as year,t as years}; diff --git a/public/admin/build/assets/ca.es-DYDNDJFJ.js b/public/admin/build/assets/ca.es-DYDNDJFJ.js new file mode 100644 index 0000000..490faa0 --- /dev/null +++ b/public/admin/build/assets/ca.es-DYDNDJFJ.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Dilluns","Dimarts","Dimecres","Dijous","Divendres","Dissabte","Diumenge"],t=["Dl","Dt","Dc","Dj","Dv","Ds","Dg"],n=["Gener","Febrer","Març","Abril","Maig","Juny","Juliol","Agost","Setembre","Octubre","Novembre","Desembre"],s="Anys",m="Any",l="Mes",o="Setmana",y="Dia",a="Avui",r="No hi ha esdeveniments",c="Tot el dia",D="Eliminar",i="Crear un esdeveniment",d="dddd D MMMM YYYY",u={weekDays:e,weekDaysShort:t,months:n,years:s,year:"Any",month:"Mes",week:o,day:"Dia",today:a,noEvent:r,allDay:c,deleteEvent:D,createEvent:i,dateFormat:d};export{c as allDay,i as createEvent,d as dateFormat,y as day,u as default,D as deleteEvent,l as month,n as months,r as noEvent,a as today,o as week,e as weekDays,t as weekDaysShort,m as year,s as years}; diff --git a/public/admin/build/assets/calls-BU_UfDax.svg b/public/admin/build/assets/calls-BU_UfDax.svg new file mode 100644 index 0000000..6ade01a --- /dev/null +++ b/public/admin/build/assets/calls-BU_UfDax.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/corner-clip-DsfWoKt7.svg b/public/admin/build/assets/corner-clip-DsfWoKt7.svg new file mode 100644 index 0000000..03e4193 --- /dev/null +++ b/public/admin/build/assets/corner-clip-DsfWoKt7.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/admin/build/assets/cs.es-CvnAvfoI.js b/public/admin/build/assets/cs.es-CvnAvfoI.js new file mode 100644 index 0000000..fa656f2 --- /dev/null +++ b/public/admin/build/assets/cs.es-CvnAvfoI.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Pondělí","Úterý","Středa","Čtvrtek","Pátek","Sobota","Neděle"],t=["Leden","Únor","Březen","Duben","Květen","Červen","Červenec","Srpen","Září","Říjen","Listopad","Prosinec"],n="Roky",v="Rok",o="Měsíc",s="Týden",k="Den",d="Dnes",c="Bez událostí",a="Celý den",r="Odstranit",y="Vytvořit událost",l="dddd D. MMMM YYYY",D={weekDays:e,months:t,years:n,year:"Rok",month:o,week:s,day:"Den",today:d,noEvent:c,allDay:a,deleteEvent:r,createEvent:y,dateFormat:l};export{a as allDay,y as createEvent,l as dateFormat,k as day,D as default,r as deleteEvent,o as month,t as months,c as noEvent,d as today,s as week,e as weekDays,v as year,n as years}; diff --git a/public/admin/build/assets/da.es-C_eJXDX3.js b/public/admin/build/assets/da.es-C_eJXDX3.js new file mode 100644 index 0000000..d5f4de4 --- /dev/null +++ b/public/admin/build/assets/da.es-C_eJXDX3.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag","Søndag"],t=["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],a="År (flertal)",l="År",n="Måned",y="Uge",M="Dag",o="I dag",r="Ingen begivenhed",s="Hele dagen",d="Slet",c="Opret et event",g="dddd D MMMM YYYY",u={weekDays:e,months:t,years:a,year:"År",month:n,week:"Uge",day:"Dag",today:o,noEvent:r,allDay:s,deleteEvent:d,createEvent:c,dateFormat:g};export{s as allDay,c as createEvent,g as dateFormat,M as day,u as default,d as deleteEvent,n as month,t as months,r as noEvent,o as today,y as week,e as weekDays,l as year,a as years}; diff --git a/public/admin/build/assets/dark-corner-clip-Deux0RQO.svg b/public/admin/build/assets/dark-corner-clip-Deux0RQO.svg new file mode 100644 index 0000000..9c36b81 --- /dev/null +++ b/public/admin/build/assets/dark-corner-clip-Deux0RQO.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/admin/build/assets/dark-logo-Dnw3cpYj.svg b/public/admin/build/assets/dark-logo-Dnw3cpYj.svg new file mode 100644 index 0000000..47f0509 --- /dev/null +++ b/public/admin/build/assets/dark-logo-Dnw3cpYj.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/admin/build/assets/de.es-Cp-R9E16.js b/public/admin/build/assets/de.es-Cp-R9E16.js new file mode 100644 index 0000000..e37f1a8 --- /dev/null +++ b/public/admin/build/assets/de.es-Cp-R9E16.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"],t=["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],n="Jahre",a="Jahr",o="Monat",s="Woche",l="Tag",r="Heute",c="Keine Events",d="Ganztägig",g="Löschen",i="Event erstellen",M="dddd D MMMM YYYY",u={weekDays:e,months:t,years:n,year:a,month:o,week:s,day:"Tag",today:r,noEvent:c,allDay:d,deleteEvent:g,createEvent:i,dateFormat:M};export{d as allDay,i as createEvent,M as dateFormat,l as day,u as default,g as deleteEvent,o as month,t as months,c as noEvent,r as today,s as week,e as weekDays,a as year,n as years}; diff --git a/public/admin/build/assets/default-DBulZ5O_.svg b/public/admin/build/assets/default-DBulZ5O_.svg new file mode 100644 index 0000000..fa4e8d9 --- /dev/null +++ b/public/admin/build/assets/default-DBulZ5O_.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/description-BwifbkWG.svg b/public/admin/build/assets/description-BwifbkWG.svg new file mode 100644 index 0000000..d367df7 --- /dev/null +++ b/public/admin/build/assets/description-BwifbkWG.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/drag-and-drop.es-CbCm7IHa.js b/public/admin/build/assets/drag-and-drop.es-CbCm7IHa.js new file mode 100644 index 0000000..b08c6d1 --- /dev/null +++ b/public/admin/build/assets/drag-and-drop.es-CbCm7IHa.js @@ -0,0 +1,5 @@ +var V=Object.defineProperty,S=(t,e,a)=>e in t?V(t,e,{enumerable:!0,configurable:!0,writable:!0,value:a}):t[e]=a,C=(t,e,a)=>S(t,e+"",a);/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const m=800;let d=null,c=null,v={id:null,date:null},f=!1,_=!0,u={el:null,cell:null,timeout:null};const r={_eid:null,fromVueCal:null,toVueCal:null},M=class{constructor(t){C(this,"_vuecal"),this._vuecal=t}_getEventStart(t){const{timeStep:e,timeCellHeight:a,timeFrom:i,utils:n}=this._vuecal;let{y:l}=n.cell.getPosition(t);return l-=t.dataTransfer.getData("cursor-grab-at")*1,Math.round(l*e/parseInt(a)+i)}_updateEventStartEnd(t,e,a,i){const n=a.duration*1||e.endTimeMinutes-e.startTimeMinutes;let l=Math.max(this._getEventStart(t),0);if(this._vuecal.snapToTime){const o=l+this._vuecal.snapToTime/2;l=o-o%this._vuecal.snapToTime}e.startTimeMinutes=l,e.start=new Date(new Date(i).setMinutes(l)),e.endTimeMinutes=Math.min(l+n,1440),e.end=new Date(new Date(i).setMinutes(e.endTimeMinutes))}eventDragStart(t,e){if(t.target.nodeType===3)return t.preventDefault();t.dataTransfer.dropEffect="move",t.dataTransfer.setData("event",JSON.stringify(e)),t.dataTransfer.setData("cursor-grab-at",t.offsetY);const{clickHoldAnEvent:a}=this._vuecal.domEvents;setTimeout(()=>{a._eid=null,clearTimeout(a.timeoutId),e.deleting=!1},0),this._vuecal.domEvents.dragAnEvent._eid=e._eid,r._eid=e._eid,r.fromVueCal=this._vuecal._.uid,e.dragging=!0,setTimeout(()=>e.draggingStatic=!0,0),f=!1,v={id:this._vuecal.view.id,date:this._vuecal.view.startDate},_=!0}eventDragEnd(t){this._vuecal.domEvents.dragAnEvent._eid=null,r._eid=null,t.dragging=!1,t.draggingStatic=!1;const{fromVueCal:e,toVueCal:a}=r;a&&e!==a&&this._vuecal.utils.event.deleteAnEvent(t),r.fromVueCal=null,r.toVueCal=null,f&&_&&v.id&&this._vuecal.switchView(v.id,v.date,!0)}cellDragEnter(t,e,a){const i=t.currentTarget;if(!t.currentTarget.contains(t.relatedTarget)){if(i===u.el||!i.className.includes("vuecal__cell-content"))return!1;u.el&&(u.cell.highlighted=!1),u={el:i,cell:e,timeout:clearTimeout(u.timeout)},e.highlighted=!0,["years","year","month"].includes(this._vuecal.view.id)&&(u.timeout=setTimeout(()=>this._vuecal.switchToNarrowerView(a),2e3))}}cellDragOver(t,e,a,i){t.preventDefault(),e.highlighted=!0,(i||i===0)&&(e.highlightedSplit=i)}cellDragLeave(t,e){t.preventDefault(),!t.currentTarget.contains(t.relatedTarget)&&(e.highlightedSplit=!1,u.cell===e&&(clearTimeout(u.timeout),u={el:null,cell:null,timeout:null},e.highlighted=!1))}cellDragDrop(t,e,a,i){t.preventDefault(),clearTimeout(u.timeout),u={el:null,cell:null,timeout:null};const n=JSON.parse(t.dataTransfer.getData("event")||"{}");let l,o;if(r.fromVueCal!==this._vuecal._.uid){const{_eid:s,start:D,end:p,duration:h,...E}=n;l=this._vuecal.utils.event.createAnEvent(a,h,{...E,split:i})}else if(l=this._vuecal.view.events.find(s=>s._eid===r._eid),l||(l=this._vuecal.mutableEvents.find(s=>s._eid===r._eid),o=!!l),!l){const s=n.endTimeMinutes-n.startTimeMinutes,{start:D,end:p,...h}=n;l=this._vuecal.utils.event.createAnEvent(a,s,{...h,split:i})}const{start:T,split:w}=l;this._updateEventStartEnd(t,l,n,a),o&&this._vuecal.addEventsToView([l]),l.dragging=!1,(i||i===0)&&(l.split=i),e.highlighted=!1,e.highlightedSplit=null,_=!1,r.toVueCal=this._vuecal._.uid;const g={event:this._vuecal.cleanupEvent(l),oldDate:T,newDate:l.start,...(i||i===0)&&{oldSplit:w,newSplit:i},originalEvent:this._vuecal.cleanupEvent(n),external:!r.fromVueCal};this._vuecal.$emit("event-drop",g),this._vuecal.$emit("event-change",{event:g.event,originalEvent:g.originalEvent}),setTimeout(()=>{r._eid&&this.eventDragEnd(l)},300)}viewSelectorDragEnter(t,e,a){t.currentTarget.contains(t.relatedTarget)||(a.highlightedControl=e,clearTimeout(d),d=setTimeout(()=>{if(["previous","next"].includes(e))this._vuecal[e](),clearInterval(c),c=setInterval(this._vuecal[e],m);else if(e==="today"){clearInterval(c);let i;this._vuecal.view.id.includes("year")&&(i=this._vuecal.enabledViews.filter(n=>!n.includes("year"))[0]),this._vuecal.switchView(i||this._vuecal.view.id,new Date(new Date().setHours(0,0,0,0)),!0)}else this._vuecal.switchView(e,null,!0);f=!0},m))}viewSelectorDragLeave(t,e,a){t.currentTarget.contains(t.relatedTarget)||a.highlightedControl===e&&(a.highlightedControl=null,d&&(d=clearTimeout(d)),c&&(c=clearInterval(c)))}};export{M as DragAndDrop}; diff --git a/public/admin/build/assets/el.es-BZ44a1Tb.js b/public/admin/build/assets/el.es-BZ44a1Tb.js new file mode 100644 index 0000000..356591f --- /dev/null +++ b/public/admin/build/assets/el.es-BZ44a1Tb.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο","Κυριακή"],n=["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],o=["Ιανουαρίου","Φεβρουαρίου","Μαρτίου","Απριλίου","Μαΐου","Ιουνίου","Ιουλίου","Αυγούστου","Σεπτεμβρίου","Οκτωβρίου","Νοεμβρίου","Δεκεμβρίου"],h="Έτη",s="Έτος",e="Μήνα",c="Εβδομάδα",a="Ημέρα",d="Σήμερα",y="Κανένα συμβάν",m="Ημερήσιο συμβάν",r="Διαγραφή",l="Δημιουργία συμβάντος",v="dddd D MMMMG YYYY",M="π.μ.",Y="μ.μ.",D={weekDays:t,months:n,monthsGenitive:o,years:"Έτη",year:s,month:e,week:c,day:a,today:d,noEvent:y,allDay:m,deleteEvent:r,createEvent:l,dateFormat:v,am:M,pm:Y};export{m as allDay,M as am,l as createEvent,v as dateFormat,a as day,D as default,r as deleteEvent,e as month,n as months,o as monthsGenitive,y as noEvent,Y as pm,d as today,c as week,t as weekDays,s as year,h as years}; diff --git a/public/admin/build/assets/emails-D_qEBj20.svg b/public/admin/build/assets/emails-D_qEBj20.svg new file mode 100644 index 0000000..24a893f --- /dev/null +++ b/public/admin/build/assets/emails-D_qEBj20.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/en.es-DwVNOttH.js b/public/admin/build/assets/en.es-DwVNOttH.js new file mode 100644 index 0000000..178b4be --- /dev/null +++ b/public/admin/build/assets/en.es-DwVNOttH.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],t=["January","February","March","April","May","June","July","August","September","October","November","December"],a="Years",n="Year",o="Month",s="Week",M="Day",y="Today",d="No Event",r="All day",c="Delete",u="Create an event",l="dddd MMMM D{S}, YYYY",m={weekDays:e,months:t,years:a,year:n,month:o,week:s,day:"Day",today:y,noEvent:d,allDay:r,deleteEvent:c,createEvent:u,dateFormat:l};export{r as allDay,u as createEvent,l as dateFormat,M as day,m as default,c as deleteEvent,o as month,t as months,d as noEvent,y as today,s as week,e as weekDays,n as year,a as years}; diff --git a/public/admin/build/assets/error-ClRu8L1K.svg b/public/admin/build/assets/error-ClRu8L1K.svg new file mode 100644 index 0000000..6fe0d2a --- /dev/null +++ b/public/admin/build/assets/error-ClRu8L1K.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/es.es-DO8PaQ_q.js b/public/admin/build/assets/es.es-DO8PaQ_q.js new file mode 100644 index 0000000..1561c2e --- /dev/null +++ b/public/admin/build/assets/es.es-DO8PaQ_q.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Lunes","Martes","Miércoles","Jueves","Viernes","Sábado","Domingo"],o=["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],t="Años",y="Año",i="Mes",n="Semana",m="Día",M="Hoy",s="No hay evento",a="Todo el día",r="Borrar",c="Crear un evento",d="dddd D MMMM YYYY",l={weekDays:e,months:o,years:t,year:"Año",month:"Mes",week:n,day:"Día",today:"Hoy",noEvent:s,allDay:a,deleteEvent:r,createEvent:c,dateFormat:d};export{a as allDay,c as createEvent,d as dateFormat,m as day,l as default,r as deleteEvent,i as month,o as months,s as noEvent,M as today,n as week,e as weekDays,y as year,t as years}; diff --git a/public/admin/build/assets/et.es-DdRIcUrz.js b/public/admin/build/assets/et.es-DdRIcUrz.js new file mode 100644 index 0000000..5816174 --- /dev/null +++ b/public/admin/build/assets/et.es-DdRIcUrz.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Esmaspäev","Teisipäev","Kolmapäev","Neljapäev","Reede","Laupäev","Pühapäev"],t=["Jaanuar","Veebruar","Märts","Aprill","Mai","Juuni","Juuli","August","September","Oktoober","November","Detsember"],s="Aastad",a="Aasta",p="Kuu",o="Nädal",n="Päev",u="Täna",c="Sündmus puudub",d="Terve päev",r="Kustuta",v="Loo sündmus",m="dddd D MMMM YYYY",l={weekDays:e,months:t,years:s,year:a,month:"Kuu",week:o,day:n,today:u,noEvent:c,allDay:d,deleteEvent:r,createEvent:v,dateFormat:m};export{d as allDay,v as createEvent,m as dateFormat,n as day,l as default,r as deleteEvent,p as month,t as months,c as noEvent,u as today,o as week,e as weekDays,a as year,s as years}; diff --git a/public/admin/build/assets/fa.es-XN0av8Tx.js b/public/admin/build/assets/fa.es-XN0av8Tx.js new file mode 100644 index 0000000..dc15e80 --- /dev/null +++ b/public/admin/build/assets/fa.es-XN0av8Tx.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["دوشنبه","سه شنبه","چهار شنبه","پنج شنبه","جمعه","شنبه","یک شنبه"],e=["ژانویه","فوریه","مارس","آوریل","می","ژوئن","ژوئیه","اوت","سپتامبر","اکتبر","نوامبر","دسامبر"],n="سالها",r="سال",l="ماه",o="هفته",m="روز",s="امروز",a="رویدادی نیست",c="تمام روز",v="حذف",d="ایجاد یک رویداد",y="dddd D MMMM YYYY",E={weekDays:t,months:e,years:n,year:"سال",month:"ماه",week:o,day:"روز",today:s,noEvent:a,allDay:c,deleteEvent:"حذف",createEvent:d,dateFormat:y};export{c as allDay,d as createEvent,y as dateFormat,m as day,E as default,v as deleteEvent,l as month,e as months,a as noEvent,s as today,o as week,t as weekDays,r as year,n as years}; diff --git a/public/admin/build/assets/favicon-BtbgZBji.ico b/public/admin/build/assets/favicon-BtbgZBji.ico new file mode 100644 index 0000000000000000000000000000000000000000..8fd69159384857b582cc8e427588faba37285153 GIT binary patch literal 1150 zcmd7OOG_J37{>888ZRK33WDIul#sCtsU*P*QtC=*loqloF1(de)Lj?qR?xkpg)Utc zKSu?@uizMks03O;XbSx+dd4__jl7pPb)>x7flw+Hh|T?SZl%@e5^KVj8R^|5+o` z=iBITQuzgxpsce--v&~D&xe6$hnrp${clDR9|lwYcb6SV`Gt4m@!aX>*mL$pwDDm}-wpzAJ^Lc@o*HLlPbNm6&K_)d+pHCk4TC4zUn!v$E_+VDj4K4MUnZs|U&W{L z8{QYj*R{1{+IkK8jnKI+tGpD+rNLhBXKl`{sm9oT5YySc2=A<`mTKx=%~(PSF0XbM zU5H{1KcM;BS=E;&EW*_&($9rKtnw}C>p!T*5oV!t>z`PCsKzq#$c3L;E48(WvFM&Y yw6C6Z9nGN?N$5Pc^r&sQY$n$uX1}}5E@}!bW?!+=_~&|&6|>bHv%S-5M!6fYLjX_! literal 0 HcmV?d00001 diff --git a/public/admin/build/assets/fi.es-B0Iy6aas.js b/public/admin/build/assets/fi.es-B0Iy6aas.js new file mode 100644 index 0000000..3e837cd --- /dev/null +++ b/public/admin/build/assets/fi.es-B0Iy6aas.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const a=["Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai","Sunnuntai"],t=["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],u="Vuodet",o="Vuosi",n="Kuukausi",s="Viikko",i="Päivä",e="Tänään",k="Ei tapahtumia",c="Koko päivä",d="Poista tapahtuma",m="Luo tapahtuma",r="dddd, D MMMM YYYY",l={weekDays:a,months:t,years:u,year:o,month:n,week:s,day:i,today:e,noEvent:k,allDay:c,deleteEvent:d,createEvent:m,dateFormat:r};export{c as allDay,m as createEvent,r as dateFormat,i as day,l as default,d as deleteEvent,n as month,t as months,k as noEvent,e as today,s as week,a as weekDays,o as year,u as years}; diff --git a/public/admin/build/assets/files-Xm-jQ6Fi.svg b/public/admin/build/assets/files-Xm-jQ6Fi.svg new file mode 100644 index 0000000..e75d4fc --- /dev/null +++ b/public/admin/build/assets/files-Xm-jQ6Fi.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/fr.es-B7WXlprl.js b/public/admin/build/assets/fr.es-B7WXlprl.js new file mode 100644 index 0000000..866d658 --- /dev/null +++ b/public/admin/build/assets/fr.es-B7WXlprl.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi","Dimanche"],n=["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"],t="Années",o="Année",r="Mois",s="Semaine",c="Jour",a="Aujourd'hui",i="Aucun événement",d="Jour entier",u="Supprimer",m="Créer un événement",v="dddd D MMMM YYYY",M={weekDays:e,months:n,years:t,year:o,month:r,week:s,day:c,today:a,noEvent:i,allDay:d,deleteEvent:u,createEvent:m,dateFormat:v};export{d as allDay,m as createEvent,v as dateFormat,c as day,M as default,u as deleteEvent,r as month,n as months,i as noEvent,a as today,s as week,e as weekDays,o as year,t as years}; diff --git a/public/admin/build/assets/he.es-BWEk5prp.js b/public/admin/build/assets/he.es-BWEk5prp.js new file mode 100644 index 0000000..4989abc --- /dev/null +++ b/public/admin/build/assets/he.es-BWEk5prp.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["שני","שלישי","רביעי","חמישי","שישי","שבת","ראשון"],n=["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],o="שנים",M="שנה",e="חודש",s="שבוע",Y="יום",c="היום",a="אין אירועים",d="כל היום",y="מחיקה",r="צור אירוע",l="dddd D MMMM YYYY",h={weekDays:t,months:n,years:o,year:"שנה",month:e,week:s,day:"יום",today:c,noEvent:a,allDay:d,deleteEvent:y,createEvent:r,dateFormat:l};export{d as allDay,r as createEvent,l as dateFormat,Y as day,h as default,y as deleteEvent,e as month,n as months,a as noEvent,c as today,s as week,t as weekDays,M as year,o as years}; diff --git a/public/admin/build/assets/hr.es-B_iqHQyr.js b/public/admin/build/assets/hr.es-B_iqHQyr.js new file mode 100644 index 0000000..d95ca07 --- /dev/null +++ b/public/admin/build/assets/hr.es-B_iqHQyr.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const a=["Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota","Nedjelja"],n=["Siječanj","Veljača","Ožujak","Travanj","Svibanj","Lipanj","Srpanj","Kolovoz","Rujan","Listopad","Studeni","Prosinac"],e="Godine",t="Godina",o="Mjesec",d="Tjedan",k="Dan",j="Današnji dan",s="Nema događaja",c="Cijeli dan",i="Obriši",r="Kreiraj događaj",l="dddd D MMMM YYYY",v={weekDays:a,months:n,years:e,year:t,month:o,week:d,day:"Dan",today:j,noEvent:s,allDay:c,deleteEvent:i,createEvent:r,dateFormat:l};export{c as allDay,r as createEvent,l as dateFormat,k as day,v as default,i as deleteEvent,o as month,n as months,s as noEvent,j as today,d as week,a as weekDays,t as year,e as years}; diff --git a/public/admin/build/assets/hu.es-D_t4Ibxf.js b/public/admin/build/assets/hu.es-D_t4Ibxf.js new file mode 100644 index 0000000..22ff2dc --- /dev/null +++ b/public/admin/build/assets/hu.es-D_t4Ibxf.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Hétfo","Kedd","Szerda","Csütörtök","Péntek","Szombat","Vasárnap"],t=["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],s="Évek",y="Év",n="Hónap",m="Hét",l="Nap",a="Mai nap",o="Nincs esemény",r="Egész nap",c="Esemény törlese",d="Esemény létrehozása",u="dddd D MMMM YYYY",p={weekDays:e,months:t,years:s,year:"Év",month:n,week:"Hét",day:"Nap",today:a,noEvent:o,allDay:r,deleteEvent:c,createEvent:d,dateFormat:u};export{r as allDay,d as createEvent,u as dateFormat,l as day,p as default,c as deleteEvent,n as month,t as months,o as noEvent,a as today,m as week,e as weekDays,y as year,s as years}; diff --git a/public/admin/build/assets/icomoon-BbY1GX-d.woff b/public/admin/build/assets/icomoon-BbY1GX-d.woff new file mode 100644 index 0000000000000000000000000000000000000000..bedf008609c593ccf2e7c0d4d34c7c9fc4bf2fb2 GIT binary patch literal 37964 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 + * @license MIT + */const a=["Senin","Selasa","Rabu","Kamis","Jumat","Sabtu","Minggu"],n=["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],e="Tahunan",t="Tahun",s="Bulan",o="Minggu",i="Hari",r="Hari Ini",u="Tidak Ada Kegiatan",c="Sepanjang Hari",d="Hapus",m="Tambah Kegiatan",b="dddd, D MMMM YYYY",g={weekDays:a,months:n,years:e,year:t,month:s,week:o,day:i,today:r,noEvent:u,allDay:c,deleteEvent:d,createEvent:m,dateFormat:b};export{c as allDay,m as createEvent,b as dateFormat,i as day,g as default,d as deleteEvent,s as month,n as months,u as noEvent,r as today,o as week,a as weekDays,t as year,e as years}; diff --git a/public/admin/build/assets/is.es-DChRpym4.js b/public/admin/build/assets/is.es-DChRpym4.js new file mode 100644 index 0000000..e9a4c11 --- /dev/null +++ b/public/admin/build/assets/is.es-DChRpym4.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const a=["Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur","Sunnudagur"],t=["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],i="Ár",l="Ár",n="Mánuður",r="Vika",e="Dagur",s="Í dag",u="Enginn atburður",o="Allan daginn",d="Eyða",c="Búðu til viðburð",g="dddd D MMMM YYYY",y={weekDays:a,months:t,years:"Ár",year:"Ár",month:n,week:r,day:e,today:s,noEvent:u,allDay:o,deleteEvent:d,createEvent:c,dateFormat:g};export{o as allDay,c as createEvent,g as dateFormat,e as day,y as default,d as deleteEvent,n as month,t as months,u as noEvent,s as today,r as week,a as weekDays,l as year,i as years}; diff --git a/public/admin/build/assets/it.es-CquQA5xx.js b/public/admin/build/assets/it.es-CquQA5xx.js new file mode 100644 index 0000000..2d894e2 --- /dev/null +++ b/public/admin/build/assets/it.es-CquQA5xx.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato","Domenica"],o=["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],t="Anni",n="Anno",a="Mese",s="Settimana",c="Giorno",r="Oggi",i="Nessun evento",d="Tutto il giorno",l="Cancella",M="Crea evento",g="dddd D MMMM YYYY",m={weekDays:e,months:o,years:t,year:n,month:a,week:s,day:c,today:r,noEvent:i,allDay:d,deleteEvent:l,createEvent:M,dateFormat:g};export{d as allDay,M as createEvent,g as dateFormat,c as day,m as default,l as deleteEvent,a as month,o as months,i as noEvent,r as today,s as week,e as weekDays,n as year,t as years}; diff --git a/public/admin/build/assets/ja.es-B19gsG6H.js b/public/admin/build/assets/ja.es-B19gsG6H.js new file mode 100644 index 0000000..fbc0fed --- /dev/null +++ b/public/admin/build/assets/ja.es-B19gsG6H.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["月","火","水","木","金","土","日"],e=["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],s="年",c="今年",d="月",y="週",l="日",r="今日",n="イベントなし",m="終日",v="削除",o="イベント作成",a="YYYY年 MMMM D日 (dddd)",D={weekDays:t,months:e,years:"年",year:"今年",month:"月",week:"週",day:"日",today:"今日",noEvent:n,allDay:"終日",deleteEvent:"削除",createEvent:o,dateFormat:a};export{m as allDay,o as createEvent,a as dateFormat,l as day,D as default,v as deleteEvent,d as month,e as months,n as noEvent,r as today,y as week,t as weekDays,c as year,s as years}; diff --git a/public/admin/build/assets/ka.es-CuOe65jc.js b/public/admin/build/assets/ka.es-CuOe65jc.js new file mode 100644 index 0000000..e89a6ed --- /dev/null +++ b/public/admin/build/assets/ka.es-CuOe65jc.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["ორშაბათი","სამშაბათი","ოთხშაბათი","ხუთშაბათი","პარასკევი","შაბათი","კვირა"],n=["იანვარი","თებერვალი","მარტი","აპრილი","მაისი","ივნისი","ივლისი","აგვისტო","სექტემბერი","ოქტომბერი","ნოემბერი","დეკემბერი"],o="წლები",s="წელი",m="თვე",e="კვირა",M="დღე",c="დღეს",a="ღონისძიება არ არის",d="მთელი დღე",y="წაშლა",r="შექმენით ღონისძიება",l="dddd D MMMM YYYY",Y={weekDays:t,months:n,years:o,year:s,month:"თვე",week:e,day:"დღე",today:c,noEvent:a,allDay:d,deleteEvent:y,createEvent:r,dateFormat:l};export{d as allDay,r as createEvent,l as dateFormat,M as day,Y as default,y as deleteEvent,m as month,n as months,a as noEvent,c as today,e as week,t as weekDays,s as year,o as years}; diff --git a/public/admin/build/assets/ko.es-CsMl2USK.js b/public/admin/build/assets/ko.es-CsMl2USK.js new file mode 100644 index 0000000..8517456 --- /dev/null +++ b/public/admin/build/assets/ko.es-CsMl2USK.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["월","화","수","목","금","토","일"],e=["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],c="년도",d="연간",y="월간",r="주간",l="일간",k="오늘",o="일정 없음",n="하루 종일",m="삭제",s="일정 추가",a="YYYY년 MMMM D일 dddd요일",v={weekDays:t,months:e,years:"년도",year:"연간",month:"월간",week:"주간",day:"일간",today:"오늘",noEvent:o,allDay:n,deleteEvent:"삭제",createEvent:s,dateFormat:a};export{n as allDay,s as createEvent,a as dateFormat,l as day,v as default,m as deleteEvent,y as month,e as months,o as noEvent,k as today,r as week,t as weekDays,d as year,c as years}; diff --git a/public/admin/build/assets/logo-Bjh7YAuF.svg b/public/admin/build/assets/logo-Bjh7YAuF.svg new file mode 100644 index 0000000..50755ec --- /dev/null +++ b/public/admin/build/assets/logo-Bjh7YAuF.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/admin/build/assets/lt.es-vqgjGyMx.js b/public/admin/build/assets/lt.es-vqgjGyMx.js new file mode 100644 index 0000000..b8ccdb3 --- /dev/null +++ b/public/admin/build/assets/lt.es-vqgjGyMx.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const i=["Pirmadienis","Antradienis","Trečiadienis","Ketvirtadienis","Penktadienis","Šeštadienis","Sekmadienis"],s=["Sausis","Vasaris","Kovas","Balandis","Gegužė","Birželis","Liepa","Rugpjūtis","Rugsėjis","Spalis","Lapkritis","Gruodis"],t="Metų pasirinkimas",e="Metai",a="Mėnesis",n="Savaitė",o="Diena",d="Šiandien",r="Jokių įvykių",c="Visa diena",k="Ištrinti",l="Sukurti įvykį",u="dddd, D MMMM YYYY",v={weekDays:i,months:s,years:t,year:e,month:a,week:n,day:o,today:d,noEvent:r,allDay:c,deleteEvent:k,createEvent:l,dateFormat:u};export{c as allDay,l as createEvent,u as dateFormat,o as day,v as default,k as deleteEvent,a as month,s as months,r as noEvent,d as today,n as week,i as weekDays,e as year,t as years}; diff --git a/public/admin/build/assets/lunches-BHeSbGQ3.svg b/public/admin/build/assets/lunches-BHeSbGQ3.svg new file mode 100644 index 0000000..9eef171 --- /dev/null +++ b/public/admin/build/assets/lunches-BHeSbGQ3.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/meetings-Cwj897qL.svg b/public/admin/build/assets/meetings-Cwj897qL.svg new file mode 100644 index 0000000..6aec1c6 --- /dev/null +++ b/public/admin/build/assets/meetings-Cwj897qL.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/mn.es-BmdwFdas.js b/public/admin/build/assets/mn.es-BmdwFdas.js new file mode 100644 index 0000000..ccd59a2 --- /dev/null +++ b/public/admin/build/assets/mn.es-BmdwFdas.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["Даваа","Мягмар","Лхавга","Пүрэв","Баасан","Бямба","Ням"],n=["1-р сар","2-р сар","3-р сар","4-р сар","5-р сар","6-р сар","7-р сар","8-р сар","9-р сар","10-р сар","11-р сар","12-р сар"],o="Жилүүд",l="Жил",M="Сар",e="Долоо хоног",s="Өдөр",c="Өнөөдөр",a="Тэмдэглэлгүй",d="Бүх өдөр",y="Устгах",r="Шинэ тэмдэглэл",m="dddd D MMMM YYYY",Y={weekDays:t,months:n,years:o,year:"Жил",month:"Сар",week:e,day:s,today:c,noEvent:a,allDay:d,deleteEvent:y,createEvent:r,dateFormat:m};export{d as allDay,r as createEvent,m as dateFormat,s as day,Y as default,y as deleteEvent,M as month,n as months,a as noEvent,c as today,e as week,t as weekDays,l as year,o as years}; diff --git a/public/admin/build/assets/mobile-dark-logo-COPgCide.svg b/public/admin/build/assets/mobile-dark-logo-COPgCide.svg new file mode 100644 index 0000000..1c24c93 --- /dev/null +++ b/public/admin/build/assets/mobile-dark-logo-COPgCide.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/admin/build/assets/mobile-light-logo-CjoobCkl.svg b/public/admin/build/assets/mobile-light-logo-CjoobCkl.svg new file mode 100644 index 0000000..3d0761d --- /dev/null +++ b/public/admin/build/assets/mobile-light-logo-CjoobCkl.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/admin/build/assets/nl.es-CFxVMCWy.js b/public/admin/build/assets/nl.es-CFxVMCWy.js new file mode 100644 index 0000000..ee578b8 --- /dev/null +++ b/public/admin/build/assets/nl.es-CFxVMCWy.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const a=["Maandag","Dinsdag","Woensdag","Donderdag","Vrijdag","Zaterdag","Zondag"],e=["Januari","Februari","Maart","April","Mei","Juni","Juli","Augustus","September","Oktober","November","December"],n="Jaren",t="Jaar",o="Maand",s="Week",l="Dag",r="Vandaag",d="Geen afspraken",c="Hele dag",g="Verwijderen",i="Nieuwe afspraak aanmaken",u="dddd D MMMM YYYY",M={weekDays:a,months:e,years:n,year:t,month:o,week:s,day:"Dag",today:r,noEvent:d,allDay:c,deleteEvent:g,createEvent:i,dateFormat:u};export{c as allDay,i as createEvent,u as dateFormat,l as day,M as default,g as deleteEvent,o as month,e as months,d as noEvent,r as today,s as week,a as weekDays,t as year,n as years}; diff --git a/public/admin/build/assets/no.es-qRcuRMwl.js b/public/admin/build/assets/no.es-qRcuRMwl.js new file mode 100644 index 0000000..3dd927f --- /dev/null +++ b/public/admin/build/assets/no.es-qRcuRMwl.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag","Søndag"],n=["Januar","Februar","Mars","April","Mai","Juni","Juli","August","September","Oktober","November","Desember"],a="Velg år",l="År",t="Måned",y="Uke",M="Dag",s="Idag",o="Ingen hendelse",d="Hele dagen",r="Ta bort",c="Ny hendelse",g="dddd, D. MMMM YYYY",u={weekDays:e,months:n,years:a,year:"År",month:t,week:"Uke",day:"Dag",today:s,noEvent:o,allDay:d,deleteEvent:r,createEvent:c,dateFormat:g};export{d as allDay,c as createEvent,g as dateFormat,M as day,u as default,r as deleteEvent,t as month,n as months,o as noEvent,s as today,y as week,e as weekDays,l as year,a as years}; diff --git a/public/admin/build/assets/notes-BdIILF0F.svg b/public/admin/build/assets/notes-BdIILF0F.svg new file mode 100644 index 0000000..d27d66c --- /dev/null +++ b/public/admin/build/assets/notes-BdIILF0F.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/organizations-aUdNng-O.svg b/public/admin/build/assets/organizations-aUdNng-O.svg new file mode 100644 index 0000000..a41bd86 --- /dev/null +++ b/public/admin/build/assets/organizations-aUdNng-O.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/pipedrive-Dk_Mji0J.svg b/public/admin/build/assets/pipedrive-Dk_Mji0J.svg new file mode 100644 index 0000000..fc9d411 --- /dev/null +++ b/public/admin/build/assets/pipedrive-Dk_Mji0J.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/pl.es-BQ3a0ltx.js b/public/admin/build/assets/pl.es-BQ3a0ltx.js new file mode 100644 index 0000000..04805ec --- /dev/null +++ b/public/admin/build/assets/pl.es-BQ3a0ltx.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota","Niedziela"],t=["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],a="Lata",k="Rok",o="Miesiąc",i="Tydzień",n="Dzień",s="Dzisiaj",c="Brak wydarzeń",d="Cały dzień",r="Usuń",z="Utwórz wydarzenie",y="dddd, D MMMM YYYY",w={weekDays:e,months:t,years:a,year:"Rok",month:o,week:i,day:n,today:s,noEvent:c,allDay:d,deleteEvent:r,createEvent:z,dateFormat:y};export{d as allDay,z as createEvent,y as dateFormat,n as day,w as default,r as deleteEvent,o as month,t as months,c as noEvent,s as today,i as week,e as weekDays,k as year,a as years}; diff --git a/public/admin/build/assets/plans-DIcOmGwv.svg b/public/admin/build/assets/plans-DIcOmGwv.svg new file mode 100644 index 0000000..669e469 --- /dev/null +++ b/public/admin/build/assets/plans-DIcOmGwv.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/products-zDh6UFtz.svg b/public/admin/build/assets/products-zDh6UFtz.svg new file mode 100644 index 0000000..6c1fab3 --- /dev/null +++ b/public/admin/build/assets/products-zDh6UFtz.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/pt-br.es-U9bVHsgs.js b/public/admin/build/assets/pt-br.es-U9bVHsgs.js new file mode 100644 index 0000000..335ddad --- /dev/null +++ b/public/admin/build/assets/pt-br.es-U9bVHsgs.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado","Domingo"],o=["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],t="Anos",m="Ano",u="Mês",a="Semana",v="Dia",n="Hoje",r="Sem eventos",s="Dia inteiro",i="Remover",c="Criar um evento",d="dddd D MMMM YYYY",y={weekDays:e,months:o,years:t,year:"Ano",month:"Mês",week:a,day:"Dia",today:n,noEvent:r,allDay:s,deleteEvent:i,createEvent:c,dateFormat:d};export{s as allDay,c as createEvent,d as dateFormat,v as day,y as default,i as deleteEvent,u as month,o as months,r as noEvent,n as today,a as week,e as weekDays,m as year,t as years}; diff --git a/public/admin/build/assets/pt-pt.es-U9bVHsgs.js b/public/admin/build/assets/pt-pt.es-U9bVHsgs.js new file mode 100644 index 0000000..335ddad --- /dev/null +++ b/public/admin/build/assets/pt-pt.es-U9bVHsgs.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado","Domingo"],o=["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],t="Anos",m="Ano",u="Mês",a="Semana",v="Dia",n="Hoje",r="Sem eventos",s="Dia inteiro",i="Remover",c="Criar um evento",d="dddd D MMMM YYYY",y={weekDays:e,months:o,years:t,year:"Ano",month:"Mês",week:a,day:"Dia",today:n,noEvent:r,allDay:s,deleteEvent:i,createEvent:c,dateFormat:d};export{s as allDay,c as createEvent,d as dateFormat,v as day,y as default,i as deleteEvent,u as month,o as months,r as noEvent,n as today,a as week,e as weekDays,m as year,t as years}; diff --git a/public/admin/build/assets/quotes-0H7ZCVr6.svg b/public/admin/build/assets/quotes-0H7ZCVr6.svg new file mode 100644 index 0000000..db5d86d --- /dev/null +++ b/public/admin/build/assets/quotes-0H7ZCVr6.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/ro.es-ldjMjt3l.js b/public/admin/build/assets/ro.es-ldjMjt3l.js new file mode 100644 index 0000000..eccb34b --- /dev/null +++ b/public/admin/build/assets/ro.es-ldjMjt3l.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Luni","Marți","Miercuri","Joi","Vineri","Sâmbăta","Duminică"],t=["Ianuarie","Februarie","Martie","Aprilie","Mai","Iunie","Iulie","August","Septembrie","Octombrie","Noiembrie","Decembrie"],u="Ani",d="An",n="Lună",i="Săptămână",m="Zi",y="Azi",a="Nici o interacțiune",o="Toată ziua",r="Șterge",c="Adaugă un eveniment",s="dddd D MMMM YYYY",M={weekDays:e,months:t,years:"Ani",year:"An",month:n,week:i,day:"Zi",today:"Azi",noEvent:a,allDay:o,deleteEvent:r,createEvent:c,dateFormat:s};export{o as allDay,c as createEvent,s as dateFormat,m as day,M as default,r as deleteEvent,n as month,t as months,a as noEvent,y as today,i as week,e as weekDays,d as year,u as years}; diff --git a/public/admin/build/assets/ru.es-CS-QKsYL.js b/public/admin/build/assets/ru.es-CS-QKsYL.js new file mode 100644 index 0000000..dd8cc55 --- /dev/null +++ b/public/admin/build/assets/ru.es-CS-QKsYL.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["Понедельник","Вторник","Среда","Четверг","Пятница","Суббота","Воскресенье"],o=["Пн","Вт","Ср","Чт","Пт","Сб","Вс"],n=["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],e="Годы",Y="Год",s="Месяц",c="Неделя",a="День",d="Сегодня",r="Нет событий",y="Весь день",l="Удалить",D="Создать событие",M="dddd D MMMM YYYY",h={weekDays:t,weekDaysShort:o,months:n,years:e,year:"Год",month:s,week:c,day:a,today:d,noEvent:r,allDay:y,deleteEvent:l,createEvent:D,dateFormat:M};export{y as allDay,D as createEvent,M as dateFormat,a as day,h as default,l as deleteEvent,s as month,n as months,r as noEvent,d as today,c as week,t as weekDays,o as weekDaysShort,Y as year,e as years}; diff --git a/public/admin/build/assets/sk.es-CAe8sEvL.js b/public/admin/build/assets/sk.es-CAe8sEvL.js new file mode 100644 index 0000000..60bbc0e --- /dev/null +++ b/public/admin/build/assets/sk.es-CAe8sEvL.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Pondelok","Utorok","Streda","Štvrtok","Piatok","Sobota","Nedeľa"],t=["Január","Február","Marec","Apríl","Máj","Jún","Júl","August","September","Október","November","December"],o="Roky",y="Rok",n="Mesiac",s="Týždeň",u="Deň",a="Dnes",r="Bez udalosti",c="Celý deň",d="Odstrániť",k="Vytvoriť udalosť",l="dddd D. MMMM YYYY",M={weekDays:e,months:t,years:o,year:"Rok",month:n,week:s,day:"Deň",today:a,noEvent:r,allDay:c,deleteEvent:d,createEvent:k,dateFormat:l};export{c as allDay,k as createEvent,l as dateFormat,u as day,M as default,d as deleteEvent,n as month,t as months,r as noEvent,a as today,s as week,e as weekDays,y as year,o as years}; diff --git a/public/admin/build/assets/sl.es-rdzOxu_X.js b/public/admin/build/assets/sl.es-rdzOxu_X.js new file mode 100644 index 0000000..64ef4a8 --- /dev/null +++ b/public/admin/build/assets/sl.es-rdzOxu_X.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Ponedeljek","Torek","Sreda","Četrtek","Petek","Sobota","Nedelja"],t=["Januar","Februar","Marec","April","Maj","Junij","Julij","Avgust","September","Oktober","November","December"],o="Leta",n="Leto",a="Mesec",s="Teden",y="Dan",d="Danes",r="Ni dogodkov",c="Cel dan",l="Odstrani",k="Ustvari dogodek",v="dddd MMMM D, YYYY",M={weekDays:e,months:t,years:o,year:n,month:a,week:s,day:"Dan",today:d,noEvent:r,allDay:c,deleteEvent:l,createEvent:k,dateFormat:v};export{c as allDay,k as createEvent,v as dateFormat,y as day,M as default,l as deleteEvent,a as month,t as months,r as noEvent,d as today,s as week,e as weekDays,n as year,o as years}; diff --git a/public/admin/build/assets/sq.es-BY5E06HI.js b/public/admin/build/assets/sq.es-BY5E06HI.js new file mode 100644 index 0000000..54fb05f --- /dev/null +++ b/public/admin/build/assets/sq.es-BY5E06HI.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["E Hënë","E Martë","E Mërkurë","E Enjte","E Premte","E Shtunë","E Diel"],e=["Hë","Ma","Mr","Enj","Pr","Sh","D"],n=["Janar","Shkurt","Mars","Prill","Maj","Qershor","Korrik","Gusht","Shtator","Tetor","Nëntor","Dhjetor"],o="Vitet",a="Viti",r="Muaji",s="Java",c="Dita",j="Sot",E="Nuk ka event",d="Tërë ditën",h="Fshijë",i="Krijo një event",M="dddd D MMMM YYYY",k={weekDays:t,weekDaysShort:e,months:n,years:o,year:a,month:r,week:s,day:c,today:"Sot",noEvent:E,allDay:d,deleteEvent:h,createEvent:i,dateFormat:M};export{d as allDay,i as createEvent,M as dateFormat,c as day,k as default,h as deleteEvent,r as month,n as months,E as noEvent,j as today,s as week,t as weekDays,e as weekDaysShort,a as year,o as years}; diff --git a/public/admin/build/assets/sr.es-QPseRVBy.js b/public/admin/build/assets/sr.es-QPseRVBy.js new file mode 100644 index 0000000..7185ce9 --- /dev/null +++ b/public/admin/build/assets/sr.es-QPseRVBy.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const a=["Ponedeljak","Utorak","Sreda","Četvrtak","Petak","Subota","Nedelja"],e=["Januar","Februar","Mart","April","Maj","Jun","Jul","Avgust","Septembar","Oktobar","Novembar","Decembar"],t="Godine",o="Godina",n="Mesec",s="Sedmica",b="Dan",r="Danas",d="Nema događaja",c="Celi dan",l="Obriši",i="Kreiraj događaj",m="dddd D MMMM YYYY",k={weekDays:a,months:e,years:t,year:o,month:n,week:s,day:"Dan",today:r,noEvent:d,allDay:c,deleteEvent:l,createEvent:i,dateFormat:m};export{c as allDay,i as createEvent,m as dateFormat,b as day,k as default,l as deleteEvent,n as month,e as months,d as noEvent,r as today,s as week,a as weekDays,o as year,t as years}; diff --git a/public/admin/build/assets/sv.es-BfPUYcV1.js b/public/admin/build/assets/sv.es-BfPUYcV1.js new file mode 100644 index 0000000..21c320f --- /dev/null +++ b/public/admin/build/assets/sv.es-BfPUYcV1.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const e=["Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag","Söndag"],a=["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],l="År",y="År",n="Månad",t="Vecka",M="Dag",s="Idag",o="Ingen händelse",d="Heldag",r="Ta bort",c="Skapa händelse",g="dddd den D MMMM YYYY",i={weekDays:e,months:a,years:"År",year:"År",month:n,week:t,day:"Dag",today:s,noEvent:o,allDay:d,deleteEvent:r,createEvent:c,dateFormat:g};export{d as allDay,c as createEvent,g as dateFormat,M as day,i as default,r as deleteEvent,n as month,a as months,o as noEvent,s as today,t as week,e as weekDays,y as year,l as years}; diff --git a/public/admin/build/assets/tr.es-Cl6kz_IH.js b/public/admin/build/assets/tr.es-Cl6kz_IH.js new file mode 100644 index 0000000..b7520fd --- /dev/null +++ b/public/admin/build/assets/tr.es-Cl6kz_IH.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi","Pazar"],a=["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],e="Yıllar",m="Yıl",d="Ay",n="Hafta",k="Gün",s="Bugün",o="Etkinlik Yok",c="Tüm gün",y="Sil",l="Etkinlik ekle",r="dddd D MMMM YYYY",i={weekDays:t,months:a,years:e,year:"Yıl",month:"Ay",week:n,day:"Gün",today:s,noEvent:o,allDay:c,deleteEvent:"Sil",createEvent:l,dateFormat:r};export{c as allDay,l as createEvent,r as dateFormat,k as day,i as default,y as deleteEvent,d as month,a as months,o as noEvent,s as today,n as week,t as weekDays,m as year,e as years}; diff --git a/public/admin/build/assets/uk.es-DBEtNQnZ.js b/public/admin/build/assets/uk.es-DBEtNQnZ.js new file mode 100644 index 0000000..2d2e3e8 --- /dev/null +++ b/public/admin/build/assets/uk.es-DBEtNQnZ.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["Понеділок","Вівторок","Середа","Четвер","П'ятниця","Субота","Неділя"],o=["Січень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],n=["Пн","Вт","Ср","Чт","Пт","Сб","Нд"],e="Роки",M="Рік",s="Місяць",c="Тиждень",a="День",d="Сьогодні",y="Немає подій",r="Весь день",k="Видалити",l="Створити подію",D="dddd D MMMM YYYY",Y={weekDays:t,months:o,weekDaysShort:n,years:e,year:"Рік",month:s,week:c,day:a,today:d,noEvent:y,allDay:r,deleteEvent:k,createEvent:l,dateFormat:D};export{r as allDay,l as createEvent,D as dateFormat,a as day,Y as default,k as deleteEvent,s as month,o as months,y as noEvent,d as today,c as week,t as weekDays,n as weekDaysShort,M as year,e as years}; diff --git a/public/admin/build/assets/users-6trD5il1.svg b/public/admin/build/assets/users-6trD5il1.svg new file mode 100644 index 0000000..1a90f23 --- /dev/null +++ b/public/admin/build/assets/users-6trD5il1.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/build/assets/vi.es-Bx2g_1jx.js b/public/admin/build/assets/vi.es-Bx2g_1jx.js new file mode 100644 index 0000000..e3c0958 --- /dev/null +++ b/public/admin/build/assets/vi.es-Bx2g_1jx.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const n=["Thứ hai","Thứ ba","Thứ tư","Thứ năm","Thứ sáu","Thứ bảy","Chủ nhật"],t=["T2","T3","T4","T5","T6","T7","CN"],e=["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],m="Năm",T="Năm nay",h="Tháng",o="Tuần",s="Ngày",a="Hôm nay",c="NKhông có Event",g="Cả ngày",r="Xóa",y="Tạo event",d="dddd MMMM D YYYY",v={weekDays:n,weekDaysShort:t,months:e,years:"Năm",year:T,month:h,week:o,day:s,today:a,noEvent:c,allDay:g,deleteEvent:"Xóa",createEvent:y,dateFormat:d};export{g as allDay,y as createEvent,d as dateFormat,s as day,v as default,r as deleteEvent,h as month,e as months,c as noEvent,a as today,o as week,n as weekDays,t as weekDaysShort,T as year,m as years}; diff --git a/public/admin/build/assets/zh-cn.es-BWMmCH-R.js b/public/admin/build/assets/zh-cn.es-BWMmCH-R.js new file mode 100644 index 0000000..e04e432 --- /dev/null +++ b/public/admin/build/assets/zh-cn.es-BWMmCH-R.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["星期一","星期二","星期三","星期四","星期五","星期六","星期日"],e=["一","二","三","四","五","六","日"],n=["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],c="年",d="本年",y="月",r="周",l="日",h="今日",o="暂无活动",D="整天",k="删除",s="新建活动",a="YYYY MMMM D dddd",m={weekDays:t,weekDaysShort:e,months:n,years:"年",year:"本年",month:"月",week:"周",day:"日",today:"今日",noEvent:o,allDay:"整天",deleteEvent:"删除",createEvent:s,dateFormat:a};export{D as allDay,s as createEvent,a as dateFormat,l as day,m as default,k as deleteEvent,y as month,n as months,o as noEvent,h as today,r as week,t as weekDays,e as weekDaysShort,d as year,c as years}; diff --git a/public/admin/build/assets/zh-hk.es-DFZ0bKvi.js b/public/admin/build/assets/zh-hk.es-DFZ0bKvi.js new file mode 100644 index 0000000..f69c58c --- /dev/null +++ b/public/admin/build/assets/zh-hk.es-DFZ0bKvi.js @@ -0,0 +1,5 @@ +/** + * vue-cal v4.10.2 + * (c) 2025 Antoni Andre + * @license MIT + */const t=["星期一","星期二","星期三","星期四","星期五","星期六","星期日"],e=["一","二","三","四","五","六","日"],o=["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],c="年",d="本年",y="月",r="周",l="日",h="今日",n="暫無活動",k="整天",D="刪除",s="新建活動",a="YYYY MMMM D dddd",m={weekDays:t,weekDaysShort:e,months:o,years:"年",year:"本年",month:"月",week:"周",day:"日",today:"今日",noEvent:n,allDay:"整天",deleteEvent:"刪除",createEvent:s,dateFormat:a};export{k as allDay,s as createEvent,a as dateFormat,l as day,m as default,D as deleteEvent,y as month,o as months,n as noEvent,h as today,r as week,t as weekDays,e as weekDaysShort,d as year,c as years}; diff --git a/public/admin/build/manifest.json b/public/admin/build/manifest.json new file mode 100644 index 0000000..22d90fd --- /dev/null +++ b/public/admin/build/manifest.json @@ -0,0 +1,428 @@ +{ + "node_modules/vue-cal/dist/drag-and-drop.es.js": { + "file": "assets/drag-and-drop.es-CbCm7IHa.js", + "name": "drag-and-drop.es", + "src": "node_modules/vue-cal/dist/drag-and-drop.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/ar.es.js": { + "file": "assets/ar.es-bitOgb9K.js", + "name": "ar.es", + "src": "node_modules/vue-cal/dist/i18n/ar.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/bg.es.js": { + "file": "assets/bg.es-5tp0KQRo.js", + "name": "bg.es", + "src": "node_modules/vue-cal/dist/i18n/bg.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/bn.es.js": { + "file": "assets/bn.es-C_qR0y0-.js", + "name": "bn.es", + "src": "node_modules/vue-cal/dist/i18n/bn.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/bs.es.js": { + "file": "assets/bs.es-DduooXS8.js", + "name": "bs.es", + "src": "node_modules/vue-cal/dist/i18n/bs.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/ca.es.js": { + "file": "assets/ca.es-DYDNDJFJ.js", + "name": "ca.es", + "src": "node_modules/vue-cal/dist/i18n/ca.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/cs.es.js": { + "file": "assets/cs.es-CvnAvfoI.js", + "name": "cs.es", + "src": "node_modules/vue-cal/dist/i18n/cs.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/da.es.js": { + "file": "assets/da.es-C_eJXDX3.js", + "name": "da.es", + "src": "node_modules/vue-cal/dist/i18n/da.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/de.es.js": { + "file": "assets/de.es-Cp-R9E16.js", + "name": "de.es", + "src": "node_modules/vue-cal/dist/i18n/de.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/el.es.js": { + "file": "assets/el.es-BZ44a1Tb.js", + "name": "el.es", + "src": "node_modules/vue-cal/dist/i18n/el.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/en.es.js": { + "file": "assets/en.es-DwVNOttH.js", + "name": "en.es", + "src": "node_modules/vue-cal/dist/i18n/en.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/es.es.js": { + "file": "assets/es.es-DO8PaQ_q.js", + "name": "es.es", + "src": "node_modules/vue-cal/dist/i18n/es.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/et.es.js": { + "file": "assets/et.es-DdRIcUrz.js", + "name": "et.es", + "src": "node_modules/vue-cal/dist/i18n/et.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/fa.es.js": { + "file": "assets/fa.es-XN0av8Tx.js", + "name": "fa.es", + "src": "node_modules/vue-cal/dist/i18n/fa.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/fi.es.js": { + "file": "assets/fi.es-B0Iy6aas.js", + "name": "fi.es", + "src": "node_modules/vue-cal/dist/i18n/fi.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/fr.es.js": { + "file": "assets/fr.es-B7WXlprl.js", + "name": "fr.es", + "src": "node_modules/vue-cal/dist/i18n/fr.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/he.es.js": { + "file": "assets/he.es-BWEk5prp.js", + "name": "he.es", + "src": "node_modules/vue-cal/dist/i18n/he.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/hr.es.js": { + "file": "assets/hr.es-B_iqHQyr.js", + "name": "hr.es", + "src": "node_modules/vue-cal/dist/i18n/hr.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/hu.es.js": { + "file": "assets/hu.es-D_t4Ibxf.js", + "name": "hu.es", + "src": "node_modules/vue-cal/dist/i18n/hu.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/id.es.js": { + "file": "assets/id.es-CzHAK-XV.js", + "name": "id.es", + "src": "node_modules/vue-cal/dist/i18n/id.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/is.es.js": { + "file": "assets/is.es-DChRpym4.js", + "name": "is.es", + "src": "node_modules/vue-cal/dist/i18n/is.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/it.es.js": { + "file": "assets/it.es-CquQA5xx.js", + "name": "it.es", + "src": "node_modules/vue-cal/dist/i18n/it.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/ja.es.js": { + "file": "assets/ja.es-B19gsG6H.js", + "name": "ja.es", + "src": "node_modules/vue-cal/dist/i18n/ja.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/ka.es.js": { + "file": "assets/ka.es-CuOe65jc.js", + "name": "ka.es", + "src": "node_modules/vue-cal/dist/i18n/ka.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/ko.es.js": { + "file": "assets/ko.es-CsMl2USK.js", + "name": "ko.es", + "src": "node_modules/vue-cal/dist/i18n/ko.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/lt.es.js": { + "file": "assets/lt.es-vqgjGyMx.js", + "name": "lt.es", + "src": "node_modules/vue-cal/dist/i18n/lt.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/mn.es.js": { + "file": "assets/mn.es-BmdwFdas.js", + "name": "mn.es", + "src": "node_modules/vue-cal/dist/i18n/mn.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/nl.es.js": { + "file": "assets/nl.es-CFxVMCWy.js", + "name": "nl.es", + "src": "node_modules/vue-cal/dist/i18n/nl.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/no.es.js": { + "file": "assets/no.es-qRcuRMwl.js", + "name": "no.es", + "src": "node_modules/vue-cal/dist/i18n/no.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/pl.es.js": { + "file": "assets/pl.es-BQ3a0ltx.js", + "name": "pl.es", + "src": "node_modules/vue-cal/dist/i18n/pl.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/pt-br.es.js": { + "file": "assets/pt-br.es-U9bVHsgs.js", + "name": "pt-br.es", + "src": "node_modules/vue-cal/dist/i18n/pt-br.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/pt-pt.es.js": { + "file": "assets/pt-pt.es-U9bVHsgs.js", + "name": "pt-pt.es", + "src": "node_modules/vue-cal/dist/i18n/pt-pt.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/ro.es.js": { + "file": "assets/ro.es-ldjMjt3l.js", + "name": "ro.es", + "src": "node_modules/vue-cal/dist/i18n/ro.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/ru.es.js": { + "file": "assets/ru.es-CS-QKsYL.js", + "name": "ru.es", + "src": "node_modules/vue-cal/dist/i18n/ru.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/sk.es.js": { + "file": "assets/sk.es-CAe8sEvL.js", + "name": "sk.es", + "src": "node_modules/vue-cal/dist/i18n/sk.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/sl.es.js": { + "file": "assets/sl.es-rdzOxu_X.js", + "name": "sl.es", + "src": "node_modules/vue-cal/dist/i18n/sl.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/sq.es.js": { + "file": "assets/sq.es-BY5E06HI.js", + "name": "sq.es", + "src": "node_modules/vue-cal/dist/i18n/sq.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/sr.es.js": { + "file": "assets/sr.es-QPseRVBy.js", + "name": "sr.es", + "src": "node_modules/vue-cal/dist/i18n/sr.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/sv.es.js": { + "file": "assets/sv.es-BfPUYcV1.js", + "name": "sv.es", + "src": "node_modules/vue-cal/dist/i18n/sv.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/tr.es.js": { + "file": "assets/tr.es-Cl6kz_IH.js", + "name": "tr.es", + "src": "node_modules/vue-cal/dist/i18n/tr.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/uk.es.js": { + "file": "assets/uk.es-DBEtNQnZ.js", + "name": "uk.es", + "src": "node_modules/vue-cal/dist/i18n/uk.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/vi.es.js": { + "file": "assets/vi.es-Bx2g_1jx.js", + "name": "vi.es", + "src": "node_modules/vue-cal/dist/i18n/vi.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/zh-cn.es.js": { + "file": "assets/zh-cn.es-BWMmCH-R.js", + "name": "zh-cn.es", + "src": "node_modules/vue-cal/dist/i18n/zh-cn.es.js", + "isDynamicEntry": true + }, + "node_modules/vue-cal/dist/i18n/zh-hk.es.js": { + "file": "assets/zh-hk.es-DFZ0bKvi.js", + "name": "zh-hk.es", + "src": "node_modules/vue-cal/dist/i18n/zh-hk.es.js", + "isDynamicEntry": true + }, + "src/Resources/assets/css/app.css": { + "file": "assets/app-oZri8xg_.css", + "src": "src/Resources/assets/css/app.css", + "isEntry": true + }, + "src/Resources/assets/fonts/icomoon.woff": { + "file": "assets/icomoon-BbY1GX-d.woff", + "src": "src/Resources/assets/fonts/icomoon.woff" + }, + "src/Resources/assets/images/corner-clip.svg": { + "file": "assets/corner-clip-DsfWoKt7.svg", + "src": "src/Resources/assets/images/corner-clip.svg" + }, + "src/Resources/assets/images/dark-corner-clip.svg": { + "file": "assets/dark-corner-clip-Deux0RQO.svg", + "src": "src/Resources/assets/images/dark-corner-clip.svg" + }, + "src/Resources/assets/images/dark-logo.svg": { + "file": "assets/dark-logo-Dnw3cpYj.svg", + "src": "src/Resources/assets/images/dark-logo.svg" + }, + "src/Resources/assets/images/empty-placeholders/activities.svg": { + "file": "assets/activities-DpB2XKqX.svg", + "src": "src/Resources/assets/images/empty-placeholders/activities.svg" + }, + "src/Resources/assets/images/empty-placeholders/calls.svg": { + "file": "assets/calls-BU_UfDax.svg", + "src": "src/Resources/assets/images/empty-placeholders/calls.svg" + }, + "src/Resources/assets/images/empty-placeholders/default.svg": { + "file": "assets/default-DBulZ5O_.svg", + "src": "src/Resources/assets/images/empty-placeholders/default.svg" + }, + "src/Resources/assets/images/empty-placeholders/description.svg": { + "file": "assets/description-BwifbkWG.svg", + "src": "src/Resources/assets/images/empty-placeholders/description.svg" + }, + "src/Resources/assets/images/empty-placeholders/emails.svg": { + "file": "assets/emails-D_qEBj20.svg", + "src": "src/Resources/assets/images/empty-placeholders/emails.svg" + }, + "src/Resources/assets/images/empty-placeholders/files.svg": { + "file": "assets/files-Xm-jQ6Fi.svg", + "src": "src/Resources/assets/images/empty-placeholders/files.svg" + }, + "src/Resources/assets/images/empty-placeholders/lunches.svg": { + "file": "assets/lunches-BHeSbGQ3.svg", + "src": "src/Resources/assets/images/empty-placeholders/lunches.svg" + }, + "src/Resources/assets/images/empty-placeholders/meetings.svg": { + "file": "assets/meetings-Cwj897qL.svg", + "src": "src/Resources/assets/images/empty-placeholders/meetings.svg" + }, + "src/Resources/assets/images/empty-placeholders/notes.svg": { + "file": "assets/notes-BdIILF0F.svg", + "src": "src/Resources/assets/images/empty-placeholders/notes.svg" + }, + "src/Resources/assets/images/empty-placeholders/organizations.svg": { + "file": "assets/organizations-aUdNng-O.svg", + "src": "src/Resources/assets/images/empty-placeholders/organizations.svg" + }, + "src/Resources/assets/images/empty-placeholders/pipedrive.svg": { + "file": "assets/pipedrive-Dk_Mji0J.svg", + "src": "src/Resources/assets/images/empty-placeholders/pipedrive.svg" + }, + "src/Resources/assets/images/empty-placeholders/plans.svg": { + "file": "assets/plans-DIcOmGwv.svg", + "src": "src/Resources/assets/images/empty-placeholders/plans.svg" + }, + "src/Resources/assets/images/empty-placeholders/products.svg": { + "file": "assets/products-zDh6UFtz.svg", + "src": "src/Resources/assets/images/empty-placeholders/products.svg" + }, + "src/Resources/assets/images/empty-placeholders/quotes.svg": { + "file": "assets/quotes-0H7ZCVr6.svg", + "src": "src/Resources/assets/images/empty-placeholders/quotes.svg" + }, + "src/Resources/assets/images/empty-placeholders/users.svg": { + "file": "assets/users-6trD5il1.svg", + "src": "src/Resources/assets/images/empty-placeholders/users.svg" + }, + "src/Resources/assets/images/error.svg": { + "file": "assets/error-ClRu8L1K.svg", + "src": "src/Resources/assets/images/error.svg" + }, + "src/Resources/assets/images/favicon.ico": { + "file": "assets/favicon-BtbgZBji.ico", + "src": "src/Resources/assets/images/favicon.ico" + }, + "src/Resources/assets/images/logo.svg": { + "file": "assets/logo-Bjh7YAuF.svg", + "src": "src/Resources/assets/images/logo.svg" + }, + "src/Resources/assets/images/mobile-dark-logo.svg": { + "file": "assets/mobile-dark-logo-COPgCide.svg", + "src": "src/Resources/assets/images/mobile-dark-logo.svg" + }, + "src/Resources/assets/images/mobile-light-logo.svg": { + "file": "assets/mobile-light-logo-CjoobCkl.svg", + "src": "src/Resources/assets/images/mobile-light-logo.svg" + }, + "src/Resources/assets/js/app.js": { + "file": "assets/app-C7K1CsM4.js", + "name": "app", + "src": "src/Resources/assets/js/app.js", + "isEntry": true, + "dynamicImports": [ + "node_modules/vue-cal/dist/i18n/ar.es.js", + "node_modules/vue-cal/dist/i18n/bg.es.js", + "node_modules/vue-cal/dist/i18n/bn.es.js", + "node_modules/vue-cal/dist/i18n/bs.es.js", + "node_modules/vue-cal/dist/i18n/ca.es.js", + "node_modules/vue-cal/dist/i18n/cs.es.js", + "node_modules/vue-cal/dist/i18n/da.es.js", + "node_modules/vue-cal/dist/i18n/de.es.js", + "node_modules/vue-cal/dist/i18n/el.es.js", + "node_modules/vue-cal/dist/i18n/en.es.js", + "node_modules/vue-cal/dist/i18n/es.es.js", + "node_modules/vue-cal/dist/i18n/et.es.js", + "node_modules/vue-cal/dist/i18n/fa.es.js", + "node_modules/vue-cal/dist/i18n/fi.es.js", + "node_modules/vue-cal/dist/i18n/fr.es.js", + "node_modules/vue-cal/dist/i18n/he.es.js", + "node_modules/vue-cal/dist/i18n/hr.es.js", + "node_modules/vue-cal/dist/i18n/hu.es.js", + "node_modules/vue-cal/dist/i18n/id.es.js", + "node_modules/vue-cal/dist/i18n/is.es.js", + "node_modules/vue-cal/dist/i18n/it.es.js", + "node_modules/vue-cal/dist/i18n/ja.es.js", + "node_modules/vue-cal/dist/i18n/ka.es.js", + "node_modules/vue-cal/dist/i18n/ko.es.js", + "node_modules/vue-cal/dist/i18n/lt.es.js", + "node_modules/vue-cal/dist/i18n/mn.es.js", + "node_modules/vue-cal/dist/i18n/nl.es.js", + "node_modules/vue-cal/dist/i18n/no.es.js", + "node_modules/vue-cal/dist/i18n/pl.es.js", + "node_modules/vue-cal/dist/i18n/pt-br.es.js", + "node_modules/vue-cal/dist/i18n/pt-pt.es.js", + "node_modules/vue-cal/dist/i18n/ro.es.js", + "node_modules/vue-cal/dist/i18n/ru.es.js", + "node_modules/vue-cal/dist/i18n/sk.es.js", + "node_modules/vue-cal/dist/i18n/sl.es.js", + "node_modules/vue-cal/dist/i18n/sq.es.js", + "node_modules/vue-cal/dist/i18n/sr.es.js", + "node_modules/vue-cal/dist/i18n/sv.es.js", + "node_modules/vue-cal/dist/i18n/tr.es.js", + "node_modules/vue-cal/dist/i18n/uk.es.js", + "node_modules/vue-cal/dist/i18n/vi.es.js", + "node_modules/vue-cal/dist/i18n/zh-cn.es.js", + "node_modules/vue-cal/dist/i18n/zh-hk.es.js", + "node_modules/vue-cal/dist/drag-and-drop.es.js" + ], + "css": [ + "assets/app-CaTJYsX9.css" + ] + }, + "src/Resources/assets/js/chart.js": { + "file": "assets/chart-DsiofqZ2.js", + "name": "chart", + "src": "src/Resources/assets/js/chart.js", + "isEntry": true + } +} \ No newline at end of file diff --git a/public/build/assets/app-DUr89oQr.js b/public/build/assets/app-DUr89oQr.js new file mode 100644 index 0000000..c4ed3ee --- /dev/null +++ b/public/build/assets/app-DUr89oQr.js @@ -0,0 +1,9 @@ +function wt(e,t){return function(){return e.apply(t,arguments)}}const{toString:Jt}=Object.prototype,{getPrototypeOf:Y}=Object,{iterator:ae,toStringTag:bt}=Symbol,be=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),ie=(e,t)=>{let n=e;const r=[];for(;n!=null&&n!==Object.prototype;){if(r.indexOf(n)!==-1)return!1;if(r.push(n),be(n,t))return!0;n=Y(n)}return!1},vt=(e,t)=>e!=null&&ie(e,t)?e[t]:void 0,Ie=(e=>t=>{const n=Jt.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),B=e=>(e=e.toLowerCase(),t=>Ie(t)===e),ge=e=>t=>typeof t===e,{isArray:K}=Array,X=ge("undefined");function ee(e){return e!==null&&!X(e)&&e.constructor!==null&&!X(e.constructor)&&U(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const Et=B("ArrayBuffer");function Kt(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t=ArrayBuffer.isView(e):t=e&&e.buffer&&Et(e.buffer),t}const Xt=ge("string"),U=ge("function"),Rt=ge("number"),te=e=>e!==null&&typeof e=="object",Gt=e=>e===!0||e===!1,me=e=>{if(!te(e))return!1;const t=Y(e);return(t===null||t===Object.prototype||Y(t)===null)&&!ie(e,bt)&&!ie(e,ae)},Qt=e=>{if(!te(e)||ee(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},Zt=B("Date"),Yt=B("File"),en=e=>!!(e&&typeof e.uri<"u"),tn=e=>e&&typeof e.getParts<"u",nn=B("Blob"),rn=B("FileList"),sn=B("Set"),on=e=>te(e)&&U(e.pipe);function an(){return typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{}}const Ze=an(),Ye=typeof Ze.FormData<"u"?Ze.FormData:void 0,cn=e=>{if(!e)return!1;if(Ye&&e instanceof Ye)return!0;const t=Y(e);if(!t||t===Object.prototype||!U(e.append))return!1;const n=Ie(e);return n==="formdata"||n==="object"&&U(e.toString)&&e.toString()==="[object FormData]"},ln=B("URLSearchParams"),[un,fn,dn,pn]=["ReadableStream","Request","Response","Headers"].map(B),hn=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function ce(e,t,{allOwnKeys:n=!1}={}){if(e===null||typeof e>"u")return;let r,s;if(typeof e!="object"&&(e=[e]),K(e))for(r=0,s=e.length;r0;)if(s=n[r],t===s.toLowerCase())return s;return null}const J=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,Ot=e=>!X(e)&&e!==J;function Fe(...e){const{caseless:t,skipUndefined:n}=Ot(this)&&this||{},r={},s=(o,i)=>{if(i==="__proto__"||i==="constructor"||i==="prototype")return;const c=t&&typeof i=="string"&>(r,i)||i,l=be(r,c)?r[c]:void 0;me(l)&&me(o)?r[c]=Fe(l,o):me(o)?r[c]=Fe({},o):K(o)?r[c]=o.slice():(!n||!X(o))&&(r[c]=o)};for(let o=0,i=e.length;o(ce(t,(s,o)=>{n&&U(s)?Object.defineProperty(e,o,{__proto__:null,value:wt(s,n),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(e,o,{__proto__:null,value:s,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:r}),e),yn=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),wn=(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),Object.defineProperty(e.prototype,"constructor",{__proto__:null,value:e,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(e,"super",{__proto__:null,value:t.prototype}),n&&Object.assign(e.prototype,n)},bn=(e,t,n,r)=>{let s,o,i;const c={};if(t=t||{},e==null)return t;do{for(s=Object.getOwnPropertyNames(e),o=s.length;o-- >0;)i=s[o],(!r||r(i,e,t))&&!c[i]&&(t[i]=e[i],c[i]=!0);e=n!==!1&&Y(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},En=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return r!==-1&&r===n},Rn=e=>{if(!e)return null;if(K(e))return e;let t=e.length;if(!Rt(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},gn=(e=>t=>e&&t instanceof e)(typeof Uint8Array<"u"&&Y(Uint8Array)),On=(e,t)=>{const r=(e&&e[ae]).call(e);let s;for(;(s=r.next())&&!s.done;){const o=s.value;t.call(e,o[0],o[1])}},Sn=(e,t)=>{let n;const r=[];for(;(n=e.exec(t))!==null;)r.push(n);return r},_n=B("HTMLFormElement"),An=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(n,r,s){return r.toUpperCase()+s}),{propertyIsEnumerable:Tn}=Object.prototype,xn=B("RegExp"),St=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};ce(n,(s,o)=>{let i;(i=t(s,o,e))!==!1&&(r[o]=i||s)}),Object.defineProperties(e,r)},Pn=e=>{St(e,(t,n)=>{if(U(e)&&["arguments","caller","callee"].includes(n))return!1;const r=e[n];if(U(r)){if(t.enumerable=!1,"writable"in t){t.writable=!1;return}t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")})}})},Cn=(e,t)=>{const n={},r=s=>{s.forEach(o=>{n[o]=!0})};return K(e)?r(e):r(String(e).split(t)),n},Nn=()=>{},Dn=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function Un(e){return!!(e&&U(e.append)&&e[bt]==="FormData"&&e[ae])}const Ln=e=>{const t=new WeakSet,n=r=>{if(te(r)){if(t.has(r))return;if(ee(r))return r;if(!("toJSON"in r)){t.add(r);let s;if(sn(r)){s=[];for(const o of r){const i=n(o);!X(i)&&s.push(i)}}else s=K(r)?[]:{},ce(r,(o,i)=>{const c=n(o);!X(c)&&(s[i]=c)});return t.delete(r),s}}return r};return n(e)},Fn=B("AsyncFunction"),Bn=e=>e&&(te(e)||U(e))&&U(e.then)&&U(e.catch),_t=((e,t)=>e?setImmediate:t?((n,r)=>(J.addEventListener("message",({source:s,data:o})=>{s===J&&o===n&&r.length&&r.shift()()},!1),s=>{r.push(s),J.postMessage(n,"*")}))(`axios@${Math.random()}`,[]):n=>setTimeout(n))(typeof setImmediate=="function",U(J.postMessage)),jn=typeof queueMicrotask<"u"?queueMicrotask.bind(J):typeof process<"u"&&process.nextTick||_t,At=e=>e!=null&&U(e[ae]),kn=e=>e!=null&&ie(e,ae)&&At(e),a={isArray:K,isArrayBuffer:Et,isBuffer:ee,isFormData:cn,isArrayBufferView:Kt,isString:Xt,isNumber:Rt,isBoolean:Gt,isObject:te,isPlainObject:me,isEmptyObject:Qt,isReadableStream:un,isRequest:fn,isResponse:dn,isHeaders:pn,isUndefined:X,isDate:Zt,isFile:Yt,isReactNativeBlob:en,isReactNative:tn,isBlob:nn,isRegExp:xn,isFunction:U,isStream:on,isURLSearchParams:ln,isTypedArray:gn,isFileList:rn,forEach:ce,merge:Fe,extend:mn,trim:hn,stripBOM:yn,inherits:wn,toFlatObject:bn,kindOf:Ie,kindOfTest:B,endsWith:En,toArray:Rn,forEachEntry:On,matchAll:Sn,isHTMLForm:_n,hasOwnProperty:be,hasOwnProp:be,hasOwnInPrototypeChain:ie,getSafeProp:vt,reduceDescriptors:St,freezeMethods:Pn,toObjectSet:Cn,toCamelCase:An,noop:Nn,toFiniteNumber:Dn,findKey:gt,global:J,isContextDefined:Ot,isSpecCompliantForm:Un,toJSONObject:Ln,isAsyncFn:Fn,isThenable:Bn,setImmediate:_t,asap:jn,isIterable:At,isSafeIterable:kn},In=a.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),qn=e=>{const t={};let n,r,s;return e&&e.split(` +`).forEach(function(i){s=i.indexOf(":"),n=i.substring(0,s).trim().toLowerCase(),r=i.substring(s+1).trim();const c=a.hasOwnProp(t,n);!n||c&&a.hasOwnProp(In,n)||(n==="set-cookie"?c?t[n].push(r):t[n]=[r]:t[n]=c?t[n]+", "+r:r)}),t};function Hn(e){let t=0,n=e.length;for(;tt;){const r=e.charCodeAt(n-1);if(r!==9&&r!==32)break;n-=1}return t===0&&n===e.length?e:e.slice(t,n)}const Mn=new RegExp("[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+","g"),$n=new RegExp("[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+","g");function qe(e,t){return a.isArray(e)?e.map(n=>qe(n,t)):Hn(String(e).replace(t,""))}const zn=e=>qe(e,Mn),Vn=e=>qe(e,$n);function Tt(e){const t=Object.create(null);return a.forEach(e.toJSON(),(n,r)=>{t[r]=Vn(n)}),t}const et=Symbol("internals");function oe(e){return e&&String(e).trim().toLowerCase()}function ye(e){return e===!1||e==null?e:a.isArray(e)?e.map(ye):zn(String(e))}function Wn(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}const Jn=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;function Pe(e){let t=0,n=e.length;for(;tt;){const r=e.charCodeAt(n-1);if(r!==9&&r!==32)break;n-=1}return t===0&&n===e.length?e:e.slice(t,n)}function vn(e){const t=e.length-1;if(t<1||e.charCodeAt(0)!==34||e.charCodeAt(t)!==34)return e;let n="";for(let r=1;r=t))return e;n+=e[r]}return n}function Kn(e){const t=Object.create(null),n=String(e);let r=0,s=!1,o=!1;function i(c){const l=Pe(n.slice(r,c)),d=l.indexOf("=");if(d<1)return;const u=Pe(l.slice(0,d));if(!Jn.test(u))return;const p=u.toLowerCase();if(p==="__proto__"||p==="constructor"||p==="prototype")return;const w=Pe(l.slice(d+1));t[p]=vn(w)}for(let c=0;c/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function Ce(e,t,n,r,s){if(a.isFunction(r))return r.call(this,t,n);if(s&&(t=n),!!a.isString(t)){if(a.isString(r))return t.indexOf(r)!==-1;if(a.isRegExp(r))return r.test(t)}}function Gn(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,n,r)=>n.toUpperCase()+r)}function Qn(e,t){const n=a.toCamelCase(" "+t);["get","set","has"].forEach(r=>{Object.defineProperty(e,r+n,{__proto__:null,value:function(s,o,i){return this[r].call(this,t,s,o,i)},configurable:!0})})}let D=class{constructor(t){t&&this.set(t)}set(t,n,r){const s=this;function o(c,l,d){const u=oe(l);if(!u)return;const p=a.findKey(s,u);(!p||s[p]===void 0||d===!0||d===void 0&&s[p]!==!1)&&(s[p||l]=ye(c))}const i=(c,l)=>a.forEach(c,(d,u)=>o(d,u,l));if(a.isPlainObject(t)||t instanceof this.constructor)i(t,n);else if(a.isString(t)&&(t=t.trim())&&!Xn(t))i(qn(t),n);else if(a.isObject(t)&&a.isSafeIterable(t)){let c=Object.create(null),l,d;for(const u of t){if(!a.isArray(u))throw new TypeError("Object iterator must return a key-value pair");d=u[0],a.hasOwnProp(c,d)?(l=c[d],c[d]=a.isArray(l)?[...l,u[1]]:[l,u[1]]):c[d]=u[1]}i(c,n)}else t!=null&&o(n,t,r);return this}get(t,n){if(t=oe(t),t){const r=a.findKey(this,t);if(r){const s=this[r];if(!n)return s;if(n===!0)return Wn(s);if(a.isFunction(n))return n.call(this,s,r);if(a.isRegExp(n))return n.exec(s);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,n){if(t=oe(t),t){const r=a.findKey(this,t);return!!(r&&this[r]!==void 0&&(!n||Ce(this,this[r],r,n)))}return!1}delete(t,n){const r=this;let s=!1;function o(i){if(i=oe(i),i){const c=a.findKey(r,i);c&&(!n||Ce(r,r[c],c,n))&&(delete r[c],s=!0)}}return a.isArray(t)?t.forEach(o):o(t),s}clear(t){const n=Object.keys(this);let r=n.length,s=!1;for(;r--;){const o=n[r];(!t||Ce(this,this[o],o,t,!0))&&(delete this[o],s=!0)}return s}normalize(t){const n=this,r={};return a.forEach(this,(s,o)=>{const i=a.findKey(r,o);if(i){n[i]=ye(s),delete n[o];return}const c=t?Gn(o):String(o).trim();c!==o&&delete n[o],n[c]=ye(s),r[c]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const n=Object.create(null);return a.forEach(this,(r,s)=>{r!=null&&r!==!1&&(n[s]=t&&a.isArray(r)?r.join(", "):r)}),n}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,n])=>t+": "+n).join(` +`)}getSetCookie(){const t=this.get("set-cookie");return a.isArray(t)?t:t==null||t===!1?[]:[t]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static parseParameters(t){return Kn(t)}static concat(t,...n){const r=new this(t);return n.forEach(s=>r.set(s)),r}static accessor(t){const r=(this[et]=this[et]={accessors:{}}).accessors,s=this.prototype;function o(i){const c=oe(i);r[c]||(Qn(s,i),r[c]=!0)}return a.isArray(t)?t.forEach(o):o(t),this}};D.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);a.reduceDescriptors(D.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(r){this[n]=r}}});a.freezeMethods(D);const Ee="[REDACTED ****]";function Zn(e){if(a.hasOwnProp(e,"toJSON"))return!0;let t=Object.getPrototypeOf(e);for(;t&&t!==Object.prototype;){if(a.hasOwnProp(t,"toJSON"))return!0;t=Object.getPrototypeOf(t)}return!1}function Yn(e,t){const n=new Set(t.map(o=>String(o).toLowerCase())),r=[],s=o=>{if(o===null||typeof o!="object"||a.isBuffer(o))return o;if(r.indexOf(o)!==-1)return;o instanceof D&&(o=o.toJSON()),r.push(o);let i;if(a.isArray(o))i=[],o.forEach((c,l)=>{const d=s(c);a.isUndefined(d)||(i[l]=d)});else{if(!a.isPlainObject(o)&&Zn(o))return r.pop(),o;i=Object.create(null);for(const[c,l]of Object.entries(o)){const d=n.has(c.toLowerCase())?Ee:s(l);a.isUndefined(d)||(i[c]=d)}}return r.pop(),i};return s(e)}function tt(e){try{return String(e)}catch{return""}}function er(e){return e.errors.map(n=>{try{return n&&n.message?tt(n.message):tt(n)}catch{return""}}).filter(Boolean).join("; ")||e.name||"AggregateError"}let h=class xt extends Error{static from(t,n,r,s,o,i){let c=t.message;!c&&a.isArray(t.errors)&&t.errors.length&&(c=er(t));const l=new xt(c,n||t.code,r,s,o);return Object.defineProperty(l,"cause",{__proto__:null,value:t,writable:!0,enumerable:!1,configurable:!0}),l.name=t.name,t.status!=null&&l.status==null&&(l.status=t.status),i&&Object.assign(l,i),l}constructor(t,n,r,s,o){super(t),Object.defineProperty(this,"message",{__proto__:null,value:t,enumerable:!0,writable:!0,configurable:!0}),this.name="AxiosError",this.isAxiosError=!0,n&&(this.code=n),r&&(this.config=r),s&&(this.request=s),o&&(this.response=o,this.status=o.status)}toJSON(){const t=this.config,n=t&&a.hasOwnProp(t,"redact")?t.redact:void 0,r=a.isArray(n)&&n.length>0?Yn(t,n):a.toJSONObject(t);return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:r,code:this.code,status:this.status}}};h.ERR_BAD_OPTION_VALUE="ERR_BAD_OPTION_VALUE";h.ERR_BAD_OPTION="ERR_BAD_OPTION";h.ECONNABORTED="ECONNABORTED";h.ETIMEDOUT="ETIMEDOUT";h.ECONNREFUSED="ECONNREFUSED";h.ERR_NETWORK="ERR_NETWORK";h.ERR_FR_TOO_MANY_REDIRECTS="ERR_FR_TOO_MANY_REDIRECTS";h.ERR_DEPRECATED="ERR_DEPRECATED";h.ERR_BAD_RESPONSE="ERR_BAD_RESPONSE";h.ERR_BAD_REQUEST="ERR_BAD_REQUEST";h.ERR_CANCELED="ERR_CANCELED";h.ERR_NOT_SUPPORT="ERR_NOT_SUPPORT";h.ERR_INVALID_URL="ERR_INVALID_URL";h.ERR_FORM_DATA_DEPTH_EXCEEDED="ERR_FORM_DATA_DEPTH_EXCEEDED";const tr=null,Pt=100;function Be(e){return a.isPlainObject(e)||a.isArray(e)}function Ct(e){return a.endsWith(e,"[]")?e.slice(0,-2):e}function Ne(e,t,n){return e?e.concat(t).map(function(s,o){return s=Ct(s),!n&&o?"["+s+"]":s}).join(n?".":""):t}function nr(e){return a.isArray(e)&&!e.some(Be)}const rr=a.toFlatObject(a,{},null,function(t){return/^is[A-Z]/.test(t)});function Oe(e,t,n){if(!a.isObject(e))throw new TypeError("target must be an object");t=t||new FormData,n=a.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(y,b){return!a.isUndefined(b[y])});const r=n.metaTokens,s=n.visitor||g,o=n.dots,i=n.indexes,c=n.Blob||typeof Blob<"u"&&Blob,l=n.maxDepth===void 0?Pt:n.maxDepth,d=c&&a.isSpecCompliantForm(t),u=[];if(!a.isFunction(s))throw new TypeError("visitor must be a function");function p(f){if(f===null)return"";if(a.isDate(f))return f.toISOString();if(a.isBoolean(f))return f.toString();if(!d&&a.isBlob(f))throw new h("Blob is not supported. Use a Buffer instead.");if(a.isArrayBuffer(f)||a.isTypedArray(f)){if(d&&typeof c=="function")return new c([f]);throw new h("Blob is not supported. Use a Buffer instead.",h.ERR_NOT_SUPPORT)}return f}function w(f){if(f>l)throw new h("Object is too deeply nested ("+f+" levels). Max depth: "+l,h.ERR_FORM_DATA_DEPTH_EXCEEDED)}function R(f,y){if(l===1/0)return JSON.stringify(f);const b=[];return JSON.stringify(f,function(C,A){if(!a.isObject(A))return A;for(;b.length&&b[b.length-1]!==this;)b.pop();return b.push(A),w(y+b.length-1),A})}function g(f,y,b){let O=f;if(a.isReactNative(t)&&a.isReactNativeBlob(f))return t.append(Ne(b,y,o),p(f)),!1;if(f&&!b&&typeof f=="object"){if(a.endsWith(y,"{}"))y=r?y:y.slice(0,-2),f=R(f,1);else if(a.isArray(f)&&nr(f)||(a.isFileList(f)||a.endsWith(y,"[]"))&&(O=a.toArray(f)))return y=Ct(y),O.forEach(function(A,q){!(a.isUndefined(A)||A===null)&&t.append(i===!0?Ne([y],q,o):i===null?y:y+"[]",p(A))}),!1}return Be(f)?!0:(t.append(Ne(b,y,o),p(f)),!1)}const S=Object.assign(rr,{defaultVisitor:g,convertValue:p,isVisitable:Be});function m(f,y,b=0){if(!a.isUndefined(f)){if(w(b),u.indexOf(f)!==-1)throw new Error("Circular reference detected in "+y.join("."));u.push(f),a.forEach(f,function(C,A){(!(a.isUndefined(C)||C===null)&&s.call(t,C,a.isString(A)?A.trim():A,y,S))===!0&&m(C,y?y.concat(A):[A],b+1)}),u.pop()}}if(!a.isObject(e))throw new TypeError("data must be an object");return m(e),t}function nt(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"};return encodeURIComponent(e).replace(/[!'()~]|%20/g,function(r){return t[r]})}function He(e,t){this._pairs=[],e&&Oe(e,this,t)}const Nt=He.prototype;Nt.append=function(t,n){this._pairs.push([t,n])};Nt.toString=function(t){const n=t?r=>t.call(this,r,nt):nt;return this._pairs.map(function(s){return n(s[0])+"="+n(s[1])},"").join("&")};function sr(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function Dt(e,t,n){if(!t)return e;e=e||"";const r=a.isFunction(n)?{serialize:n}:n,s=a.getSafeProp(r,"encode")||sr,o=a.getSafeProp(r,"serialize");let i;if(o?i=o(t,r):i=a.isURLSearchParams(t)?t.toString():new He(t,r).toString(s),i){const c=e.indexOf("#");c!==-1&&(e=e.slice(0,c)),e+=(e.indexOf("?")===-1?"?":"&")+i}return e}class rt{constructor(){this.handlers=[]}use(t,n,r){return this.handlers.push({fulfilled:t,rejected:n,synchronous:r?r.synchronous:!1,runWhen:r?r.runWhen:null}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(t){a.forEach(this.handlers,function(r){r!==null&&t(r)})}}const Me={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1,legacyInterceptorReqResOrdering:!0,advertiseZstdAcceptEncoding:!1,validateStatusUndefinedResolves:!0},or=typeof URLSearchParams<"u"?URLSearchParams:He,ir=typeof FormData<"u"?FormData:null,ar=typeof Blob<"u"?Blob:null,cr={isBrowser:!0,classes:{URLSearchParams:or,FormData:ir,Blob:ar},protocols:["http","https","file","blob","url","data"]},$e=typeof window<"u"&&typeof document<"u",je=typeof navigator=="object"&&navigator||void 0,lr=$e&&(!je||["ReactNative","NativeScript","NS"].indexOf(je.product)<0),ur=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",fr=$e&&window.location.href||"http://localhost",dr=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:$e,hasStandardBrowserEnv:lr,hasStandardBrowserWebWorkerEnv:ur,navigator:je,origin:fr},Symbol.toStringTag,{value:"Module"})),P={...dr,...cr};function pr(e,t){return Oe(e,new P.classes.URLSearchParams,{visitor:function(n,r,s,o){return P.isNode&&a.isBuffer(n)?(this.append(r,n.toString("base64")),!1):o.defaultVisitor.apply(this,arguments)},...t})}const st=Pt;function Ut(e){if(e>st)throw new h("FormData field is too deeply nested ("+e+" levels). Max depth: "+st,h.ERR_FORM_DATA_DEPTH_EXCEEDED)}function hr(e){const t=[],n=/[^.[\]]+|\[([^.[\]]*)]/g;let r;for(;(r=n.exec(e))!==null;)Ut(t.length),t.push(r[0]==="[]"?"":r[1]||r[0]);return t}function mr(e){const t={},n=Object.keys(e);let r;const s=n.length;let o;for(r=0;r=n.length;return i=!i&&a.isArray(s)?s.length:i,l?(a.hasOwnProp(s,i)?s[i]=a.isArray(s[i])?s[i].concat(r):[s[i],r]:s[i]=r,!c):((!a.hasOwnProp(s,i)||!a.isObject(s[i]))&&(s[i]=[]),t(n,r,s[i],o)&&a.isArray(s[i])&&(s[i]=mr(s[i])),!c)}if(a.isFormData(e)&&a.isFunction(e.entries)){const n={};return a.forEachEntry(e,(r,s)=>{t(hr(r),s,n,0)}),n}return null}const Z=(e,t)=>e!=null&&a.hasOwnProp(e,t)?e[t]:void 0;function yr(e,t,n){if(a.isString(e))try{return(t||JSON.parse)(e),a.trim(e)}catch(r){if(r.name!=="SyntaxError")throw r}return(n||JSON.stringify)(e)}const le={transitional:Me,adapter:["xhr","http","fetch"],transformRequest:[function(t,n){const r=n.getContentType()||"",s=r.indexOf("application/json")>-1,o=a.isObject(t);if(o&&a.isHTMLForm(t)&&(t=new FormData(t)),a.isFormData(t))return s?JSON.stringify(Lt(t)):t;if(a.isArrayBuffer(t)||a.isBuffer(t)||a.isStream(t)||a.isFile(t)||a.isBlob(t)||a.isReadableStream(t))return t;if(a.isArrayBufferView(t))return t.buffer;if(a.isURLSearchParams(t))return n.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),t.toString();let c;if(o){const l=Z(this,"formSerializer");if(r.indexOf("application/x-www-form-urlencoded")>-1)return pr(t,l).toString();if((c=a.isFileList(t))||r.indexOf("multipart/form-data")>-1){const d=Z(this,"env"),u=d&&d.FormData;return Oe(c?{"files[]":t}:t,u&&new u,l)}}return o||s?(n.setContentType("application/json",!1),yr(t)):t}],transformResponse:[function(t){const n=Z(this,"transitional")||le.transitional,r=n&&n.forcedJSONParsing,s=Z(this,"responseType"),o=s==="json";if(a.isResponse(t)||a.isReadableStream(t))return t;if(t&&a.isString(t)&&(r&&!s||o)){const c=!(n&&n.silentJSONParsing)&&o;try{return JSON.parse(t,Z(this,"parseReviver"))}catch(l){if(c)throw l.name==="SyntaxError"?h.from(l,h.ERR_BAD_RESPONSE,this,null,Z(this,"response")):l}}return t}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:P.classes.FormData,Blob:P.classes.Blob},validateStatus:function(t){return t>=200&&t<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};a.forEach(["delete","get","head","post","put","patch","query"],e=>{le.headers[e]={}});function De(e,t){const n=this||le,r=t||n,s=D.from(r.headers);let o=r.data;return a.forEach(e,function(c){o=c.call(n,o,s.normalize(),t?t.status:void 0)}),s.normalize(),o}function Ft(e){return!!(e&&e.__CANCEL__)}let ue=class extends h{constructor(t,n,r){super(t??"canceled",h.ERR_CANCELED,n,r),this.name="CanceledError",this.__CANCEL__=!0}};function Bt(e,t,n){const r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new h("Request failed with status code "+n.status,n.status>=400&&n.status<500?h.ERR_BAD_REQUEST:h.ERR_BAD_RESPONSE,n.config,n.request,n))}function wr(e){const t=/^([-+\w]{1,25}):(?:\/\/)?/.exec(e);return t&&t[1]||""}function br(e,t){e=e||10;const n=new Array(e),r=new Array(e);let s=0,o=0,i;return t=t!==void 0?t:1e3,function(l){const d=Date.now(),u=r[o];i||(i=d),n[s]=l,r[s]=d;let p=o,w=0;for(;p!==s;)w+=n[p++],p=p%e;if(s=(s+1)%e,s===o&&(o=(o+1)%e),d-i{n=u,s=null,o&&(clearTimeout(o),o=null),e(...d)};return[(...d)=>{const u=Date.now(),p=u-n;p>=r?i(d,u):(s=d,o||(o=setTimeout(()=>{o=null,i(s)},r-p)))},()=>s&&i(s)]}const Re=(e,t,n=3)=>{let r=0;const s=br(50,250);return Er(o=>{if(!o||typeof o.loaded!="number")return;const i=o.loaded,c=o.lengthComputable?o.total:void 0,l=Math.max(0,c!=null?Math.min(i,c):i),d=Math.max(0,l-r),u=s(d);r=Math.max(r,l);const p={loaded:l,total:c,progress:c?l/c:void 0,bytes:d,rate:u||void 0,estimated:u&&c?(c-l)/u:void 0,event:o,lengthComputable:c!=null,[t?"download":"upload"]:!0};e(p)},n)},ot=(e,t)=>{const n=e!=null;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},it=(e,t=a.asap)=>(...n)=>t(()=>e(...n)),Rr=P.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,P.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(P.origin),P.navigator&&/(msie|trident)/i.test(P.navigator.userAgent)):()=>!0,gr=P.hasStandardBrowserEnv?{write(e,t,n,r,s,o,i){if(typeof document>"u")return;const c=[`${e}=${encodeURIComponent(t)}`];a.isNumber(n)&&c.push(`expires=${new Date(n).toUTCString()}`),a.isString(r)&&c.push(`path=${r}`),a.isString(s)&&c.push(`domain=${s}`),o===!0&&c.push("secure"),a.isString(i)&&c.push(`SameSite=${i}`),document.cookie=c.join("; ")},read(e){if(typeof document>"u")return null;const t=document.cookie.split(";");for(let n=0;n0&&e.charCodeAt(n-1)===47;)n--;return e.slice(0,n)+"/"+t.replace(/^\/+/,"")}const _r=/^https?:(?!\/\/)/i,Ar=/[\t\n\r]/g;function Tr(e){let t=0;for(;t`${n}${r}${Ee}`)}function Cr(e){const t=e.replace(/^(https?:\/{0,2})[^/?#]*@/i,`$1${Ee}@`),n=t.indexOf("#"),s=(n===-1?t:t.slice(0,n)).replace(/([?&][^=&#]*=)[^&#]*/g,`$1${Ee}`);return n===-1?s:`${s}#${Pr(t.slice(n+1))}`}function at(e,t){if(typeof e=="string"){const n=xr(e);if(_r.test(n))throw new h(`Invalid URL ${JSON.stringify(Cr(n))}: missing "//" after protocol`,h.ERR_INVALID_URL,t)}}function jt(e,t,n,r){at(t,r);let s=!Or(t);return e&&(s||n===!1)?(at(e,r),Sr(e,t)):t}const ct=e=>e instanceof D?{...e}:e,Nr=e=>Object.getOwnPropertySymbols&&Object.getOwnPropertyDescriptor?Object.keys(e).concat(Object.getOwnPropertySymbols(e).filter(t=>Object.getOwnPropertyDescriptor(e,t).enumerable)):Object.keys(e);function G(e,t){e=e||{},t=t||{};const n=Object.create(null);Object.defineProperty(n,"hasOwnProperty",{__proto__:null,value:Object.prototype.hasOwnProperty,enumerable:!1,writable:!0,configurable:!0});function r(u,p,w,R){return a.isPlainObject(u)&&a.isPlainObject(p)?a.merge.call({caseless:R},u,p):a.isPlainObject(p)?a.merge({},p):a.isArray(p)?p.slice():p}function s(u,p,w,R){if(a.isUndefined(p)){if(!a.isUndefined(u))return r(void 0,u,w,R)}else return r(u,p,w,R)}function o(u,p){if(!a.isUndefined(p))return r(void 0,p)}function i(u,p){if(a.isUndefined(p)){if(!a.isUndefined(u))return r(void 0,u)}else return r(void 0,p)}function c(u){const p=a.hasOwnProp(t,"transitional")?t.transitional:void 0;if(!a.isUndefined(p))if(a.isPlainObject(p)){if(a.hasOwnProp(p,u))return p[u]}else return;const w=a.hasOwnProp(e,"transitional")?e.transitional:void 0;if(a.isPlainObject(w)&&a.hasOwnProp(w,u))return w[u]}function l(u,p,w){if(a.hasOwnProp(t,w))return r(u,p);if(a.hasOwnProp(e,w))return r(void 0,u)}const d={url:o,method:o,data:o,baseURL:i,transformRequest:i,transformResponse:i,paramsSerializer:i,timeout:i,timeoutMessage:i,withCredentials:i,withXSRFToken:i,adapter:i,responseType:i,xsrfCookieName:i,xsrfHeaderName:i,onUploadProgress:i,onDownloadProgress:i,decompress:i,maxContentLength:i,maxBodyLength:i,beforeRedirect:i,transport:i,httpAgent:i,httpsAgent:i,cancelToken:i,socketPath:i,allowedSocketPaths:i,responseEncoding:i,validateStatus:l,headers:(u,p,w)=>s(ct(u),ct(p),w,!0)};return a.forEach(Nr({...e,...t}),function(p){if(p==="__proto__"||p==="constructor"||p==="prototype")return;const w=a.hasOwnProp(d,p)?d[p]:s,R=a.hasOwnProp(e,p)?e[p]:void 0,g=a.hasOwnProp(t,p)?t[p]:void 0,S=w(R,g,p);a.isUndefined(S)&&w!==l||(n[p]=S)}),a.hasOwnProp(t,"validateStatus")&&a.isUndefined(t.validateStatus)&&c("validateStatusUndefinedResolves")===!1&&(a.hasOwnProp(e,"validateStatus")?n.validateStatus=r(void 0,e.validateStatus):delete n.validateStatus),n}const Dr=["content-type","content-length"];function Ur(e,t,n){if(n!=="content-only"){e.set(t);return}Object.entries(t||{}).forEach(([r,s])=>{Dr.includes(r.toLowerCase())&&e.set(r,s)})}const Lr=e=>encodeURIComponent(e).replace(/%([0-9A-F]{2})/gi,(t,n)=>String.fromCharCode(parseInt(n,16)));function kt(e){const t=G({},e),n=w=>a.hasOwnProp(t,w)?t[w]:void 0,r=n("data");let s=n("withXSRFToken");const o=n("xsrfHeaderName"),i=n("xsrfCookieName");let c=n("headers");const l=n("auth"),d=n("baseURL"),u=n("allowAbsoluteUrls"),p=n("url");if(t.headers=c=D.from(c),t.url=Dt(jt(d,p,u,t),n("params"),n("paramsSerializer")),l){const w=a.getSafeProp(l,"username")||"",R=a.getSafeProp(l,"password")||"";try{c.set("Authorization","Basic "+btoa(w+":"+(R?Lr(R):"")))}catch(g){throw h.from(g,h.ERR_BAD_OPTION_VALUE,e)}}if(a.isFormData(r)&&(P.hasStandardBrowserEnv||P.hasStandardBrowserWebWorkerEnv||a.isReactNative(r)?c.setContentType(void 0):a.isFunction(r.getHeaders)&&Ur(c,r.getHeaders(),n("formDataHeaderPolicy"))),P.hasStandardBrowserEnv&&(a.isFunction(s)&&(s=s(t)),s===!0||s==null&&Rr(t.url))){const R=o&&i&&gr.read(i);R&&c.set(o,R)}return t}const Fr=typeof XMLHttpRequest<"u",Br=Fr&&function(e){return new Promise(function(n,r){const s=kt(e);let o=s.data;const i=D.from(s.headers).normalize();let{responseType:c,onUploadProgress:l,onDownloadProgress:d}=s,u,p,w,R,g;function S(){R&&R(),g&&g(),s.cancelToken&&s.cancelToken.unsubscribe(u),s.signal&&s.signal.removeEventListener("abort",u)}let m=new XMLHttpRequest;m.open(s.method.toUpperCase(),s.url,!0),m.timeout=s.timeout;function f(){if(!m)return;const b=D.from("getAllResponseHeaders"in m&&m.getAllResponseHeaders()),C={data:!c||c==="text"||c==="json"?m.responseText:m.response,status:m.status,statusText:m.statusText,headers:b,config:e,request:m};Bt(function(q){n(q),S()},function(q){r(q),S()},C),m=null}"onloadend"in m?m.onloadend=f:m.onreadystatechange=function(){!m||m.readyState!==4||m.status===0&&!(m.responseURL&&m.responseURL.startsWith("file:"))||setTimeout(f)},m.onabort=function(){m&&(r(new h("Request aborted",h.ECONNABORTED,e,m)),S(),m=null)},m.onerror=function(O){const C=O&&O.message?O.message:"Network Error",A=new h(C,h.ERR_NETWORK,e,m);A.event=O||null,r(A),S(),m=null},m.ontimeout=function(){let O=s.timeout?"timeout of "+s.timeout+"ms exceeded":"timeout exceeded";const C=s.transitional||Me;s.timeoutErrorMessage&&(O=s.timeoutErrorMessage),r(new h(O,C.clarifyTimeoutError?h.ETIMEDOUT:h.ECONNABORTED,e,m)),S(),m=null},o===void 0&&i.setContentType(null),"setRequestHeader"in m&&a.forEach(Tt(i),function(O,C){m.setRequestHeader(C,O)}),a.isUndefined(s.withCredentials)||(m.withCredentials=!!s.withCredentials),c&&c!=="json"&&(m.responseType=s.responseType),d&&([w,g]=Re(d,!0),m.addEventListener("progress",w)),l&&m.upload&&([p,R]=Re(l),m.upload.addEventListener("progress",p),m.upload.addEventListener("loadend",R)),(s.cancelToken||s.signal)&&(u=b=>{m&&(r(!b||b.type?new ue(null,e,m):b),m.abort(),S(),m=null)},s.cancelToken&&s.cancelToken.subscribe(u),s.signal&&(s.signal.aborted?u():s.signal.addEventListener("abort",u)));const y=wr(s.url);if(y&&!P.protocols.includes(y)){r(new h("Unsupported protocol "+y+":",h.ERR_BAD_REQUEST,e)),S();return}m.send(o||null)})},jr=(e,t)=>{if(e=e?e.filter(Boolean):[],!t&&!e.length)return;const n=new AbortController;let r=!1;const s=function(l){if(!r){r=!0,i();const d=l instanceof Error?l:this.reason;n.abort(d instanceof h?d:new ue(d instanceof Error?d.message:d))}};let o=t&&setTimeout(()=>{o=null,s(new h(`timeout of ${t}ms exceeded`,h.ETIMEDOUT))},t);const i=()=>{e&&(o&&clearTimeout(o),o=null,e.forEach(l=>{l.unsubscribe?l.unsubscribe(s):l.removeEventListener("abort",s)}),e=null)};e.forEach(l=>{if(!r){if(l.aborted){s.call(l);return}l.addEventListener("abort",s,{once:!0})}});const{signal:c}=n;return c.unsubscribe=()=>a.asap(i),c},kr=function*(e,t){let n=e.byteLength;if(n{const s=Ir(e,t);let o=0,i,c=l=>{i||(i=!0,r&&r(l))};return new ReadableStream({async pull(l){try{const{done:d,value:u}=await s.next();if(d){c(),l.close();return}let p=u.byteLength;if(n){let w=o+=p;n(w)}l.enqueue(new Uint8Array(u))}catch(d){throw c(d),d}},cancel(l){return c(l),s.return()}},{highWaterMark:2})},ut=e=>e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102,It=(e,t,n)=>t+2e<=57?e-48:(e&223)-55,Hr=e=>e>=65&&e<=90||e>=97&&e<=122||e>=48&&e<=57||e===43||e===47||e===45||e===95,Mr=e=>e===9||e===10||e===12||e===13||e===32,$r=e=>{const t=Math.floor(e/4),n=e%4;return t*3+(n===2?1:n===3?2:0)},zr=e=>{const t=e.length;let n=0;return t>0&&e.charCodeAt(t-1)===61&&(n++,t>1&&e.charCodeAt(t-2)===61&&n++),Math.floor((t-n)*3/4)},Vr=e=>{const t=e.length;let n=0,r=0,s=!1;for(let o=0;o0){s=!0;continue}n++}}return s||r>2||r>0&&(n+r)%4!==0||n%4===1?zr(e):$r(n)},Wr=(e,t)=>{if(!e||typeof e!="string"||!e.startsWith("data:"))return 0;const n=e.indexOf(",");if(n<0)return 0;const r=e.slice(5,n),s=e.slice(n+1);if(/;base64/i.test(r))return t(s);let i=0;for(let c=0,l=s.length;c=55296&&d<=56319&&c+1=56320&&u<=57343?(i+=4,c++):i+=3}else i+=3}return i};function Jr(e){const t=typeof e=="string"?e.indexOf("#"):-1;return Wr(t===-1?e:e.slice(0,t),Vr)}const ze="1.19.0",dt=64*1024,{isFunction:he}=a,vr=e=>encodeURIComponent(e).replace(/%([0-9A-F]{2})/gi,(t,n)=>String.fromCharCode(parseInt(n,16))),pt=e=>{if(!a.isString(e))return e;try{return decodeURIComponent(e)}catch{return e}},ht=(e,...t)=>{try{return!!e(...t)}catch{return!1}},Kr=e=>{const t=e.indexOf("://");let n=e;return t!==-1&&(n=n.slice(t+3)),n.includes("@")||n.includes(":")},Xr=e=>{const t=a.global!==void 0&&a.global!==null?a.global:globalThis,{ReadableStream:n,TextEncoder:r}=t;e=a.merge.call({skipUndefined:!0},{Request:t.Request,Response:t.Response},e);const{fetch:s,Request:o,Response:i}=e,c=s?he(s):typeof fetch=="function",l=he(o),d=he(i);if(!c)return!1;const u=c&&he(n),p=c&&(typeof r=="function"?(f=>y=>f.encode(y))(new r):async f=>new Uint8Array(await new o(f).arrayBuffer())),w=l&&u&&ht(()=>{let f=!1;const y=new o(P.origin,{body:new n,method:"POST",get duplex(){return f=!0,"half"}}),b=y.headers.has("Content-Type");return y.body!=null&&y.body.cancel(),f&&!b}),R=d&&u&&ht(()=>a.isReadableStream(new i("").body)),g={stream:R&&(f=>f.body)};c&&["text","arrayBuffer","blob","formData","stream"].forEach(f=>{!g[f]&&(g[f]=(y,b)=>{let O=y&&y[f];if(O)return O.call(y);throw new h(`Response type '${f}' is not supported`,h.ERR_NOT_SUPPORT,b)})});const S=async f=>{if(f==null)return 0;if(a.isBlob(f))return f.size;if(a.isSpecCompliantForm(f))return(await new o(P.origin,{method:"POST",body:f}).arrayBuffer()).byteLength;if(a.isArrayBufferView(f)||a.isArrayBuffer(f))return f.byteLength;if(a.isURLSearchParams(f)&&(f=f+""),a.isString(f))return(await p(f)).byteLength},m=async(f,y)=>{const b=a.toFiniteNumber(f.getContentLength());return b??S(y)};return async f=>{let{url:y,method:b,data:O,signal:C,cancelToken:A,timeout:q,onDownloadProgress:_e,onUploadProgress:Ae,responseType:H,headers:M,withCredentials:fe="same-origin",fetchOptions:We,maxContentLength:j,maxBodyLength:de}=kt(f);const ne=a.isNumber(j)&&j>-1,Te=a.isNumber(de)&&de>-1,zt=_=>a.hasOwnProp(f,_)?f[_]:void 0;let Je=s||fetch;H=H?(H+"").toLowerCase():"text";let $=jr([C,A&&A.toAbortSignal()],q),x=null;const V=$&&$.unsubscribe&&(()=>{$.unsubscribe()});let Q,re=null;const ve=()=>new h("Request body larger than maxBodyLength limit",h.ERR_BAD_REQUEST,f,x);try{let _;const F=zt("auth");if(F){const E=a.getSafeProp(F,"username")||"",L=a.getSafeProp(F,"password")||"";_={username:E,password:L}}if(Kr(y)){const E=new URL(y,P.origin);if(!_&&(E.username||E.password)){const L=pt(E.username),z=pt(E.password);_={username:L,password:z}}(E.username||E.password)&&(E.username="",E.password="",y=E.href)}if(_&&(M.delete("authorization"),M.set("Authorization","Basic "+btoa(vr((_.username||"")+":"+(_.password||""))))),ne&&typeof y=="string"&&y.startsWith("data:")&&Jr(y)>j)throw new h("maxContentLength size of "+j+" exceeded",h.ERR_BAD_RESPONSE,f,x);if(Te&&b!=="get"&&b!=="head"){const E=await S(O);if(typeof E=="number"&&isFinite(E)&&(Q=E,E>de))throw ve()}const pe=Te&&(a.isReadableStream(O)||a.isStream(O)),Ke=(E,L,z)=>lt(E,dt,W=>{if(Te&&W>de)throw re=ve();L&&L(W)},z);if(w&&b!=="get"&&b!=="head"&&(Ae||pe)){if(Q=Q??await m(M,O),Q!==0||pe){let E=new o(y,{method:"POST",body:O,duplex:"half"}),L;if(a.isFormData(O)&&(L=E.headers.get("content-type"))&&M.setContentType(L),E.body){const[z,W]=Ae&&ot(Q,Re(it(Ae)))||[];O=Ke(E.body,z,W)}}}else if(pe&&!l&&u&&b!=="get"&&b!=="head")O=Ke(O);else if(pe&&l&&!w&&b!=="get"&&b!=="head")throw new h("Stream request bodies are not supported by the current fetch implementation",h.ERR_NOT_SUPPORT,f,x);a.isString(fe)||(fe=fe?"include":"omit");const Vt=l&&"credentials"in o.prototype;if(a.isFormData(O)){const E=M.getContentType();E&&/^multipart\/form-data/i.test(E)&&!/boundary=/i.test(E)&&M.delete("content-type")}M.set("User-Agent","axios/"+ze,!1);const Xe={...We,signal:$,method:b.toUpperCase(),headers:Tt(M.normalize()),body:O,duplex:"half",credentials:Vt?fe:void 0};x=l&&new o(y,Xe);let k=await(l?Je(x,We):Je(y,Xe));const Ge=D.from(k.headers);if(ne){const E=a.toFiniteNumber(Ge.getContentLength());if(E!=null&&E>j)throw new h("maxContentLength size of "+j+" exceeded",h.ERR_BAD_RESPONSE,f,x)}const xe=R&&(H==="stream"||H==="response");if(R&&k.body&&(_e||ne||xe&&V)){const E={};["status","statusText","headers"].forEach(se=>{E[se]=k[se]});const L=a.toFiniteNumber(Ge.getContentLength()),[z,W]=_e&&ot(L,Re(it(_e),!0))||[];let Qe=0;const Wt=se=>{if(ne&&(Qe=se,Qe>j))throw new h("maxContentLength size of "+j+" exceeded",h.ERR_BAD_RESPONSE,f,x);z&&z(se)};k=new i(lt(k.body,dt,Wt,()=>{W&&W(),V&&V()}),E)}H=H||"text";let I=await g[a.findKey(g,H)||"text"](k,f);if(ne&&!R&&!xe){let E;if(I!=null&&(typeof I.byteLength=="number"?E=I.byteLength:typeof I.size=="number"?E=I.size:typeof I=="string"&&(E=typeof r=="function"?new r().encode(I).byteLength:I.length)),typeof E=="number"&&E>j)throw new h("maxContentLength size of "+j+" exceeded",h.ERR_BAD_RESPONSE,f,x)}return!xe&&V&&V(),await new Promise((E,L)=>{Bt(E,L,{data:I,headers:D.from(k.headers),status:k.status,statusText:k.statusText,config:f,request:x})})}catch(_){if(V&&V(),$&&$.aborted&&$.reason instanceof h){const F=$.reason;throw F.config=f,x&&(F.request=x),_!==F&&Object.defineProperty(F,"cause",{__proto__:null,value:_,writable:!0,enumerable:!1,configurable:!0}),F}if(re)throw x&&!re.request&&(re.request=x),re;if(_ instanceof h)throw x&&!_.request&&(_.request=x),_;if(_&&_.name==="TypeError"&&/Load failed|fetch/i.test(_.message)){const F=new h("Network Error",h.ERR_NETWORK,f,x,_&&_.response);throw Object.defineProperty(F,"cause",{__proto__:null,value:_.cause||_,writable:!0,enumerable:!1,configurable:!0}),F}throw h.from(_,_&&_.code,f,x,_&&_.response)}}},Gr=new Map,qt=e=>{let t=e&&e.env||{};const{fetch:n,Request:r,Response:s}=t,o=[r,s,n];let i=o.length,c=i,l,d,u=Gr;for(;c--;)l=o[c],d=u.get(l),d===void 0&&u.set(l,d=c?new Map:Xr(t)),u=d;return d};qt();const Ve={http:tr,xhr:Br,fetch:{get:qt}};a.forEach(Ve,(e,t)=>{if(e){try{Object.defineProperty(e,"name",{__proto__:null,value:t})}catch{}Object.defineProperty(e,"adapterName",{__proto__:null,value:t})}});const mt=e=>`- ${e}`,Qr=e=>a.isFunction(e)||e===null||e===!1;function Zr(e,t){e=a.isArray(e)?e:[e];const{length:n}=e;let r,s;const o={};for(let i=0;i`adapter ${l} `+(d===!1?"is not supported by the environment":"is not available in the build"));let c=n?i.length>1?`since : +`+i.map(mt).join(` +`):" "+mt(i[0]):"as no adapter specified";throw new h("There is no suitable adapter to dispatch the request "+c,h.ERR_NOT_SUPPORT)}return s}const Ht={getAdapter:Zr,adapters:Ve};function Ue(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new ue(null,e)}function Le(e){return Ue(e),e.headers=D.from(e.headers),e.data=De.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Ht.getAdapter(e.adapter||le.adapter,e)(e).then(function(r){Ue(e),e.response=r;try{r.data=De.call(e,e.transformResponse,r)}finally{delete e.response}return r.headers=D.from(r.headers),r},function(r){if(!Ft(r)&&(Ue(e),r&&r.response)){e.response=r.response;try{r.response.data=De.call(e,e.transformResponse,r.response)}finally{delete e.response}r.response.headers=D.from(r.response.headers)}return Promise.reject(r)})}const Se={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{Se[e]=function(r){return typeof r===e||"a"+(t<1?"n ":" ")+e}});const yt={};Se.transitional=function(t,n,r){function s(o,i){return"[Axios v"+ze+"] Transitional option '"+o+"'"+i+(r?". "+r:"")}return(o,i,c)=>{if(t===!1)throw new h(s(i," has been removed"+(n?" in "+n:"")),h.ERR_DEPRECATED);return n&&!yt[i]&&(yt[i]=!0,console.warn(s(i," has been deprecated since v"+n+" and will be removed in the near future"))),t?t(o,i,c):!0}};Se.spelling=function(t){return(n,r)=>(console.warn(`${r} is likely a misspelling of ${t}`),!0)};function Yr(e,t,n){if(typeof e!="object"||e===null)throw new h("options must be an object",h.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let s=r.length;for(;s-- >0;){const o=r[s],i=Object.prototype.hasOwnProperty.call(t,o)?t[o]:void 0;if(i){const c=e[o],l=c===void 0||i(c,o,e);if(l!==!0)throw new h("option "+o+" must be "+l,h.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new h("Unknown option "+o,h.ERR_BAD_OPTION)}}const we={assertOptions:Yr,validators:Se},N=we.validators;let v=class{constructor(t){this.defaults=t||{},this.interceptors={request:new rt,response:new rt}}async request(t,n){try{return await this._request(t,n)}catch(r){if(r instanceof Error){let s={};Error.captureStackTrace?Error.captureStackTrace(s):s=new Error;const o=(()=>{if(!s.stack)return"";const i=s.stack.indexOf(` +`);return i===-1?"":s.stack.slice(i+1)})();try{if(!r.stack)r.stack=o;else if(o){const i=o.indexOf(` +`),c=i===-1?-1:o.indexOf(` +`,i+1),l=c===-1?"":o.slice(c+1);String(r.stack).endsWith(l)||(r.stack+=` +`+o)}}catch{}}throw r}}_request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=G(this.defaults,n);const{transitional:r,paramsSerializer:s,headers:o}=n;r!==void 0&&we.assertOptions(r,{silentJSONParsing:N.transitional(N.boolean),forcedJSONParsing:N.transitional(N.boolean),clarifyTimeoutError:N.transitional(N.boolean),legacyInterceptorReqResOrdering:N.transitional(N.boolean),advertiseZstdAcceptEncoding:N.transitional(N.boolean),validateStatusUndefinedResolves:N.transitional(N.boolean)},!1),s!=null&&(a.isFunction(s)?n.paramsSerializer={serialize:s}:we.assertOptions(s,{encode:N.function,serialize:N.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),we.assertOptions(n,{baseUrl:N.spelling("baseURL"),withXsrfToken:N.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let i=o&&a.merge(o.common,o[n.method]);o&&a.forEach(["delete","get","head","post","put","patch","query","common"],g=>{delete o[g]}),n.headers=D.concat(i,o);const c=[];let l=!0;this.interceptors.request.forEach(function(S){if(typeof S.runWhen=="function"&&S.runWhen(n)===!1)return;l=l&&S.synchronous;const m=n.transitional||Me;m&&m.legacyInterceptorReqResOrdering?c.unshift(S.fulfilled,S.rejected):c.push(S.fulfilled,S.rejected)});const d=[];this.interceptors.response.forEach(function(S){d.push(S.fulfilled,S.rejected)});let u,p=0,w;if(!l){const g=[Le.bind(this),void 0];for(g.unshift(...c),g.push(...d),w=g.length,u=Promise.resolve(n);pLe.call(this,R)))}catch(f){u=Promise.reject(f)}break}}if(!u)try{u=Le.call(this,R)}catch(g){u=Promise.reject(g)}for(p=0,w=d.length;p{if(!r._listeners)return;let o=r._listeners.length;for(;o-- >0;)r._listeners[o](s);r._listeners=null}),this.promise.then=s=>{let o;const i=new Promise(c=>{r.subscribe(c),o=c}).then(s);return i.cancel=function(){r.unsubscribe(o)},i},t(function(o,i,c){r.reason||(r.reason=new ue(o,i,c),n(r.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexOf(t);n!==-1&&this._listeners.splice(n,1)}toAbortSignal(){const t=new AbortController,n=r=>{t.abort(r)};return this.subscribe(n),t.signal.unsubscribe=()=>this.unsubscribe(n),t.signal}static source(){let t;return{token:new Mt(function(s){t=s}),cancel:t}}};function ts(e){return function(n){return e.apply(null,n)}}function ns(e){return a.isObject(e)&&e.isAxiosError===!0}const ke={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerReturnsAnUnknownError:520,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(ke).forEach(([e,t])=>{ke[t]=e});function $t(e){const t=new v(e),n=wt(v.prototype.request,t);return a.extend(n,v.prototype,t,{allOwnKeys:!0}),a.extend(n,t,null,{allOwnKeys:!0}),n.create=function(s){return $t(G(e,s))},n}const T=$t(le);T.Axios=v;T.CanceledError=ue;T.CancelToken=es;T.isCancel=Ft;T.VERSION=ze;T.toFormData=Oe;T.AxiosError=h;T.Cancel=T.CanceledError;T.all=function(t){return Promise.all(t)};T.spread=ts;T.isAxiosError=ns;T.mergeConfig=G;T.AxiosHeaders=D;T.formToJSON=e=>Lt(a.isHTMLForm(e)?new FormData(e):e);T.getAdapter=Ht.getAdapter;T.HttpStatusCode=ke;T.default=T;const{Axios:is,AxiosError:as,CanceledError:cs,isCancel:ls,CancelToken:us,VERSION:fs,all:ds,Cancel:ps,isAxiosError:hs,spread:ms,toFormData:ys,AxiosHeaders:ws,HttpStatusCode:bs,formToJSON:Es,getAdapter:Rs,mergeConfig:gs,create:Os}=T;window.axios=T;window.axios.defaults.headers.common["X-Requested-With"]="XMLHttpRequest"; diff --git a/public/build/assets/app-l0sNRNKZ.js b/public/build/assets/app-l0sNRNKZ.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/public/build/assets/app-l0sNRNKZ.js @@ -0,0 +1 @@ + diff --git a/public/build/manifest.json b/public/build/manifest.json new file mode 100644 index 0000000..fc2b768 --- /dev/null +++ b/public/build/manifest.json @@ -0,0 +1,14 @@ +{ + "resources/css/app.css": { + "file": "assets/app-l0sNRNKZ.js", + "name": "app", + "src": "resources/css/app.css", + "isEntry": true + }, + "resources/js/app.js": { + "file": "assets/app-DUr89oQr.js", + "name": "app", + "src": "resources/js/app.js", + "isEntry": true + } +} \ No newline at end of file diff --git a/public/fonts/.gitignore b/public/fonts/.gitignore new file mode 100755 index 0000000..ada16db --- /dev/null +++ b/public/fonts/.gitignore @@ -0,0 +1,12 @@ +* +!Hind-Bold.ttf +!Hind-Regular.ttf +!NotoSansBengali-Bold.ttf +!NotoSansBengali-Regular.ttf +!NotoSansJP-Bold.ttf +!NotoSansJP-Regular.ttf +!NotoSansSC-Bold.ttf +!NotoSansSC-Regular.ttf +!NotoSansSinhala-Bold.ttf +!NotoSansSinhala-Regular.ttf +!.gitignore diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..947d989 --- /dev/null +++ b/public/index.php @@ -0,0 +1,17 @@ +handleRequest(Request::capture()); diff --git a/public/installer/build/assets/app-C9-BdERk.css b/public/installer/build/assets/app-C9-BdERk.css new file mode 100644 index 0000000..f5d8fb9 --- /dev/null +++ b/public/installer/build/assets/app-C9-BdERk.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:16px;padding-left:16px}@media(min-width:1920px){.container{max-width:1920px}}::-webkit-scrollbar{width:12px}::-webkit-scrollbar-track{background:#f1f1f1}::-webkit-scrollbar-thumb{background:#888;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#555}*{scrollbar-width:thin;scrollbar-color:#888 #f1f1f1}::-moz-selection{background-color:#0044f233}::selection{background-color:#0044f233}body{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1));font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}button:disabled{cursor:not-allowed;opacity:.5}button:disabled:hover{cursor:not-allowed;opacity:.5}.icon-tick:before{content:""}.icon-cross-large:before{content:""}.icon-left-arrow:before{content:""}.icon-right-arrow:before{content:""}.icon-up-arrow:before{content:""}.icon-down-arrow:before{content:""}p{font-size:14px;line-height:17px!important}input,textarea,select{outline:2px solid transparent;outline-offset:2px}.primary-button{display:flex;cursor:pointer;place-content:center;align-items:center;-moz-column-gap:.25rem;column-gap:.25rem;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(14 144 217 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(14 144 217 / var(--tw-bg-opacity, 1));padding:.375rem .75rem;font-weight:600;--tw-text-opacity: 1;color:rgb(249 250 251 / var(--tw-text-opacity, 1));transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.primary-button:hover{opacity:.9}.primary-button:focus{opacity:.9}.custom-select{-webkit-appearance:none;-moz-appearance:none;background:transparent;background-image:url("data:image/svg+xml;utf8,");background-repeat:no-repeat;background-position-x:calc(100% - 10px);background-position-y:50%}.dark .custom-select{background-image:url("data:image/svg+xml;utf8,")}[dir=rtl] .custom-select{background-position-x:calc(100% - (100% - 10px))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.m-auto{margin:auto}.mb-2\.5{margin-bottom:.625rem}.mb-6{margin-bottom:1.5rem}.mt-1{margin-top:.25rem}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-5{height:1.25rem}.h-72{height:18rem}.h-8{height:2rem}.h-\[100px\]{height:100px}.h-\[100vh\]{height:100vh}.h-\[388px\]{height:388px}.h-\[484px\]{height:484px}.h-\[486px\]{height:486px}.h-full{height:100%}.w-5{width:1.25rem}.w-8{width:2rem}.w-full{width:100%}.max-w-\[362px\]{max-width:362px}.max-w-\[568px\]{max-width:568px}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-12{gap:3rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.place-self-end{place-self:end}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border,.border-\[1px\]{border-width:1px}.border-b{border-bottom-width:1px}.border-brandColor{--tw-border-opacity: 1;border-color:rgb(14 144 217 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-600{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))}.bg-brandColor{--tw-bg-opacity: 1;background-color:rgb(14 144 217 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.p-1\.5{padding:.375rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-inter{font-family:Inter}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.italic{font-style:italic}.leading-6{line-height:1.5rem}.\!text-black{--tw-text-opacity: 1 !important;color:rgb(0 0 0 / var(--tw-text-opacity, 1))!important}.text-brandColor{--tw-text-opacity: 1;color:rgb(14 144 217 / var(--tw-text-opacity, 1))}.text-gray-50{--tw-text-opacity: 1;color:rgb(249 250 251 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@font-face{font-family:icomoon;src:url(icomoon-BFKbaEOo.eot?w2trdd);src:url(icomoon-BFKbaEOo.eot?w2trdd#iefix) format("embedded-opentype"),url(icomoon-BgFe9HIr.ttf?w2trdd) format("truetype"),url(icomoon-B5aogpno.woff?w2trdd) format("woff"),url(icomoon-r7Xq6sz-.svg?w2trdd#icomoon) format("svg");font-weight:400;font-style:normal;font-display:block}[class^=icon-],[class*=" icon-"]{font-family:icomoon!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.hover\:border-gray-400:hover{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.hover\:border-red-600:hover{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))}.hover\:opacity-90:hover{opacity:.9}.peer:checked~.peer-checked\:text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.dark\:bg-gray-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.\[\&\>\*\]\:flex>*{display:flex}.\[\&\>\*\]\:items-center>*{align-items:center}.\[\&\>\*\]\:gap-1>*{gap:.25rem} diff --git a/public/installer/build/assets/favicon-BtbgZBji.ico b/public/installer/build/assets/favicon-BtbgZBji.ico new file mode 100644 index 0000000000000000000000000000000000000000..8fd69159384857b582cc8e427588faba37285153 GIT binary patch literal 1150 zcmd7OOG_J37{>888ZRK33WDIul#sCtsU*P*QtC=*loqloF1(de)Lj?qR?xkpg)Utc zKSu?@uizMks03O;XbSx+dd4__jl7pPb)>x7flw+Hh|T?SZl%@e5^KVj8R^|5+o` z=iBITQuzgxpsce--v&~D&xe6$hnrp${clDR9|lwYcb6SV`Gt4m@!aX>*mL$pwDDm}-wpzAJ^Lc@o*HLlPbNm6&K_)d+pHCk4TC4zUn!v$E_+VDj4K4MUnZs|U&W{L z8{QYj*R{1{+IkK8jnKI+tGpD+rNLhBXKl`{sm9oT5YySc2=A<`mTKx=%~(PSF0XbM zU5H{1KcM;BS=E;&EW*_&($9rKtnw}C>p!T*5oV!t>z`PCsKzq#$c3L;E48(WvFM&Y yw6C6Z9nGN?N$5Pc^r&sQY$n$uX1}}5E@}!bW?!+=_~&|&6|>bHv%S-5M!6fYLjX_! literal 0 HcmV?d00001 diff --git a/public/installer/build/assets/icomoon-B5aogpno.woff b/public/installer/build/assets/icomoon-B5aogpno.woff new file mode 100644 index 0000000000000000000000000000000000000000..de686bfad7e679c400633a8265ad4c3107fa87bf GIT binary patch literal 35708 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 literal 0 HcmV?d00001 diff --git a/public/installer/build/assets/icomoon-BFKbaEOo.eot b/public/installer/build/assets/icomoon-BFKbaEOo.eot new file mode 100644 index 0000000000000000000000000000000000000000..b75ff17cf0765555f34728195708dc64b8e9b065 GIT binary patch literal 35796 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 literal 0 HcmV?d00001 diff --git a/public/installer/build/assets/icomoon-BgFe9HIr.ttf b/public/installer/build/assets/icomoon-BgFe9HIr.ttf new file mode 100644 index 0000000000000000000000000000000000000000..cbf5418e807918507d11873ed6c0dfdd560e0c06 GIT binary patch literal 35632 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 OJBwMD#+T&E + + + + + \ No newline at end of file diff --git a/public/installer/build/assets/spinner-CWS6ej98.svg b/public/installer/build/assets/spinner-CWS6ej98.svg new file mode 100644 index 0000000..993d164 --- /dev/null +++ b/public/installer/build/assets/spinner-CWS6ej98.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/public/installer/build/manifest.json b/public/installer/build/manifest.json new file mode 100644 index 0000000..8a29d3b --- /dev/null +++ b/public/installer/build/manifest.json @@ -0,0 +1,41 @@ +{ + "src/Resources/assets/css/app.css": { + "file": "assets/app-C9-BdERk.css", + "src": "src/Resources/assets/css/app.css", + "isEntry": true + }, + "src/Resources/assets/fonts/icomoon.eot": { + "file": "assets/icomoon-BFKbaEOo.eot", + "src": "src/Resources/assets/fonts/icomoon.eot" + }, + "src/Resources/assets/fonts/icomoon.svg": { + "file": "assets/icomoon-r7Xq6sz-.svg", + "src": "src/Resources/assets/fonts/icomoon.svg" + }, + "src/Resources/assets/fonts/icomoon.ttf": { + "file": "assets/icomoon-BgFe9HIr.ttf", + "src": "src/Resources/assets/fonts/icomoon.ttf" + }, + "src/Resources/assets/fonts/icomoon.woff": { + "file": "assets/icomoon-B5aogpno.woff", + "src": "src/Resources/assets/fonts/icomoon.woff" + }, + "src/Resources/assets/images/favicon.ico": { + "file": "assets/favicon-BtbgZBji.ico", + "src": "src/Resources/assets/images/favicon.ico" + }, + "src/Resources/assets/images/krayin-logo.svg": { + "file": "assets/krayin-logo-D5SO44Ys.svg", + "src": "src/Resources/assets/images/krayin-logo.svg" + }, + "src/Resources/assets/images/spinner.svg": { + "file": "assets/spinner-CWS6ej98.svg", + "src": "src/Resources/assets/images/spinner.svg" + }, + "src/Resources/assets/js/app.js": { + "file": "assets/app-B7GgTQD5.js", + "name": "app", + "src": "src/Resources/assets/js/app.js", + "isEntry": true + } +} \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/web.config b/public/web.config new file mode 100644 index 0000000..323482f --- /dev/null +++ b/public/web.config @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/webform/build/assets/app-CWW-WGL4.css b/public/webform/build/assets/app-CWW-WGL4.css new file mode 100644 index 0000000..2858cf0 --- /dev/null +++ b/public/webform/build/assets/app-CWW-WGL4.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:16px;padding-left:16px}@media(min-width:1920px){.container{max-width:1920px}}.icon-cross-large:before{content:""}.primary-button{display:flex;cursor:pointer;place-content:center;align-items:center;-moz-column-gap:.25rem;column-gap:.25rem;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(14 144 217 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(14 144 217 / var(--tw-bg-opacity, 1));padding:.375rem .75rem;font-weight:600;--tw-text-opacity: 1;color:rgb(249 250 251 / var(--tw-text-opacity, 1));transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.primary-button:hover{opacity:.9}.primary-button:focus{opacity:.9}::-webkit-scrollbar{width:12px}::-webkit-scrollbar-track{background:#f1f1f1}::-webkit-scrollbar-thumb{background:#888;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#555}*{scrollbar-width:thin;scrollbar-color:#888 #f1f1f1}::-moz-selection{background-color:#0044f233}::selection{background-color:#0044f233}body{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1));font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}button:disabled{cursor:not-allowed;opacity:.5}button:disabled:hover{cursor:not-allowed;opacity:.5}html.dark [class^=icon-],html.dark [class*=" icon-"]{color:#d1d5db}p{font-size:14px;line-height:17px!important}input,textarea,select{outline:2px solid transparent;outline-offset:2px}.required:after{--tw-content: "*";content:var(--tw-content)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-5{bottom:1.25rem}.left-1\/2{left:50%}.z-\[10003\]{z-index:10003}.\!mb-2{margin-bottom:.5rem!important}.mb-1\.5{margin-bottom:.375rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-5{height:1.25rem}.h-\[100vh\]{height:100vh}.h-full{height:100%}.w-5{width:1.25rem}.w-9{width:2.25rem}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.min-w-\[300px\]{min-width:300px}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-end{justify-items:end}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-5{gap:1.25rem}.overflow-hidden{overflow:hidden}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.\!border-red-600{--tw-border-opacity: 1 !important;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))!important}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.p-1\.5{padding:.375rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.italic{font-style:italic}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow-\[0px_10px_20px_0px_rgba\(0\,0\,0\,0\.12\)\]{--tw-shadow: 0px 10px 20px 0px rgba(0,0,0,.12);--tw-shadow-colored: 0px 10px 20px 0px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-300{transition-duration:.3s}.ease-\[cubic-bezier\(\.4\,0\,\.2\,1\)\]{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}@font-face{font-family:icomoon;src:url(icomoon-CO37Y6g-.woff?w2trdd) format("woff");font-weight:400;font-style:normal;font-display:block}[class^=icon-],[class*=" icon-"]{font-family:icomoon!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:top-0\.5:after{content:var(--tw-content);top:.125rem}.after\:h-4:after{content:var(--tw-content);height:1rem}.after\:w-4:after{content:var(--tw-content);width:1rem}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:border:after{content:var(--tw-content);border-width:1px}.after\:border-gray-300:after{content:var(--tw-content);--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content: "";content:var(--tw-content)}.focus-within\:border-gray-400:focus-within{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.hover\:border-gray-400:hover{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.hover\:border-red-600:hover{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))}.focus\:border-gray-400:focus{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.peer:checked~.peer-checked\:bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.peer:checked~.peer-checked\:text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.peer:checked~.peer-checked\:after\:border-white:after{content:var(--tw-content);--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.peer:focus~.peer-focus\:outline-none{outline:2px solid transparent;outline-offset:2px}.peer:focus~.peer-focus\:ring-blue-300{--tw-ring-opacity: 1;--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity, 1))}.dark\:border-gray-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.dark\:bg-gray-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.dark\:text-gray-300:is(.dark *){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-white:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:file\:bg-gray-800:is(.dark *)::file-selector-button{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:file\:dark\:text-white:is(.dark *):is(.dark *)::file-selector-button{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:after\:border-white:is(.dark *):after{content:var(--tw-content);--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.dark\:after\:bg-white:is(.dark *):after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.dark\:hover\:border-gray-400:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.dark\:focus\:border-gray-400:focus:is(.dark *){--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.peer:checked~.dark\:peer-checked\:bg-gray-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.ltr\:translate-y-0:where([dir=ltr],[dir=ltr] *){--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.ltr\:translate-y-full:where([dir=ltr],[dir=ltr] *){--tw-translate-y: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.ltr\:pl-4:where([dir=ltr],[dir=ltr] *){padding-left:1rem}.after\:ltr\:left-0\.5:where([dir=ltr],[dir=ltr] *):after{content:var(--tw-content);left:.125rem}.peer:checked~.peer-checked\:after\:ltr\:translate-x-full:where([dir=ltr],[dir=ltr] *):after{content:var(--tw-content);--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:-translate-y-0:where([dir=rtl],[dir=rtl] *){--tw-translate-y: -0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:-translate-y-full:where([dir=rtl],[dir=rtl] *){--tw-translate-y: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:pr-4:where([dir=rtl],[dir=rtl] *){padding-right:1rem}.after\:rtl\:right-0\.5:where([dir=rtl],[dir=rtl] *):after{content:var(--tw-content);right:.125rem}.peer:checked~.peer-checked\:after\:rtl\:-translate-x-full:where([dir=rtl],[dir=rtl] *):after{content:var(--tw-content);--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))} diff --git a/public/webform/build/assets/app-DsP8OK1c.css b/public/webform/build/assets/app-DsP8OK1c.css new file mode 100644 index 0000000..1ad7830 --- /dev/null +++ b/public/webform/build/assets/app-DsP8OK1c.css @@ -0,0 +1 @@ +.flatpickr-calendar{background:transparent;overflow:hidden;max-height:0;opacity:0;visibility:hidden;text-align:center;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:315px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,.08);box-shadow:1px 0 #e6e6e6,-1px 0 #e6e6e6,0 1px #e6e6e6,0 -1px #e6e6e6,0 3px 13px #00000014}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;visibility:visible;overflow:visible;max-height:640px}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1);animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px)}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.hasWeeks{width:auto}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.showTimeInput.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:"";height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.rightMost:after{left:auto;right:22px}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-month{background:transparent;color:#000000e6;fill:#000000e6;height:28px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden}.flatpickr-prev-month,.flatpickr-next-month{text-decoration:none;cursor:pointer;position:absolute;top:0;line-height:16px;height:28px;padding:10px calc(3.57% - 1.5px);z-index:3}.flatpickr-prev-month i,.flatpickr-next-month i{position:relative}.flatpickr-prev-month.flatpickr-prev-month,.flatpickr-next-month.flatpickr-prev-month{left:0}.flatpickr-prev-month.flatpickr-next-month,.flatpickr-next-month.flatpickr-next-month{right:0}.flatpickr-prev-month:hover,.flatpickr-next-month:hover{color:#959ea9}.flatpickr-prev-month:hover svg,.flatpickr-next-month:hover svg{fill:#f64747}.flatpickr-prev-month svg,.flatpickr-next-month svg{width:14px}.flatpickr-prev-month svg path,.flatpickr-next-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,.05);-webkit-box-sizing:border-box;box-sizing:border-box}.numInputWrapper span:hover{background:#0000001a}.numInputWrapper span:active{background:#0003}.numInputWrapper span:after{display:block;content:"";position:absolute;top:33%}.numInputWrapper span.arrowUp{top:0;border-bottom:0}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,.6)}.numInputWrapper span.arrowDown{top:50%}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,.6)}.numInputWrapper span svg{width:inherit;height:auto}.numInputWrapper span svg path{fill:#00000080}.numInputWrapper:hover{background:#0000000d}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{font-size:135%;line-height:inherit;font-weight:300;color:inherit;position:absolute;width:75%;left:12.5%;padding:6.16px 0 0;line-height:1;height:28px;display:inline-block;text-align:center;-webkit-transform:translate3d(0px,0px,0px);transform:translateZ(0)}.flatpickr-current-month.slideLeft{-webkit-transform:translate3d(-100%,0px,0px);transform:translate3d(-100%,0,0);-webkit-animation:fpFadeOut .4s ease,fpSlideLeft .4s cubic-bezier(.23,1,.32,1);animation:fpFadeOut .4s ease,fpSlideLeft .4s cubic-bezier(.23,1,.32,1)}.flatpickr-current-month.slideLeftNew{-webkit-transform:translate3d(100%,0px,0px);transform:translate3d(100%,0,0);-webkit-animation:fpFadeIn .4s ease,fpSlideLeftNew .4s cubic-bezier(.23,1,.32,1);animation:fpFadeIn .4s ease,fpSlideLeftNew .4s cubic-bezier(.23,1,.32,1)}.flatpickr-current-month.slideRight{-webkit-transform:translate3d(100%,0px,0px);transform:translate3d(100%,0,0);-webkit-animation:fpFadeOut .4s ease,fpSlideRight .4s cubic-bezier(.23,1,.32,1);animation:fpFadeOut .4s ease,fpSlideRight .4s cubic-bezier(.23,1,.32,1)}.flatpickr-current-month.slideRightNew{-webkit-transform:translate3d(0,0,0px);transform:translateZ(0);-webkit-animation:fpFadeIn .4s ease,fpSlideRightNew .4s cubic-bezier(.23,1,.32,1);animation:fpFadeIn .4s ease,fpSlideRightNew .4s cubic-bezier(.23,1,.32,1)}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0}.flatpickr-current-month span.cur-month:hover{background:#0000000d}.flatpickr-current-month .numInputWrapper{width:6ch;width:7ch�;display:inline-block}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:#000000e6}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:#000000e6}.flatpickr-current-month input.cur-year{background:transparent;-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;cursor:default;padding:0 0 0 .5ch;margin:0;display:inline-block;font-size:inherit;font-family:inherit;font-weight:300;line-height:inherit;height:initial;border:0;border-radius:0;vertical-align:initial}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{font-size:100%;color:#00000080;background:transparent;pointer-events:none}.flatpickr-weekdays{background:transparent;text-align:center;overflow:hidden;width:315px;display:-webkit-flex;display:-ms-flexbox;display:-webkit-box;display:flex;-webkit-align-items:center;-ms-flex-align:center;-webkit-box-align:center;align-items:center;height:28px}span.flatpickr-weekday{cursor:default;font-size:90%;background:transparent;color:#0000008a;line-height:1;margin:0;text-align:center;display:block;-webkit-flex:1;-ms-flex:1;-webkit-box-flex:1;flex:1;font-weight:bolder}.dayContainer,.flatpickr-weeks{padding:1px 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-flex;display:-ms-flexbox;display:-webkit-box;display:flex;width:315px}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:315px;min-width:315px;max-width:315px;-webkit-box-sizing:border-box;box-sizing:border-box;display:inline-block;display:-ms-flexbox;display:-webkit-flex;display:-webkit-box;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-wrap:wrap;-ms-flex-pack:justify;-webkit-justify-content:space-around;justify-content:space-around;-webkit-transform:translate3d(0px,0px,0px);transform:translateZ(0);opacity:1}.flatpickr-calendar.animate .dayContainer.slideLeft{-webkit-animation:fpFadeOut .4s cubic-bezier(.23,1,.32,1),fpSlideLeft .4s cubic-bezier(.23,1,.32,1);animation:fpFadeOut .4s cubic-bezier(.23,1,.32,1),fpSlideLeft .4s cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.animate .dayContainer.slideLeft,.flatpickr-calendar.animate .dayContainer.slideLeftNew{-webkit-transform:translate3d(-100%,0px,0px);transform:translate3d(-100%,0,0)}.flatpickr-calendar.animate .dayContainer.slideLeftNew{-webkit-animation:fpFadeIn .4s cubic-bezier(.23,1,.32,1),fpSlideLeft .4s cubic-bezier(.23,1,.32,1);animation:fpFadeIn .4s cubic-bezier(.23,1,.32,1),fpSlideLeft .4s cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.animate .dayContainer.slideRight{-webkit-animation:fpFadeOut .4s cubic-bezier(.23,1,.32,1),fpSlideRight .4s cubic-bezier(.23,1,.32,1);animation:fpFadeOut .4s cubic-bezier(.23,1,.32,1),fpSlideRight .4s cubic-bezier(.23,1,.32,1);-webkit-transform:translate3d(100%,0px,0px);transform:translate3d(100%,0,0)}.flatpickr-calendar.animate .dayContainer.slideRightNew{-webkit-animation:fpFadeIn .4s cubic-bezier(.23,1,.32,1),fpSlideRightNew .4s cubic-bezier(.23,1,.32,1);animation:fpFadeIn .4s cubic-bezier(.23,1,.32,1),fpSlideRightNew .4s cubic-bezier(.23,1,.32,1)}.flatpickr-day{background:none;border:1px solid transparent;border-radius:150px;-webkit-box-sizing:border-box;box-sizing:border-box;color:#393939;cursor:pointer;font-weight:400;width:14.2857143%;-webkit-flex-basis:14.2857143%;-ms-flex-preferred-size:14.2857143%;flex-basis:14.2857143%;max-width:40px;height:40px;line-height:40px;margin:0;display:inline-block;position:relative;-webkit-justify-content:center;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center;text-align:center}.flatpickr-day.inRange,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.today.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day:hover,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.nextMonthDay:hover,.flatpickr-day:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.nextMonthDay:focus{cursor:pointer;outline:0;background:#e6e6e6;border-color:#e6e6e6}.flatpickr-day.today{border-color:#959ea9}.flatpickr-day.today:hover,.flatpickr-day.today:focus{border-color:#959ea9;background:#959ea9;color:#fff}.flatpickr-day.selected,.flatpickr-day.startRange,.flatpickr-day.endRange,.flatpickr-day.selected.inRange,.flatpickr-day.startRange.inRange,.flatpickr-day.endRange.inRange,.flatpickr-day.selected:focus,.flatpickr-day.startRange:focus,.flatpickr-day.endRange:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange:hover,.flatpickr-day.endRange:hover,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.endRange.nextMonthDay{background:#569ff7;-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:#569ff7}.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange,.flatpickr-day.endRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange,.flatpickr-day.endRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.selected.startRange+.endRange,.flatpickr-day.startRange.startRange+.endRange,.flatpickr-day.endRange.startRange+.endRange{-webkit-box-shadow:-10px 0 0 #569ff7;box-shadow:-10px 0 #569ff7}.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange,.flatpickr-day.endRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;-webkit-box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-5px 0 #e6e6e6,5px 0 #e6e6e6}.flatpickr-day.disabled,.flatpickr-day.disabled:hover{pointer-events:none}.flatpickr-day.disabled,.flatpickr-day.disabled:hover,.flatpickr-day.prevMonthDay,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.notAllowed.nextMonthDay{color:#3939394d;background:transparent;border-color:transparent;cursor:default}.flatpickr-day.week.selected{border-radius:0;-webkit-box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7;box-shadow:-5px 0 #569ff7,5px 0 #569ff7}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{display:inline-block;float:left}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 #e6e6e6;box-shadow:1px 0 #e6e6e6}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day{display:block;width:100%;max-width:none}.flatpickr-innerContainer{display:block;display:-webkit-flex;display:-ms-flexbox;display:-webkit-box;display:flex;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden}.flatpickr-rContainer{display:inline-block;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;display:-webkit-flex;display:-ms-flexbox;display:-webkit-box;display:flex}.flatpickr-time:after{content:"";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-flex:1;-ms-flex:1;-webkit-box-flex:1;flex:1;width:40%;height:40px;float:left}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:#393939}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:#393939}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:transparent;-webkit-box-shadow:none;box-shadow:none;border:0;border-radius:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;cursor:pointer;color:#393939;font-size:14px;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box}.flatpickr-time input.flatpickr-hour{font-weight:700}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-time-separator,.flatpickr-time .flatpickr-am-pm{height:inherit;display:inline-block;float:left;line-height:inherit;color:#393939;font-weight:700;width:2%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-align-self:center;-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time .flatpickr-am-pm:focus{background:#f0f0f0}.flatpickr-input[readonly]{cursor:pointer}@-webkit-keyframes fpFadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translateZ(0)}}@keyframes fpFadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translateZ(0)}}@-webkit-keyframes fpSlideLeft{0%{-webkit-transform:translate3d(0px,0px,0px);transform:translateZ(0)}to{-webkit-transform:translate3d(-100%,0px,0px);transform:translate3d(-100%,0,0)}}@keyframes fpSlideLeft{0%{-webkit-transform:translate3d(0px,0px,0px);transform:translateZ(0)}to{-webkit-transform:translate3d(-100%,0px,0px);transform:translate3d(-100%,0,0)}}@-webkit-keyframes fpSlideLeftNew{0%{-webkit-transform:translate3d(100%,0px,0px);transform:translate3d(100%,0,0)}to{-webkit-transform:translate3d(0px,0px,0px);transform:translateZ(0)}}@keyframes fpSlideLeftNew{0%{-webkit-transform:translate3d(100%,0px,0px);transform:translate3d(100%,0,0)}to{-webkit-transform:translate3d(0px,0px,0px);transform:translateZ(0)}}@-webkit-keyframes fpSlideRight{0%{-webkit-transform:translate3d(0,0,0px);transform:translateZ(0)}to{-webkit-transform:translate3d(100%,0px,0px);transform:translate3d(100%,0,0)}}@keyframes fpSlideRight{0%{-webkit-transform:translate3d(0,0,0px);transform:translateZ(0)}to{-webkit-transform:translate3d(100%,0px,0px);transform:translate3d(100%,0,0)}}@-webkit-keyframes fpSlideRightNew{0%{-webkit-transform:translate3d(-100%,0,0px);transform:translate3d(-100%,0,0)}to{-webkit-transform:translate3d(0,0,0px);transform:translateZ(0)}}@keyframes fpSlideRightNew{0%{-webkit-transform:translate3d(-100%,0,0px);transform:translate3d(-100%,0,0)}to{-webkit-transform:translate3d(0,0,0px);transform:translateZ(0)}}@-webkit-keyframes fpFadeOut{0%{opacity:1}to{opacity:0}}@keyframes fpFadeOut{0%{opacity:1}to{opacity:0}}@-webkit-keyframes fpFadeIn{0%{opacity:0}to{opacity:1}}@keyframes fpFadeIn{0%{opacity:0}to{opacity:1}} diff --git a/public/webform/build/assets/icomoon-CO37Y6g-.woff b/public/webform/build/assets/icomoon-CO37Y6g-.woff new file mode 100644 index 0000000000000000000000000000000000000000..93f8b8c670d49b5dfb99e3a77ed8f9398c65bfd3 GIT binary patch literal 37396 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 literal 0 HcmV?d00001 diff --git a/public/webform/build/manifest.json b/public/webform/build/manifest.json new file mode 100644 index 0000000..12e26bd --- /dev/null +++ b/public/webform/build/manifest.json @@ -0,0 +1,20 @@ +{ + "src/Resources/assets/css/app.css": { + "file": "assets/app-CWW-WGL4.css", + "src": "src/Resources/assets/css/app.css", + "isEntry": true + }, + "src/Resources/assets/fonts/icomoon.woff": { + "file": "assets/icomoon-CO37Y6g-.woff", + "src": "src/Resources/assets/fonts/icomoon.woff" + }, + "src/Resources/assets/js/app.js": { + "file": "assets/app-Di8tgz7D.js", + "name": "app", + "src": "src/Resources/assets/js/app.js", + "isEntry": true, + "css": [ + "assets/app-DsP8OK1c.css" + ] + } +} \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..e69de29 diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..846d350 --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,32 @@ +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo'; + +// import Pusher from 'pusher-js'; +// window.Pusher = Pusher; + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: import.meta.env.VITE_PUSHER_APP_KEY, +// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', +// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, +// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, +// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, +// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', +// enabledTransports: ['ws', 'wss'], +// }); diff --git a/resources/views/.gitignore b/resources/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/resources/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..bcb8b18 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,19 @@ +get('/user', function (Request $request) { + return $request->user(); +}); diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php new file mode 100644 index 0000000..ec5b66a --- /dev/null +++ b/routes/breadcrumbs.php @@ -0,0 +1,464 @@ +push(menu()->getLabel('dashboard', 'admin::app.layouts.dashboard'), route('admin.dashboard.index')); +}); + +// Dashboard > Leads +Breadcrumbs::for('leads', function (BreadcrumbTrail $trail) { + $trail->parent('dashboard'); + $trail->push(menu()->getLabel('leads', 'admin::app.layouts.leads'), route('admin.leads.index')); +}); + +// Dashboard > Leads > Create +Breadcrumbs::for('leads.create', function (BreadcrumbTrail $trail) { + $trail->parent('leads'); + $trail->push(trans('admin::app.leads.create.title'), route('admin.leads.create')); +}); + +// Leads Edit +Breadcrumbs::for('leads.edit', function (BreadcrumbTrail $trail, $lead) { + $trail->parent('leads'); + $trail->push(trans('admin::app.leads.edit.title'), route('admin.leads.edit', $lead->id)); +}); + +// Dashboard > Leads > Title +Breadcrumbs::for('leads.view', function (BreadcrumbTrail $trail, $lead) { + $trail->parent('leads'); + $trail->push('#'.$lead->id, route('admin.leads.view', $lead->id)); +}); + +// Dashboard > Quotes +Breadcrumbs::for('quotes', function (BreadcrumbTrail $trail) { + $trail->parent('dashboard'); + $trail->push(menu()->getLabel('quotes', 'admin::app.layouts.quotes'), route('admin.quotes.index')); +}); + +// Dashboard > Quotes > Add Quote +Breadcrumbs::for('quotes.create', function (BreadcrumbTrail $trail) { + $trail->parent('quotes'); + $trail->push(trans('admin::app.quotes.create.title'), route('admin.quotes.create')); +}); + +// Dashboard > Quotes > Edit Quote +Breadcrumbs::for('quotes.edit', function (BreadcrumbTrail $trail, $quote) { + $trail->parent('quotes'); + $trail->push(trans('admin::app.quotes.edit.title'), route('admin.quotes.edit', $quote->id)); +}); + +// Mail +Breadcrumbs::for('mail', function (BreadcrumbTrail $trail) { + $trail->parent('dashboard'); + $trail->push(menu()->getLabel('mail', 'admin::app.layouts.mail.title'), route('admin.mail.index', ['route' => 'inbox'])); +}); + +// Mail > [Compose | Inbox | Outbox | Draft | Sent | Trash] +Breadcrumbs::for('mail.route', function (BreadcrumbTrail $trail, $route) { + $trail->parent('mail'); + $trail->push(menu()->getLabel('mail.'.$route, 'admin::app.mail.index.'.$route), route('admin.mail.index', ['route' => $route])); +}); + +// Mail > [Inbox | Outbox | Draft | Sent | Trash] > Title +Breadcrumbs::for('mail.route.view', function (BreadcrumbTrail $trail, $route, $email) { + $trail->parent('mail.route', $route); + $trail->push($email->subject ?? '', route('admin.mail.view', ['route' => $route, 'id' => $email->id])); +}); + +// Dashboard > Activities +Breadcrumbs::for('activities', function (BreadcrumbTrail $trail) { + $trail->parent('dashboard'); + $trail->push(menu()->getLabel('activities', 'admin::app.layouts.activities'), route('admin.activities.index')); +}); + +// Dashboard > activities > Edit Activity +Breadcrumbs::for('activities.edit', function (BreadcrumbTrail $trail, $activity) { + $trail->parent('activities'); + $trail->push(trans('admin::app.activities.edit.title'), route('admin.activities.edit', $activity->id)); +}); + +// Dashboard > Contacts +Breadcrumbs::for('contacts', function (BreadcrumbTrail $trail) { + $trail->parent('dashboard'); + $trail->push(menu()->getLabel('contacts', 'admin::app.layouts.contacts'), route('admin.contacts.persons.index')); +}); + +// Dashboard > Contacts > Persons +Breadcrumbs::for('contacts.persons', function (BreadcrumbTrail $trail) { + $trail->parent('contacts'); + $trail->push(menu()->getLabel('contacts.persons', 'admin::app.layouts.persons'), route('admin.contacts.persons.index')); +}); + +// Dashboard > Contacts > Persons > Create +Breadcrumbs::for('contacts.persons.create', function (BreadcrumbTrail $trail) { + $trail->parent('contacts.persons'); + $trail->push(trans('admin::app.contacts.persons.create.title'), route('admin.contacts.persons.create')); +}); + +// Dashboard > Contacts > Persons > Edit +Breadcrumbs::for('contacts.persons.edit', function (BreadcrumbTrail $trail, $person) { + $trail->parent('contacts.persons'); + $trail->push(trans('admin::app.contacts.persons.edit.title'), route('admin.contacts.persons.edit', $person->id)); +}); + +// Dashboard > Contacts > Persons > View +Breadcrumbs::for('contacts.persons.view', function (BreadcrumbTrail $trail, $person) { + $trail->parent('contacts.persons'); + $trail->push('#'.$person->id, route('admin.contacts.persons.index')); +}); + +// Dashboard > Contacts > Organizations +Breadcrumbs::for('contacts.organizations', function (BreadcrumbTrail $trail) { + $trail->parent('contacts'); + $trail->push(menu()->getLabel('contacts.organizations', 'admin::app.layouts.organizations'), route('admin.contacts.organizations.index')); +}); + +// Dashboard > Contacts > Organizations > Create +Breadcrumbs::for('contacts.organizations.create', function (BreadcrumbTrail $trail) { + $trail->parent('contacts.organizations'); + $trail->push(trans('admin::app.contacts.organizations.create.title'), route('admin.contacts.organizations.create')); +}); + +// Dashboard > Contacts > Organizations > Edit +Breadcrumbs::for('contacts.organizations.edit', function (BreadcrumbTrail $trail, $organization) { + $trail->parent('contacts.organizations'); + $trail->push(trans('admin::app.contacts.organizations.edit.title'), route('admin.contacts.organizations.edit', $organization->id)); +}); + +// Products +Breadcrumbs::for('products', function (BreadcrumbTrail $trail) { + $trail->parent('dashboard'); + $trail->push(menu()->getLabel('products', 'admin::app.layouts.products'), route('admin.products.index')); +}); + +// Dashboard > Products > Create Product +Breadcrumbs::for('products.create', function (BreadcrumbTrail $trail) { + $trail->parent('products'); + $trail->push(trans('admin::app.products.create.title'), route('admin.products.create')); +}); + +// Dashboard > Products > View Product +Breadcrumbs::for('products.view', function (BreadcrumbTrail $trail, $product) { + $trail->parent('products'); + $trail->push('#'.$product->id, route('admin.products.view', $product->id)); +}); + +// Dashboard > Products > Edit Product +Breadcrumbs::for('products.edit', function (BreadcrumbTrail $trail, $product) { + $trail->parent('products'); + $trail->push(trans('admin::app.products.edit.title'), route('admin.products.edit', $product->id)); +}); + +// Settings +Breadcrumbs::for('settings', function (BreadcrumbTrail $trail) { + $trail->parent('dashboard'); + $trail->push(menu()->getLabel('settings', 'admin::app.layouts.settings'), route('admin.settings.index')); +}); + +// Settings > Groups +Breadcrumbs::for('settings.groups', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.user.groups', 'admin::app.layouts.groups'), route('admin.settings.groups.index')); +}); + +// Dashboard > Groups > Create Group +Breadcrumbs::for('settings.groups.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.groups'); + $trail->push(trans('admin::app.settings.groups.create-title'), route('admin.settings.groups.create')); +}); + +// Dashboard > Groups > Edit Group +Breadcrumbs::for('settings.groups.edit', function (BreadcrumbTrail $trail, $role) { + $trail->parent('settings.groups'); + $trail->push(trans('admin::app.settings.groups.edit-title'), route('admin.settings.groups.edit', $role->id)); +}); + +// Settings > Roles +Breadcrumbs::for('settings.roles', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.user.roles', 'admin::app.layouts.roles'), route('admin.settings.roles.index')); +}); + +// Dashboard > Roles > Create Role +Breadcrumbs::for('settings.roles.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.roles'); + $trail->push(trans('admin::app.settings.roles.create.title'), route('admin.settings.roles.create')); +}); + +// Dashboard > Roles > Edit Role +Breadcrumbs::for('settings.roles.edit', function (BreadcrumbTrail $trail, $role) { + $trail->parent('settings.roles'); + $trail->push(trans('admin::app.settings.roles.edit.title'), route('admin.settings.roles.edit', $role->id)); +}); + +// Settings > Users +Breadcrumbs::for('settings.users', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.user.users', 'admin::app.layouts.users'), route('admin.settings.users.index')); +}); + +// Dashboard > Users > Create Role +Breadcrumbs::for('settings.users.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.users'); + $trail->push(trans('admin::app.settings.users.create-title'), route('admin.settings.users.create')); +}); + +// Dashboard > Users > Edit Role +Breadcrumbs::for('settings.users.edit', function (BreadcrumbTrail $trail, $user) { + $trail->parent('settings.users'); + $trail->push(trans('admin::app.settings.users.edit-title'), route('admin.settings.users.edit', $user->id)); +}); + +// Settings > Attributes +Breadcrumbs::for('settings.attributes', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.automation.attributes', 'admin::app.layouts.attributes'), route('admin.settings.attributes.index')); +}); + +// Dashboard > Attributes > Create Attribute +Breadcrumbs::for('settings.attributes.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.attributes'); + $trail->push(trans('admin::app.settings.attributes.create.title'), route('admin.settings.attributes.create')); +}); + +// Dashboard > Attributes > Edit Attribute +Breadcrumbs::for('settings.attributes.edit', function (BreadcrumbTrail $trail, $attribute) { + $trail->parent('settings.attributes'); + $trail->push(trans('admin::app.settings.attributes.edit.title'), route('admin.settings.attributes.edit', $attribute->id)); +}); + +// Settings > Pipelines +Breadcrumbs::for('settings.pipelines', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.lead.pipelines', 'admin::app.layouts.pipelines'), route('admin.settings.pipelines.index')); +}); + +// Dashboard > Pipelines > Create Pipeline +Breadcrumbs::for('settings.pipelines.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.pipelines'); + $trail->push(trans('admin::app.settings.pipelines.create.title'), route('admin.settings.pipelines.create')); +}); + +// Dashboard > Pipelines > Edit Pipeline +Breadcrumbs::for('settings.pipelines.edit', function (BreadcrumbTrail $trail, $pipeline) { + $trail->parent('settings.pipelines'); + $trail->push(trans('admin::app.settings.pipelines.edit.title'), route('admin.settings.pipelines.edit', $pipeline->id)); +}); + +// Settings > Sources +Breadcrumbs::for('settings.sources', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.lead.sources', 'admin::app.layouts.sources'), route('admin.settings.sources.index')); +}); + +// Dashboard > Sources > Edit Source +Breadcrumbs::for('settings.sources.edit', function (BreadcrumbTrail $trail, $source) { + $trail->parent('settings.sources'); + $trail->push(trans('admin::app.settings.sources.edit-title'), route('admin.settings.sources.edit', $source->id)); +}); + +// Settings > Types +Breadcrumbs::for('settings.types', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.lead.types', 'admin::app.layouts.types'), route('admin.settings.types.index')); +}); + +// Dashboard > Types > Edit Type +Breadcrumbs::for('settings.types.edit', function (BreadcrumbTrail $trail, $type) { + $trail->parent('settings.types'); + $trail->push(trans('admin::app.settings.types.edit-title'), route('admin.settings.types.edit', $type->id)); +}); + +// Settings > Email Templates +Breadcrumbs::for('settings.email_templates', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.email-template.index.title'), route('admin.settings.email_templates.index')); +}); + +// Dashboard > Email Templates > Create Email Template +Breadcrumbs::for('settings.email_templates.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.email_templates'); + $trail->push(trans('admin::app.settings.email-template.create.title'), route('admin.settings.email_templates.create')); +}); + +// Dashboard > Email Templates > Edit Email Template +Breadcrumbs::for('settings.email_templates.edit', function (BreadcrumbTrail $trail, $emailTemplate) { + $trail->parent('settings.email_templates'); + $trail->push(trans('admin::app.settings.email-template.edit.title'), route('admin.settings.email_templates.edit', $emailTemplate->id)); +}); + +// Settings > Marketing Events +Breadcrumbs::for('settings.marketing.events', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.marketing.events.index.title'), route('admin.settings.marketing.events.index')); +}); + +Breadcrumbs::for('settings.marketing.campaigns', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.marketing.campaigns.index.title'), route('admin.settings.marketing.campaigns.index')); +}); + +// Settings > Workflows +Breadcrumbs::for('settings.workflows', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.automation.workflows', 'admin::app.layouts.workflows'), route('admin.settings.workflows.index')); +}); + +// Dashboard > Workflows > Create Workflow +Breadcrumbs::for('settings.workflows.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.workflows'); + $trail->push(trans('admin::app.settings.workflows.create.title'), route('admin.settings.workflows.create')); +}); + +// Dashboard > Workflows > Edit Workflow +Breadcrumbs::for('settings.workflows.edit', function (BreadcrumbTrail $trail, $workflow) { + $trail->parent('settings.workflows'); + $trail->push(trans('admin::app.settings.workflows.edit.title'), route('admin.settings.workflows.edit', $workflow->id)); +}); + +// Settings > Webhooks +Breadcrumbs::for('settings.webhooks', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.webhooks.index.title'), route('admin.settings.webhooks.index')); +}); + +// Dashboard > Webhooks > Create Workflow +Breadcrumbs::for('settings.webhooks.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.webhooks'); + $trail->push(trans('admin::app.settings.webhooks.create.title'), route('admin.settings.webhooks.create')); +}); + +// Dashboard > Webhooks > Edit Workflow +Breadcrumbs::for('settings.webhooks.edit', function (BreadcrumbTrail $trail, $workflow) { + $trail->parent('settings.webhooks'); + $trail->push(trans('admin::app.settings.webhooks.edit.edit-btn'), route('admin.settings.workflows.edit', $workflow->id)); +}); + +// Settings > Tags +Breadcrumbs::for('settings.tags', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(menu()->getLabel('settings.other_settings.tags', 'admin::app.layouts.tags'), route('admin.settings.tags.index')); +}); + +// Dashboard > Tags > Edit Tag +Breadcrumbs::for('settings.tags.edit', function (BreadcrumbTrail $trail, $tag) { + $trail->parent('settings.tags'); + $trail->push(trans('admin::app.settings.tags.edit-title'), route('admin.settings.tags.edit', $tag->id)); +}); + +// Settings > Google Contacts +Breadcrumbs::for('settings.google_contacts', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.google-contacts.index.title'), route('admin.settings.google_contacts.index')); +}); + +// Settings > Web Form +Breadcrumbs::for('settings.web_forms', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.webforms.index.title'), route('admin.settings.web_forms.index')); +}); + +// Dashboard > Web Form > Create Web Form +Breadcrumbs::for('settings.web_forms.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.web_forms'); + $trail->push(trans('admin::app.settings.webforms.create.title'), route('admin.settings.web_forms.create')); +}); + +// Dashboard > Web Form > Edit Web Form +Breadcrumbs::for('settings.web_forms.edit', function (BreadcrumbTrail $trail, $webForm) { + $trail->parent('settings.web_forms'); + $trail->push(trans('admin::app.settings.webforms.edit.title'), route('admin.settings.web_forms.edit', $webForm->id)); +}); + +// Settings > Warehouse +Breadcrumbs::for('settings.warehouses', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.warehouses.index.title'), route('admin.settings.warehouses.index')); +}); + +// Dashboard > Settings > Warehouse > Create Warehouse +Breadcrumbs::for('settings.warehouses.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.warehouses'); + $trail->push(trans('admin::app.settings.warehouses.create.title'), route('admin.settings.warehouses.create')); +}); + +// Dashboard > Settings > Warehouse > Edit Warehouse +Breadcrumbs::for('settings.warehouses.edit', function (BreadcrumbTrail $trail, $warehouse) { + $trail->parent('settings.warehouses'); + $trail->push(trans('admin::app.settings.warehouses.edit.title'), route('admin.settings.warehouses.edit', $warehouse->id)); +}); + +// Dashboard > Settings > Warehouse > View Warehouse +Breadcrumbs::for('settings.warehouses.view', function (BreadcrumbTrail $trail, $warehouse) { + $trail->parent('settings.warehouses'); + $trail->push('#'.$warehouse->id, route('admin.settings.warehouses.view', $warehouse->id)); +}); + +// Dashboard > Settings > Warehouse > View Warehouse > Products +Breadcrumbs::for('settings.warehouses.view.products', function (BreadcrumbTrail $trail, $warehouse) { + $trail->parent('settings.warehouses.view', $warehouse); + $trail->push(trans('admin::app.settings.warehouses.products'), route('admin.settings.warehouses.products.index', $warehouse->id)); +}); + +// Dashboard > Settings > Locations +Breadcrumbs::for('settings.locations', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.locations.title'), route('admin.settings.locations.index')); +}); + +// Dashboard > Settings > Locations > Create Warehouse +Breadcrumbs::for('settings.locations.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.locations'); + $trail->push(trans('admin::app.settings.locations.create-title'), route('admin.settings.locations.create')); +}); + +// Dashboard > Settings > Locations > Edit Warehouse +Breadcrumbs::for('settings.locations.edit', function (BreadcrumbTrail $trail, $location) { + $trail->parent('settings.locations'); + $trail->push(trans('admin::app.settings.locations.edit-title'), route('admin.settings.locations.edit', $location->id)); +}); + +// Dashboard > Settings > Data Transfers +Breadcrumbs::for('settings.data_transfers', function (BreadcrumbTrail $trail) { + $trail->parent('settings'); + $trail->push(trans('admin::app.settings.data-transfer.imports.index.title'), route('admin.settings.data_transfer.imports.index')); +}); + +// Dashboard > Settings > Data Transfers > Create Data Transfer +Breadcrumbs::for('settings.data_transfers.create', function (BreadcrumbTrail $trail) { + $trail->parent('settings.data_transfers'); + $trail->push(trans('admin::app.settings.data-transfer.imports.create.title'), route('admin.settings.data_transfer.imports.create')); +}); + +// Dashboard > Settings > Data Transfers > Edit Data Transfer +Breadcrumbs::for('settings.data_transfers.edit', function (BreadcrumbTrail $trail, $import) { + $trail->parent('settings.data_transfers'); + $trail->push(trans('admin::app.settings.data-transfer.imports.edit.title'), route('admin.settings.data_transfer.imports.edit', $import->id)); +}); + +// Dashboard > Settings > Data Transfers > Import Data Transfer +Breadcrumbs::for('settings.data_transfers.import', function (BreadcrumbTrail $trail, $import) { + $trail->parent('settings.data_transfers'); + $trail->push(trans('admin::app.settings.data-transfer.imports.import.title'), route('admin.settings.data_transfer.imports.import', $import->id)); +}); + +// Configuration +Breadcrumbs::for('configuration', function (BreadcrumbTrail $trail) { + $trail->parent('dashboard'); + $trail->push(menu()->getLabel('configuration', 'admin::app.layouts.configuration'), route('admin.configuration.index')); +}); + +// Configuration > Config +Breadcrumbs::for('configuration.slug', function (BreadcrumbTrail $trail, $slug) { + $trail->parent('configuration'); + $trail->push('', route('admin.configuration.index', ['slug' => $slug])); +}); + +// Dashboard > Account > Edit +Breadcrumbs::for('dashboard.account.edit', function (BreadcrumbTrail $trail, $user) { + $trail->parent('dashboard'); + $trail->push(trans('admin::app.account.edit.title'), route('admin.user.account.edit', $user->id)); +}); diff --git a/routes/channels.php b/routes/channels.php new file mode 100644 index 0000000..5d451e1 --- /dev/null +++ b/routes/channels.php @@ -0,0 +1,18 @@ +id === (int) $id; +}); diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..796d7f8 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,22 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); + +Schedule::command('inbound-emails:process')->everyFiveMinutes(); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/routes/web.php @@ -0,0 +1 @@ + TaskMaster Pro - Advanced Project Management Tool,"Urgent Requirement: TaskMaster Pro – an advanced project management tool designed to enhance team collaboration, streamline task tracking, and optimize time management for small to medium-sized teams. Ideal for businesses looking to improve project efficiency and productivity.",50000,1,,2024-08-30 14:39:58,1,1,2,2,1,"price=45,quantity=2,id=1",5,2024-09-02 +,Need more information About Product -> CyberGuard Pro,A client wants to know about the CyberGuard Pro Product in details and he is interested in the product.,100000,1,,,1,2,4,1,1,"price=45,quantity=2,id=2",3,2024-09-06 diff --git a/storage/app/public/data-transfer/samples/persons.csv b/storage/app/public/data-transfer/samples/persons.csv new file mode 100644 index 0000000..d415544 --- /dev/null +++ b/storage/app/public/data-transfer/samples/persons.csv @@ -0,0 +1,3 @@ +name,emails,contact_numbers,organization_id,job_title,user_id +Wilson Fisk,"[{""label"": ""work"", ""value"": ""contact@wilson.com""}, {""label"": ""home"", ""value"": ""contact.home@wilson.com""}]","[{""label"": ""work"", ""value"": ""5454445454""}]",1,Sales Executive,1 +Sasha Calle,"[{""label"": ""work"", ""value"": ""contact@sasha.com""}]","[{""label"": ""work"", ""value"": ""15454445454""}]",1,Sales Representatives,1 diff --git a/storage/app/public/data-transfer/samples/products.csv b/storage/app/public/data-transfer/samples/products.csv new file mode 100644 index 0000000..b27b317 --- /dev/null +++ b/storage/app/public/data-transfer/samples/products.csv @@ -0,0 +1,3 @@ +sku,name,description,quantity,price +100,TaskMaster Pro - Advanced Project Management Tool,"A project management tool designed for small to medium-sized teams. It offers task tracking, team collaboration, and time management features. It also integrates with popular communication platforms.",45,500 +101,ProjectPulse - A Simple and Effective Project Management Tool,"A cloud-based project management solution that provides Gantt charts, resource allocation, and project reporting. It is designed for large enterprises and supports multi-project management with real-time analytics.",20,450 diff --git a/storage/debugbar/.gitignore b/storage/debugbar/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/debugbar/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100644 index 0000000..3770f49 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,10 @@ +compiled.php +config.php +down +events.scanned.php +/laravel-excel +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Feature/AuthenticationTest.php b/tests/Feature/AuthenticationTest.php new file mode 100644 index 0000000..04f93a1 --- /dev/null +++ b/tests/Feature/AuthenticationTest.php @@ -0,0 +1,28 @@ +get(route('admin.session.create')) + ->assertOK(); +}); + +it('can see the dashboard page after login', function () { + $admin = getDefaultAdmin(); + + test()->actingAs($admin) + ->get(route('admin.dashboard.index')) + ->assertOK(); + + expect(auth()->guard('user')->user()->name)->toBe($admin->name); +}); + +it('can logout from the admin panel', function () { + $admin = getDefaultAdmin(); + + test()->actingAs($admin) + ->delete(route('admin.session.destroy'), [ + '_token' => csrf_token(), + ]) + ->assertStatus(302); + + expect(auth()->guard('user')->user())->toBeNull(); +}); diff --git a/tests/Feature/HelpTest.php b/tests/Feature/HelpTest.php new file mode 100644 index 0000000..693416a --- /dev/null +++ b/tests/Feature/HelpTest.php @@ -0,0 +1,16 @@ +actingAs($admin) + ->get(route('admin.help.index')) + ->assertOk() + ->assertSee('Help & Resources') + ->assertSee('Cloud Hosting') + ->assertSee('Extensions') + ->assertSee('krayincrm.com/cloud-hosting') + ->assertSee('Still need a hand?') + ->assertSee('Community Forums') + ->assertSee('Video Tutorials'); +}); diff --git a/tests/Feature/Security/AuthenticationHardeningTest.php b/tests/Feature/Security/AuthenticationHardeningTest.php new file mode 100644 index 0000000..d7cbee7 --- /dev/null +++ b/tests/Feature/Security/AuthenticationHardeningTest.php @@ -0,0 +1,145 @@ + 'Auth Probe Role '.bin2hex(random_bytes(6)), + 'description' => 'Created by the authentication hardening tests.', + 'permission_type' => 'all', + ]); + + return User::create([ + 'name' => 'Auth Probe', + 'email' => 'auth-probe-'.bin2hex(random_bytes(6)).'@example.invalid', + 'password' => bcrypt($password), + 'status' => $status, + 'role_id' => $role->id, + 'view_permission' => 'global', + ]); +} + +beforeEach(function () { + /** + * The configured APP_URL carries a subdirectory, which the test client would otherwise prepend + * to every request path, so no admin route would match. + */ + URL::forceRootUrl('http://localhost'); + + /** + * Rate limiter state lives in the cache; phpunit.xml pins CACHE_STORE=array, so flushing here + * isolates each test without touching any real cache. + */ + Cache::flush(); +}); + +it('rate limits repeated failed logins', function () { + $credentials = [ + 'email' => 'no-such-user@example.invalid', + 'password' => 'wrong-password', + ]; + + for ($attempt = 1; $attempt <= 5; $attempt++) { + expect(test()->post('/admin/login', $credentials)->getStatusCode()) + ->not->toBe(429, "attempt {$attempt} should not yet be throttled"); + } + + test()->post('/admin/login', $credentials)->assertStatus(429); +}); + +it('rate limits repeated forgot password requests', function () { + $payload = ['email' => 'no-such-user@example.invalid']; + + for ($attempt = 1; $attempt <= 5; $attempt++) { + test()->post('/admin/forget-password', $payload); + } + + test()->post('/admin/forget-password', $payload)->assertStatus(429); +}); + +it('does not reveal whether an email belongs to an account', function () { + $response = test()->post('/admin/forget-password', [ + 'email' => 'definitely-not-registered@example.invalid', + ]); + + $response->assertSessionHasNoErrors(); + + expect(session('success'))->not->toBeNull(); +}); + +it('destroys the whole session on logout, not just the auth state', function () { + $admin = makeAuthProbeUser(); + + $response = test() + ->actingAs($admin, 'user') + ->withSession(['canary' => 'survives-only-if-session-is-not-invalidated']) + ->delete('/admin/logout'); + + $response->assertStatus(302); + $response->assertSessionMissing('canary'); + + expect(auth()->guard('user')->check())->toBeFalse(); +}); + +/** + * Only *failed* sign-ins may count against the limit. + * + * Throttling every attempt also throttles people who sign in correctly — the browser test suite + * signs in once per test case from a single address, and several staff can share one office + * address — while adding nothing against password guessing, which is made up entirely of failures. + */ +it('never throttles repeated successful logins', function () { + $password = 'correct-horse-battery-staple'; + + $user = makeAuthProbeUser($password); + + foreach (range(1, 12) as $attempt) { + $status = test() + ->post('/admin/login', ['email' => $user->email, 'password' => $password]) + ->getStatusCode(); + + expect($status)->not->toBe(429, "successful login {$attempt} was throttled"); + + auth()->guard('user')->logout(); + } +}); + +it('lets a correct sign-in clear earlier failures for those credentials', function () { + $password = 'correct-horse-battery-staple'; + + $user = makeAuthProbeUser($password); + + foreach (range(1, 4) as $ignored) { + test()->post('/admin/login', ['email' => $user->email, 'password' => 'wrong']); + } + + test()->post('/admin/login', ['email' => $user->email, 'password' => $password]); + + auth()->guard('user')->logout(); + + /** + * The budget was reset by the successful sign-in, so a fresh run of failures is needed again + * before the limit trips. + */ + foreach (range(1, 4) as $attempt) { + expect(test()->post('/admin/login', ['email' => $user->email, 'password' => 'wrong'])->getStatusCode()) + ->not->toBe(429, "failure {$attempt} after a successful sign-in should not be throttled"); + } +}); diff --git a/tests/Feature/Security/HtmlSanitizationTest.php b/tests/Feature/Security/HtmlSanitizationTest.php new file mode 100644 index 0000000..7147146 --- /dev/null +++ b/tests/Feature/Security/HtmlSanitizationTest.php @@ -0,0 +1,50 @@ +sanitizer = new class + { + use Sanitizer; + }; +}); + +it('strips the advisory payload from footer html', function () { + $output = $this->sanitizer->sanitizeHtml(''); + + expect($output)->not->toContain('onerror') + ->and($output)->not->toContain('alert('); +}); + +it('strips script elements and javascript uris', function (string $payload, string $forbidden) { + expect($this->sanitizer->sanitizeHtml($payload))->not->toContain($forbidden); +})->with([ + ['', 'x', 'javascript:'], + ['', 'onload'], + ['', '', 'onload'], +]); + +it('keeps legitimate formatting and target blank links intact', function () { + $output = $this->sanitizer->sanitizeHtml( + '

      Powered by Krayin

      ' + ); + + expect($output)->toContain('

      ') + ->and($output)->toContain('href="https://krayincrm.com"') + ->and($output)->toContain('target="_blank"') + ->and($output)->toContain('Krayin'); +}); + +it('returns an empty string for null and blank input', function () { + expect($this->sanitizer->sanitizeHtml(null))->toBe('') + ->and($this->sanitizer->sanitizeHtml(' '))->toBe(''); +}); diff --git a/tests/Feature/Security/InstallerLockdownTest.php b/tests/Feature/Security/InstallerLockdownTest.php new file mode 100644 index 0000000..f3b4eac --- /dev/null +++ b/tests/Feature/Security/InstallerLockdownTest.php @@ -0,0 +1,204 @@ +originalStoragePath = app()->storagePath(); + + $this->tempStorage = sys_get_temp_dir().'/krayin-installer-test-'.bin2hex(random_bytes(6)); + + mkdir($this->tempStorage, 0777, true); + + app()->useStoragePath($this->tempStorage); + + /** + * $completed — whether the completion flag has been recorded in the database. + * $populated — whether the database actually holds an installed application. + */ + $this->fakeDatabase = function (bool $completed, bool $populated = false) { + app()->instance(DatabaseManager::class, new class($completed, $populated) extends DatabaseManager + { + public bool $backfilled = false; + + public function __construct(private bool $completed, private bool $populated) {} + + public function isInstallationCompleted(): bool + { + return $this->completed; + } + + public function isInstalled() + { + return $this->populated; + } + + public function markInstallationCompleted(): void + { + $this->backfilled = true; + $this->completed = true; + } + }); + }; +}); + +afterEach(function () { + app()->useStoragePath($this->originalStoragePath); + + @unlink($this->tempStorage.'/installed'); + @unlink($this->tempStorage.'/installing'); + @rmdir($this->tempStorage); +}); + +it('treats the application as installed when the marker file is present', function () { + ($this->fakeDatabase)(false); + + touch(storage_path('installed')); + + expect((new CanInstall)->isInstallationComplete())->toBeTrue(); +}); + +it('treats the application as installed when the marker is gone but the database says so', function () { + ($this->fakeDatabase)(true); + + expect(file_exists(storage_path('installed')))->toBeFalse() + ->and((new CanInstall)->isInstallationComplete())->toBeTrue(); +}); + +it('still allows a genuine installation to proceed', function () { + ($this->fakeDatabase)(false); + + expect(file_exists(storage_path('installed')))->toBeFalse() + ->and((new CanInstall)->isInstallationComplete())->toBeFalse(); +}); + +it('blocks the installer api on an installed app that lost its marker file', function (string $path) { + ($this->fakeDatabase)(true); + + $reached = false; + + $response = (new CanInstall)->handle( + Request::create($path, 'POST'), + function () use (&$reached) { + $reached = true; + + return response('controller reached'); + } + ); + + expect($reached)->toBeFalse() + ->and($response->getStatusCode())->toBe(302); +})->with([ + '/install/api/admin-config-setup', + '/install/api/run-migration', + '/install/api/run-seeder', + '/install/api/env-file-setup', +]); + +it('lets the installer api through while an installation is genuinely in progress', function () { + ($this->fakeDatabase)(false); + + $reached = false; + + (new CanInstall)->handle( + Request::create('/install/api/run-migration', 'POST'), + function () use (&$reached) { + $reached = true; + + return response('ok'); + } + ); + + expect($reached)->toBeTrue(); +}); + +/** + * A legacy installation — one completed before the database flag existed — has neither signal + * after a deploy that leaves `storage/` behind, yet its database is fully populated. The installer + * API must still be closed to it: reaching run-migration there wipes live data, and + * admin-config-setup overwrites the administrator account. + */ +it('treats a legacy installation with no flag and no marker as installed', function () { + ($this->fakeDatabase)(completed: false, populated: true); + + expect(file_exists(storage_path('installed')))->toBeFalse() + ->and((new CanInstall)->isInstallationComplete())->toBeTrue(); +}); + +it('blocks the installer api for a legacy installation that lost its marker file', function (string $path) { + ($this->fakeDatabase)(completed: false, populated: true); + + $reached = false; + + (new CanInstall)->handle(Request::create($path, 'POST'), function () use (&$reached) { + $reached = true; + + return response('controller reached'); + }); + + expect($reached)->toBeFalse(); +})->with([ + '/install/api/run-migration', + '/install/api/admin-config-setup', + '/install/api/env-file-setup', +]); + +/** + * The seeder creates the default administrator before `admin-config-setup` runs, so from that + * moment the database looks "installed" while the installation is in fact still going. The + * in-progress marker is what keeps the final step reachable. + */ +it('does not lock out the final installer step once the seeder has run', function () { + ($this->fakeDatabase)(completed: false, populated: true); + + app(DatabaseManager::class)->markInstallationInProgress(); + + expect(app(DatabaseManager::class)->isInstallationComplete())->toBeFalse(); + + $reached = false; + + (new CanInstall)->handle(Request::create('/install/api/admin-config-setup', 'POST'), function () use (&$reached) { + $reached = true; + + return response('ok'); + }); + + expect($reached)->toBeTrue(); +}); + +it('closes the installer again once the in-progress marker is cleared', function () { + ($this->fakeDatabase)(completed: false, populated: true); + + $database = app(DatabaseManager::class); + + $database->markInstallationInProgress(); + expect($database->isInstallationComplete())->toBeFalse(); + + $database->clearInstallationInProgress(); + expect($database->isInstallationComplete())->toBeTrue(); +}); + +it('backfills the database flag when it infers completion from the database', function () { + ($this->fakeDatabase)(completed: false, populated: true); + + $database = app(DatabaseManager::class); + + expect($database->backfilled)->toBeFalse(); + + $database->isInstallationComplete(); + + expect($database->backfilled)->toBeTrue(); +}); diff --git a/tests/Feature/Security/PasswordResetStatusTest.php b/tests/Feature/Security/PasswordResetStatusTest.php new file mode 100644 index 0000000..5dccd9e --- /dev/null +++ b/tests/Feature/Security/PasswordResetStatusTest.php @@ -0,0 +1,81 @@ +getMethod('resetPassword'); + $method->setAccessible(true); + $method->invoke($controller, $user, 'brand-new-password'); +} + +function makeUser(int $status): User +{ + /** + * Created here rather than assumed: a freshly migrated test database has no roles, and the + * users table has a foreign key onto them. + */ + $role = Role::create([ + 'name' => 'Reset Probe Role '.bin2hex(random_bytes(6)), + 'description' => 'Created by the password reset status tests.', + 'permission_type' => 'all', + ]); + + return User::create([ + 'name' => 'Reset Status Probe', + 'email' => 'reset-probe-'.bin2hex(random_bytes(6)).'@example.invalid', + 'password' => bcrypt('original-password'), + 'status' => $status, + 'role_id' => $role->id, + 'view_permission' => 'global', + ]); +} + +afterEach(function () { + auth()->guard('user')->logout(); +}); + +it('does not sign in a deactivated account after a password reset', function () { + $user = makeUser(status: 0); + + completeResetFor($user); + + expect(auth()->guard('user')->check())->toBeFalse(); +}); + +it('still signs in an active account after a password reset', function () { + $user = makeUser(status: 1); + + completeResetFor($user); + + expect(auth()->guard('user')->check())->toBeTrue() + ->and(auth()->guard('user')->id())->toBe($user->id); +}); + +it('changes the password for a deactivated account even though it does not sign in', function () { + $user = makeUser(status: 0); + + completeResetFor($user); + + expect(Hash::check('brand-new-password', $user->fresh()->password))->toBeTrue(); +}); diff --git a/tests/Feature/Security/SvgUploadSanitizationTest.php b/tests/Feature/Security/SvgUploadSanitizationTest.php new file mode 100644 index 0000000..508ac01 --- /dev/null +++ b/tests/Feature/Security/SvgUploadSanitizationTest.php @@ -0,0 +1,88 @@ + + + + click + + +SVG; + +beforeEach(function () { + Storage::fake(); + + $this->sanitizer = new class + { + use Sanitizer; + }; +}); + +/** + * Store the payload the way the upload handlers do, then sanitize it. + */ +function storeThenSanitize(object $sanitizer, string $storedPath, string $clientName): string +{ + Storage::put($storedPath, MALICIOUS_SVG); + + $sanitizer->sanitizeSvg( + $storedPath, + UploadedFile::fake()->createWithContent($clientName, MALICIOUS_SVG) + ); + + return (string) Storage::get($storedPath); +} + +it('sanitizes an svg uploaded under a non-svg name', function () { + $result = storeThenSanitize($this->sanitizer, 'tinymce/abc123.svg', 'payload.png'); + + expect($result)->not->toContain('and($result)->not->toContain('onload') + ->and($result)->not->toContain('javascript:'); +}); + +it('still sanitizes an svg named honestly', function () { + $result = storeThenSanitize($this->sanitizer, 'tinymce/abc123.svg', 'logo.svg'); + + expect($result)->not->toContain('and($result)->not->toContain('onload'); +}); + +it('sanitizes svg markup even when neither the name nor the stored path says svg', function () { + $result = storeThenSanitize($this->sanitizer, 'configuration/abc123.png', 'payload.png'); + + expect($result)->not->toContain('and($result)->not->toContain('onload'); +}); + +it('detects svg by stored path and by markup, not only by client name', function () { + expect($this->sanitizer->isSvgPath('tinymce/abc.svg'))->toBeTrue() + ->and($this->sanitizer->isSvgPath('tinymce/abc.png'))->toBeFalse() + ->and($this->sanitizer->hasSvgMarkup(MALICIOUS_SVG))->toBeTrue() + ->and($this->sanitizer->hasSvgMarkup('just plain text'))->toBeFalse(); +}); + +it('leaves a genuine non-svg upload untouched', function () { + $png = "\x89PNG\r\n\x1a\n".str_repeat("\x00", 32); + + Storage::put('tinymce/real.png', $png); + + $this->sanitizer->sanitizeSvg( + 'tinymce/real.png', + UploadedFile::fake()->createWithContent('real.png', $png) + ); + + expect(Storage::get('tinymce/real.png'))->toBe($png); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..0da3fc7 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,92 @@ +in('Feature'); + +/** + * The configured APP_URL may point at a subdirectory (for example when the application is served + * from `/public` under a project folder). Laravel's test client prepends that URL to every request + * path, so the resulting path info would carry the subdirectory and no route would ever match, + * failing every feature test with a 404. Pinning a bare root URL keeps request paths aligned with + * the routes as registered, whatever APP_URL happens to be on the machine running the tests. + */ +uses()->beforeEach(function () { + URL::forceRootUrl('http://localhost'); +})->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| + */ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| + */ + +/** + * Get default admin which is created on fresh instance. + * + * @return User + */ +function getDefaultAdmin() +{ + $admin = User::find(1); + + return $admin; +} + +/** + * Sanctum authenticated admin. + * + * @return User + */ +function actingAsSanctumAuthenticatedAdmin() +{ + return Sanctum::actingAs( + getDefaultAdmin(), + ['*'] + ); +} + +/** + * Get first name. + * + * @param string $fullName + * @return string + */ +function getFirstName($fullName) +{ + return explode(' ', $fullName)[0]; +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + + expect(true)->toBeTrue(); +}); diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..421b569 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + ], +});