snapshot of https://github.com/krayin/laravel-crm.git (2.2, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Admin | Playwright Tests / Admin | Playwright Tests | Shard 1 Of 6 (push) Failing after 37s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 2 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 4 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 6 Of 6 (push) Failing after 6s
Linting Tests / PHP 8.3 test on ubuntu-latest (push) Failing after 5s
CI / PHP 8.3 test on ubuntu-latest (push) Failing after 6s
Skills Consistency / validate-skills (push) Successful in 10s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 3 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 5 Of 6 (push) Failing after 6s
Some checks failed
Admin | Playwright Tests / Admin | Playwright Tests | Shard 1 Of 6 (push) Failing after 37s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 2 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 4 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 6 Of 6 (push) Failing after 6s
Linting Tests / PHP 8.3 test on ubuntu-latest (push) Failing after 5s
CI / PHP 8.3 test on ubuntu-latest (push) Failing after 6s
Skills Consistency / validate-skills (push) Successful in 10s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 3 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 5 Of 6 (push) Failing after 6s
This commit is contained in:
5
packages/Webkul/Admin/.gitignore
vendored
Executable file
5
packages/Webkul/Admin/.gitignore
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
npm-debug.log
|
||||
/playwright-report
|
||||
/test-results
|
||||
35
packages/Webkul/Admin/composer.json
Executable file
35
packages/Webkul/Admin/composer.json
Executable file
@@ -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"
|
||||
}
|
||||
1
packages/Webkul/Admin/example.txt
Normal file
1
packages/Webkul/Admin/example.txt
Normal file
@@ -0,0 +1 @@
|
||||
Hello, this is a generated file!
|
||||
36
packages/Webkul/Admin/package.json
Normal file
36
packages/Webkul/Admin/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
3
packages/Webkul/Admin/postcss.config.cjs
Normal file
3
packages/Webkul/Admin/postcss.config.cjs
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = ({ env }) => ({
|
||||
plugins: [require("tailwindcss")(), require("autoprefixer")()],
|
||||
});
|
||||
60
packages/Webkul/Admin/src/Bouncer.php
Executable file
60
packages/Webkul/Admin/src/Bouncer.php
Executable file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin;
|
||||
|
||||
use Webkul\User\Repositories\UserRepository;
|
||||
|
||||
class Bouncer
|
||||
{
|
||||
/**
|
||||
* Checks if user allowed or not for certain action
|
||||
*
|
||||
* @param string $permission
|
||||
* @return void
|
||||
*/
|
||||
public function hasPermission($permission)
|
||||
{
|
||||
if (auth()->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];
|
||||
}
|
||||
}
|
||||
}
|
||||
590
packages/Webkul/Admin/src/Config/acl.php
Normal file
590
packages/Webkul/Admin/src/Config/acl.php
Normal file
@@ -0,0 +1,590 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
'key' => '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,
|
||||
],
|
||||
];
|
||||
33
packages/Webkul/Admin/src/Config/attribute_entity_types.php
Normal file
33
packages/Webkul/Admin/src/Config/attribute_entity_types.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'leads' => [
|
||||
'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',
|
||||
],
|
||||
];
|
||||
54
packages/Webkul/Admin/src/Config/attribute_lookups.php
Normal file
54
packages/Webkul/Admin/src/Config/attribute_lookups.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'leads' => [
|
||||
'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',
|
||||
],
|
||||
];
|
||||
353
packages/Webkul/Admin/src/Config/core_config.php
Normal file
353
packages/Webkul/Admin/src/Config/core_config.php
Normal file
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/**
|
||||
* General.
|
||||
*/
|
||||
[
|
||||
'key' => '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 <span style="color: rgb(14, 144, 217);"><a href="http://www.krayincrm.com" target="_blank">Krayin</a></span>, an open-source project by <span style="color: rgb(14, 144, 217);"><a href="https://webkul.com" target="_blank">Webkul</a></span>.',
|
||||
'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'),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
301
packages/Webkul/Admin/src/Config/menu.php
Normal file
301
packages/Webkul/Admin/src/Config/menu.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/**
|
||||
* Dashboard.
|
||||
*/
|
||||
[
|
||||
'key' => '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',
|
||||
],
|
||||
];
|
||||
70
packages/Webkul/Admin/src/Config/support.php
Normal file
70
packages/Webkul/Admin/src/Config/support.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'services' => [
|
||||
[
|
||||
'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',
|
||||
];
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Activity;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Admin\Traits\ProvideDropdownOptions;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
use Webkul\User\Repositories\UserRepository;
|
||||
|
||||
class ActivityDataGrid extends DataGrid
|
||||
{
|
||||
use ProvideDropdownOptions;
|
||||
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('activities')
|
||||
->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 "<a class='text-brandColor hover:underline' href='".$route."'>".$row->created_by.'</a>';
|
||||
},
|
||||
]);
|
||||
|
||||
$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 "<span class='text-gray-800 dark:text-gray-300'>N/A</span>";
|
||||
}
|
||||
|
||||
$route = urldecode(route('admin.leads.view', $row->lead_id));
|
||||
|
||||
return "<a class='text-brandColor hover:underline' target='_blank' href='".$route."'>".$row->lead_title.'</a>';
|
||||
},
|
||||
]);
|
||||
|
||||
$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,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Contact;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class OrganizationDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('organizations')
|
||||
->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
240
packages/Webkul/Admin/src/DataGrids/Contact/PersonDataGrid.php
Normal file
240
packages/Webkul/Admin/src/DataGrids/Contact/PersonDataGrid.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Contact;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Attribute\Models\Attribute;
|
||||
use Webkul\Attribute\Models\AttributeValue;
|
||||
use Webkul\Contact\Repositories\OrganizationRepository;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
use Webkul\Tag\Repositories\TagRepository;
|
||||
|
||||
class PersonDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Create a new class instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected OrganizationRepository $organizationRepository) {}
|
||||
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('persons')
|
||||
->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
388
packages/Webkul/Admin/src/DataGrids/Lead/LeadDataGrid.php
Normal file
388
packages/Webkul/Admin/src/DataGrids/Lead/LeadDataGrid.php
Normal file
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Lead;
|
||||
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Attribute\Models\Attribute;
|
||||
use Webkul\Attribute\Models\AttributeValue;
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
use Webkul\Contract\Repositories\Pipeline;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
use Webkul\Lead\Repositories\PipelineRepository;
|
||||
use Webkul\Lead\Repositories\SourceRepository;
|
||||
use Webkul\Lead\Repositories\StageRepository;
|
||||
use Webkul\Lead\Repositories\TypeRepository;
|
||||
use Webkul\Tag\Repositories\TagRepository;
|
||||
use Webkul\User\Repositories\UserRepository;
|
||||
|
||||
class LeadDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Pipeline instance.
|
||||
*
|
||||
* @var Pipeline
|
||||
*/
|
||||
protected $pipeline;
|
||||
|
||||
/**
|
||||
* Create data grid instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected PipelineRepository $pipelineRepository,
|
||||
protected StageRepository $stageRepository,
|
||||
protected SourceRepository $sourceRepository,
|
||||
protected TypeRepository $typeRepository,
|
||||
protected UserRepository $userRepository,
|
||||
protected TagRepository $tagRepository,
|
||||
) {
|
||||
if (request('pipeline_id')) {
|
||||
$this->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 "<a class=\"text-brandColor transition-all hover:underline\" href='".$route."'>".$row->person_name.'</a>';
|
||||
},
|
||||
]);
|
||||
|
||||
$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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
226
packages/Webkul/Admin/src/DataGrids/Mail/EmailDataGrid.php
Normal file
226
packages/Webkul/Admin/src/DataGrids/Mail/EmailDataGrid.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Mail;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
use Webkul\Tag\Repositories\TagRepository;
|
||||
|
||||
class EmailDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Default sort column of datagrid.
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
protected $sortColumn = 'created_at';
|
||||
|
||||
/**
|
||||
* Tags of the current page's emails, keyed by email id.
|
||||
*/
|
||||
protected array $emailTags = [];
|
||||
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('emails')
|
||||
->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 ? '<i class="icon-attachment text-2xl"></i>' : '',
|
||||
]);
|
||||
|
||||
$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',
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
174
packages/Webkul/Admin/src/DataGrids/Product/ProductDataGrid.php
Normal file
174
packages/Webkul/Admin/src/DataGrids/Product/ProductDataGrid.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Product;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
use Webkul\Tag\Repositories\TagRepository;
|
||||
|
||||
class ProductDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$tablePrefix = DB::getTablePrefix();
|
||||
|
||||
$queryBuilder = DB::table('products')
|
||||
->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
244
packages/Webkul/Admin/src/DataGrids/Quote/QuoteDataGrid.php
Normal file
244
packages/Webkul/Admin/src/DataGrids/Quote/QuoteDataGrid.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Quote;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
use Webkul\User\Repositories\UserRepository;
|
||||
|
||||
class QuoteDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$tablePrefix = DB::getTablePrefix();
|
||||
|
||||
$queryBuilder = DB::table('quotes')
|
||||
->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 "<a class=\"text-brandColor transition-all hover:underline\" href='".$route."'>".$row->person_name.'</a>';
|
||||
},
|
||||
]);
|
||||
|
||||
$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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class AttributeDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('attributes')
|
||||
->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings\DataTransfer;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class ImportDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
return DB::table('imports')
|
||||
->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 '<a href="'.route('admin.settings.data_transfer.imports.download', $row->id).'" class="cursor-pointer text-blue-600 hover:underline">'.$row->file_path.'<a>';
|
||||
},
|
||||
]);
|
||||
|
||||
$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 '<a href="'.route('admin.settings.data_transfer.imports.download_error_report', $row->id).'" class="cursor-pointer text-blue-600 hover:underline">'.$row->error_file_path.'<a>';
|
||||
},
|
||||
]);
|
||||
|
||||
$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);
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class EmailTemplateDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder()
|
||||
{
|
||||
$queryBuilder = DB::table('email_templates')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class GroupDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('groups')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings\Marketing;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class CampaignDatagrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder()
|
||||
{
|
||||
$queryBuilder = DB::table('marketing_campaigns')
|
||||
->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings\Marketing;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class EventDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder()
|
||||
{
|
||||
$queryBuilder = DB::table('marketing_events')
|
||||
->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class PipelineDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('lead_pipelines')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
packages/Webkul/Admin/src/DataGrids/Settings/RoleDataGrid.php
Normal file
120
packages/Webkul/Admin/src/DataGrids/Settings/RoleDataGrid.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class RoleDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('roles')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class SourceDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('lead_sources')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
packages/Webkul/Admin/src/DataGrids/Settings/TagDataGrid.php
Normal file
124
packages/Webkul/Admin/src/DataGrids/Settings/TagDataGrid.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class TagDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('tags')
|
||||
->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class TypeDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('lead_types')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
190
packages/Webkul/Admin/src/DataGrids/Settings/UserDataGrid.php
Normal file
190
packages/Webkul/Admin/src/DataGrids/Settings/UserDataGrid.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
use Webkul\User\Repositories\GroupRepository;
|
||||
|
||||
class UserDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Create data grid instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected GroupRepository $groupRepository) {}
|
||||
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$tablePrefix = DB::getTablePrefix();
|
||||
|
||||
$queryBuilder = DB::table('users')
|
||||
->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,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class WarehouseDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('warehouses')
|
||||
->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);
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class WebhookDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('webhooks')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\DataGrids\Settings;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\DataGrid\DataGrid;
|
||||
|
||||
class WorkflowDataGrid extends DataGrid
|
||||
{
|
||||
/**
|
||||
* Prepare query builder.
|
||||
*/
|
||||
public function prepareQueryBuilder(): Builder
|
||||
{
|
||||
$queryBuilder = DB::table('workflows')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('attributes')->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() {}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('core_config', function (Blueprint $table) {
|
||||
$table->text('value')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('core_config', function (Blueprint $table) {
|
||||
$table->text('value')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
119
packages/Webkul/Admin/src/Exceptions/Handler.php
Executable file
119
packages/Webkul/Admin/src/Exceptions/Handler.php
Executable file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Exceptions;
|
||||
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use PDOException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* Json error messages.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $jsonErrorMessages = [];
|
||||
|
||||
/**
|
||||
* Create handler instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
|
||||
$this->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'));
|
||||
}
|
||||
}
|
||||
18
packages/Webkul/Admin/src/Facades/Bouncer.php
Executable file
18
packages/Webkul/Admin/src/Facades/Bouncer.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Bouncer extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'bouncer';
|
||||
}
|
||||
}
|
||||
149
packages/Webkul/Admin/src/Helpers/Dashboard.php
Normal file
149
packages/Webkul/Admin/src/Helpers/Dashboard.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Helpers;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Webkul\Admin\Helpers\Reporting\Activity;
|
||||
use Webkul\Admin\Helpers\Reporting\Lead;
|
||||
use Webkul\Admin\Helpers\Reporting\Organization;
|
||||
use Webkul\Admin\Helpers\Reporting\Person;
|
||||
use Webkul\Admin\Helpers\Reporting\Product;
|
||||
use Webkul\Admin\Helpers\Reporting\Quote;
|
||||
|
||||
class Dashboard
|
||||
{
|
||||
/**
|
||||
* Create a controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected Lead $leadReporting,
|
||||
protected Activity $activityReporting,
|
||||
protected Product $productReporting,
|
||||
protected Person $personReporting,
|
||||
protected Organization $organizationReporting,
|
||||
protected Quote $quoteReporting,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the overall revenue statistics.
|
||||
*/
|
||||
public function getRevenueStats(): array
|
||||
{
|
||||
return [
|
||||
'total_won_revenue' => $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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Helpers\Reporting;
|
||||
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class AbstractReporting
|
||||
{
|
||||
/**
|
||||
* Number of days used when the configured range is missing or unusable.
|
||||
*/
|
||||
public const FALLBACK_RANGE_IN_DAYS = 30;
|
||||
|
||||
/**
|
||||
* The starting date for a given period.
|
||||
*/
|
||||
protected Carbon $startDate;
|
||||
|
||||
/**
|
||||
* The ending date for a given period.
|
||||
*/
|
||||
protected Carbon $endDate;
|
||||
|
||||
/**
|
||||
* The starting date for the previous period.
|
||||
*/
|
||||
protected Carbon $lastStartDate;
|
||||
|
||||
/**
|
||||
* The ending date for the previous period.
|
||||
*/
|
||||
protected Carbon $lastEndDate;
|
||||
|
||||
/**
|
||||
* Create a helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
5
packages/Webkul/Admin/src/Helpers/Reporting/Activity.php
Normal file
5
packages/Webkul/Admin/src/Helpers/Reporting/Activity.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Helpers\Reporting;
|
||||
|
||||
class Activity extends AbstractReporting {}
|
||||
503
packages/Webkul/Admin/src/Helpers/Reporting/Lead.php
Normal file
503
packages/Webkul/Admin/src/Helpers/Reporting/Lead.php
Normal file
@@ -0,0 +1,503 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Helpers\Reporting;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
use Webkul\Lead\Repositories\PipelineRepository;
|
||||
use Webkul\Lead\Repositories\StageRepository;
|
||||
|
||||
class Lead extends AbstractReporting
|
||||
{
|
||||
/**
|
||||
* The channel ids.
|
||||
*/
|
||||
protected array $stageIds;
|
||||
|
||||
/**
|
||||
* The all stage ids.
|
||||
*/
|
||||
protected array $allStageIds;
|
||||
|
||||
/**
|
||||
* The won stage ids.
|
||||
*/
|
||||
protected array $wonStageIds;
|
||||
|
||||
/**
|
||||
* The lost stage ids.
|
||||
*/
|
||||
protected array $lostStageIds;
|
||||
|
||||
/**
|
||||
* The pipeline the dashboard is scoped to.
|
||||
*/
|
||||
protected $pipeline;
|
||||
|
||||
/**
|
||||
* Create a helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected LeadRepository $leadRepository,
|
||||
protected StageRepository $stageRepository,
|
||||
protected PipelineRepository $pipelineRepository
|
||||
) {
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
}
|
||||
81
packages/Webkul/Admin/src/Helpers/Reporting/Organization.php
Normal file
81
packages/Webkul/Admin/src/Helpers/Reporting/Organization.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Helpers\Reporting;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Contact\Repositories\OrganizationRepository;
|
||||
|
||||
class Organization extends AbstractReporting
|
||||
{
|
||||
/**
|
||||
* Create a helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected OrganizationRepository $organizationRepository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves total organizations and their progress.
|
||||
*/
|
||||
public function getTotalOrganizationsProgress(): array
|
||||
{
|
||||
return [
|
||||
'previous' => $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;
|
||||
}
|
||||
}
|
||||
82
packages/Webkul/Admin/src/Helpers/Reporting/Person.php
Normal file
82
packages/Webkul/Admin/src/Helpers/Reporting/Person.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Helpers\Reporting;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
|
||||
class Person extends AbstractReporting
|
||||
{
|
||||
/**
|
||||
* Create a helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected PersonRepository $personRepository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves total persons and their progress.
|
||||
*/
|
||||
public function getTotalPersonsProgress(): array
|
||||
{
|
||||
return [
|
||||
'previous' => $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;
|
||||
}
|
||||
}
|
||||
94
packages/Webkul/Admin/src/Helpers/Reporting/Product.php
Normal file
94
packages/Webkul/Admin/src/Helpers/Reporting/Product.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Helpers\Reporting;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webkul\Lead\Repositories\ProductRepository;
|
||||
|
||||
class Product extends AbstractReporting
|
||||
{
|
||||
/**
|
||||
* Create a helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ProductRepository $productRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets top-selling products by revenue.
|
||||
*
|
||||
* @param int $limit
|
||||
*/
|
||||
public function getTopSellingProductsByRevenue($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.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;
|
||||
}
|
||||
}
|
||||
45
packages/Webkul/Admin/src/Helpers/Reporting/Quote.php
Normal file
45
packages/Webkul/Admin/src/Helpers/Reporting/Quote.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Helpers\Reporting;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Webkul\Quote\Repositories\QuoteRepository;
|
||||
|
||||
class Quote extends AbstractReporting
|
||||
{
|
||||
/**
|
||||
* Create a helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected QuoteRepository $quoteRepository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves total quotes and their progress.
|
||||
*/
|
||||
public function getTotalQuotesProgress(): array
|
||||
{
|
||||
return [
|
||||
'previous' => $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();
|
||||
}
|
||||
}
|
||||
286
packages/Webkul/Admin/src/Http/Controllers/Activity/ActivityController.php
Executable file
286
packages/Webkul/Admin/src/Http/Controllers/Activity/ActivityController.php
Executable file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Activity;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Webkul\Activity\Repositories\ActivityRepository;
|
||||
use Webkul\Activity\Repositories\FileRepository;
|
||||
use Webkul\Admin\DataGrids\Activity\ActivityDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Admin\Http\Requests\MassUpdateRequest;
|
||||
use Webkul\Admin\Http\Resources\ActivityResource;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
|
||||
class ActivityController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ActivityRepository $activityRepository,
|
||||
protected FileRepository $fileRepository,
|
||||
protected AttributeRepository $attributeRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Resolve the user ids that own or participate in the given activity (owner or participant).
|
||||
*/
|
||||
private function activityOwnerIds($activity): array
|
||||
{
|
||||
return array_merge(
|
||||
[$activity->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Configuration;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\ConfigurationForm;
|
||||
use Webkul\Core\Repositories\CoreConfigRepository as ConfigurationRepository;
|
||||
|
||||
class ConfigurationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected ConfigurationRepository $configurationRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
if (
|
||||
request()->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Contact;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Contact\OrganizationDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\AttributeForm;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Contact\Repositories\OrganizationRepository;
|
||||
|
||||
class OrganizationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected OrganizationRepository $organizationRepository)
|
||||
{
|
||||
request()->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Contact\Persons;
|
||||
|
||||
use Webkul\Activity\Repositories\ActivityRepository;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Traits\HandlesActivityTimeline;
|
||||
use Webkul\Email\Repositories\AttachmentRepository;
|
||||
|
||||
class ActivityController extends Controller
|
||||
{
|
||||
use HandlesActivityTimeline;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ActivityRepository $activityRepository,
|
||||
protected AttachmentRepository $attachmentRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The pivot linking activities to the person.
|
||||
*/
|
||||
protected function timelinePivot(): array
|
||||
{
|
||||
return ['table' => 'person_activities', 'foreign' => 'person_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emails are linked to the person through the `person_id` column.
|
||||
*/
|
||||
protected function timelineEmailColumn(): ?string
|
||||
{
|
||||
return 'person_id';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Contact\Persons;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Webkul\Admin\DataGrids\Contact\PersonDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\GoogleContact\Jobs\Export\PrepareExportBatch;
|
||||
use Webkul\GoogleContact\Models\ContactExportBatch;
|
||||
use Webkul\GoogleContact\Repositories\ContactExportBatchRepository;
|
||||
use Webkul\GoogleContact\Repositories\GoogleContactAccountRepository;
|
||||
|
||||
class GoogleContactExportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected GoogleContactAccountRepository $googleContactAccountRepository,
|
||||
protected ContactExportBatchRepository $contactExportBatchRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Kick off an async export of every person matching the current datagrid
|
||||
* filters/sort (mirrors the CSV/XLS export, not a row-selection action)
|
||||
* to the current user's connected Google account.
|
||||
*/
|
||||
public function store(Request $request, PersonDataGrid $personDataGrid): JsonResponse
|
||||
{
|
||||
$request->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Contact\Persons;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Prettus\Repository\Criteria\RequestCriteria;
|
||||
use Webkul\Admin\DataGrids\Contact\PersonDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\AttributeForm;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Admin\Http\Resources\PersonResource;
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
|
||||
class PersonController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new class instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected PersonRepository $personRepository)
|
||||
{
|
||||
request()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Contact\Persons;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected PersonRepository $personRepository) {}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function attach(int $id): JsonResponse
|
||||
{
|
||||
Event::dispatch('persons.tag.create.before', $id);
|
||||
|
||||
$person = $this->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
67
packages/Webkul/Admin/src/Http/Controllers/Controller.php
Executable file
67
packages/Webkul/Admin/src/Http/Controllers/Controller.php
Executable file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function redirectToLogin()
|
||||
{
|
||||
return redirect()->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();
|
||||
}
|
||||
}
|
||||
131
packages/Webkul/Admin/src/Http/Controllers/DashboardController.php
Executable file
131
packages/Webkul/Admin/src/Http/Controllers/DashboardController.php
Executable file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Webkul\Admin\Helpers\Dashboard;
|
||||
use Webkul\Core\Traits\PDFHandler;
|
||||
use Webkul\Lead\Repositories\PipelineRepository;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use PDFHandler;
|
||||
|
||||
/**
|
||||
* Request param functions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $typeFunctions = [
|
||||
'over-all' => '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}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\DataGrid;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\DataGrid\Repositories\SavedFilterRepository;
|
||||
|
||||
class SavedFilterController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*/
|
||||
public function __construct(protected SavedFilterRepository $savedFilterRepository) {}
|
||||
|
||||
/**
|
||||
* Save filters to the database.
|
||||
*/
|
||||
public function store()
|
||||
{
|
||||
$userId = auth()->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class DataGridController extends Controller
|
||||
{
|
||||
/**
|
||||
* Look up.
|
||||
*/
|
||||
public function lookUp()
|
||||
{
|
||||
/**
|
||||
* Validation for parameters.
|
||||
*/
|
||||
$params = $this->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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HelpController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the help & resources page.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin::help.index', [
|
||||
'support' => config('support'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Lead;
|
||||
|
||||
use Webkul\Activity\Repositories\ActivityRepository;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Traits\HandlesActivityTimeline;
|
||||
use Webkul\Email\Repositories\AttachmentRepository;
|
||||
|
||||
class ActivityController extends Controller
|
||||
{
|
||||
use HandlesActivityTimeline;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ActivityRepository $activityRepository,
|
||||
protected AttachmentRepository $attachmentRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The pivot linking activities to the lead.
|
||||
*/
|
||||
protected function timelinePivot(): array
|
||||
{
|
||||
return ['table' => 'lead_activities', 'foreign' => 'lead_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emails are linked to the lead through the `lead_id` column.
|
||||
*/
|
||||
protected function timelineEmailColumn(): ?string
|
||||
{
|
||||
return 'lead_id';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Lead;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Admin\Http\Controllers\Mail\EmailController as BaseEmailController;
|
||||
use Webkul\Admin\Http\Resources\ActivityResource;
|
||||
|
||||
class EmailController extends BaseEmailController
|
||||
{
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function store()
|
||||
{
|
||||
$response = json_decode(parent::store()->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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
848
packages/Webkul/Admin/src/Http/Controllers/Lead/LeadController.php
Executable file
848
packages/Webkul/Admin/src/Http/Controllers/Lead/LeadController.php
Executable file
@@ -0,0 +1,848 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Lead;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\View\View;
|
||||
use Prettus\Repository\Criteria\RequestCriteria;
|
||||
use Webkul\Admin\DataGrids\Lead\LeadDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\LeadForm;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Admin\Http\Requests\MassUpdateRequest;
|
||||
use Webkul\Admin\Http\Resources\LeadResource;
|
||||
use Webkul\Admin\Http\Resources\StageResource;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
use Webkul\DataGrid\ColumnTypes\Date as DateColumn;
|
||||
use Webkul\DataGrid\Enums\DateRangeOptionEnum;
|
||||
use Webkul\Lead\Helpers\MagicAI;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
use Webkul\Lead\Repositories\PipelineRepository;
|
||||
use Webkul\Lead\Repositories\ProductRepository;
|
||||
use Webkul\Lead\Repositories\SourceRepository;
|
||||
use Webkul\Lead\Repositories\StageRepository;
|
||||
use Webkul\Lead\Repositories\TypeRepository;
|
||||
use Webkul\Lead\Services\MagicAIService;
|
||||
use Webkul\Quote\Repositories\QuoteItemRepository;
|
||||
use Webkul\Quote\Repositories\QuoteRepository;
|
||||
use Webkul\Tag\Repositories\TagRepository;
|
||||
use Webkul\User\Repositories\UserRepository;
|
||||
|
||||
class LeadController extends Controller
|
||||
{
|
||||
/**
|
||||
* Const variable for supported types.
|
||||
*/
|
||||
const SUPPORTED_TYPES = 'pdf,bmp,jpeg,jpg,png,webp';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected UserRepository $userRepository,
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected SourceRepository $sourceRepository,
|
||||
protected TypeRepository $typeRepository,
|
||||
protected PipelineRepository $pipelineRepository,
|
||||
protected StageRepository $stageRepository,
|
||||
protected LeadRepository $leadRepository,
|
||||
protected ProductRepository $productRepository,
|
||||
protected QuoteItemRepository $quoteItemRepository,
|
||||
protected QuoteRepository $quoteRepository,
|
||||
protected PersonRepository $personRepository
|
||||
) {
|
||||
request()->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Lead;
|
||||
|
||||
use ArPHP\I18N\Arabic;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Mpdf\Mpdf;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Notifications\Common;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
use Webkul\Quote\Repositories\QuoteRepository;
|
||||
|
||||
class QuoteController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected LeadRepository $leadRepository,
|
||||
protected QuoteRepository $quoteRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function store($id)
|
||||
{
|
||||
$lead = $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Lead;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected LeadRepository $leadRepository) {}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function attach($id)
|
||||
{
|
||||
Event::dispatch('leads.tag.create.before', $id);
|
||||
|
||||
$lead = $this->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Mail;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Mail\EmailDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Admin\Http\Requests\MassUpdateRequest;
|
||||
use Webkul\Admin\Http\Resources\EmailResource;
|
||||
use Webkul\Email\Enums\SupportedFolderEnum;
|
||||
use Webkul\Email\InboundEmailProcessor\Contracts\InboundEmailProcessor;
|
||||
use Webkul\Email\Mails\Email;
|
||||
use Webkul\Email\Repositories\AttachmentRepository;
|
||||
use Webkul\Email\Repositories\EmailRepository;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
|
||||
class EmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected LeadRepository $leadRepository,
|
||||
protected EmailRepository $emailRepository,
|
||||
protected AttachmentRepository $attachmentRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse|RedirectResponse
|
||||
{
|
||||
$route = request('route');
|
||||
|
||||
if (! $route) {
|
||||
return redirect()->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Mail;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Email\Repositories\EmailRepository;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected EmailRepository $emailRepository) {}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function attach(int $id): JsonResponse
|
||||
{
|
||||
Event::dispatch('mails.tag.create.before', $id);
|
||||
|
||||
$mail = $this->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Products;
|
||||
|
||||
use Webkul\Activity\Repositories\ActivityRepository;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Traits\HandlesActivityTimeline;
|
||||
|
||||
class ActivityController extends Controller
|
||||
{
|
||||
use HandlesActivityTimeline;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ActivityRepository $activityRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The pivot linking activities to the product.
|
||||
*/
|
||||
protected function timelinePivot(): array
|
||||
{
|
||||
return ['table' => 'product_activities', 'foreign' => 'product_id'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Products;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Prettus\Repository\Criteria\RequestCriteria;
|
||||
use Webkul\Admin\DataGrids\Product\ProductDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\AttributeForm;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Admin\Http\Resources\ProductResource;
|
||||
use Webkul\Product\Repositories\ProductRepository;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected ProductRepository $productRepository)
|
||||
{
|
||||
request()->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Products;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Product\Repositories\ProductRepository;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected ProductRepository $productRepository) {}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function attach($id)
|
||||
{
|
||||
Event::dispatch('products.tag.create.before', $id);
|
||||
|
||||
$product = $this->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Quote;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Prettus\Repository\Criteria\RequestCriteria;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Webkul\Admin\DataGrids\Quote\QuoteDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\AttributeForm;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Admin\Http\Resources\QuoteResource;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Core\Traits\PDFHandler;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
use Webkul\Quote\Repositories\QuoteRepository;
|
||||
|
||||
class QuoteController extends Controller
|
||||
{
|
||||
use PDFHandler;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected QuoteRepository $quoteRepository,
|
||||
protected LeadRepository $leadRepository,
|
||||
protected AttributeRepository $attributeRepository
|
||||
) {
|
||||
request()->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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\AttributeDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Attribute\Repositories\AttributeValueRepository;
|
||||
use Webkul\Core\Contracts\Validations\Code;
|
||||
|
||||
class AttributeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected AttributeValueRepository $attributeValueRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings\DataTransfer;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\DataTransfer\ImportDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\DataTransfer\Helpers\Import;
|
||||
use Webkul\DataTransfer\Repositories\ImportRepository;
|
||||
|
||||
class ImportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImportRepository $importRepository,
|
||||
protected Import $importHelper,
|
||||
protected AttributeRepository $attributeRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
133
packages/Webkul/Admin/src/Http/Controllers/Settings/EmailTemplateController.php
Executable file
133
packages/Webkul/Admin/src/Http/Controllers/Settings/EmailTemplateController.php
Executable file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\EmailTemplateDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Automation\Helpers\Entity;
|
||||
use Webkul\EmailTemplate\Repositories\EmailTemplateRepository;
|
||||
|
||||
class EmailTemplateController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected EmailTemplateRepository $emailTemplateRepository,
|
||||
protected Entity $workflowEntityHelper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the email template.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Google\Service\Oauth2;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\GoogleContact\Repositories\GoogleContactAccountRepository;
|
||||
use Webkul\GoogleContact\Services\GoogleClientFactory;
|
||||
|
||||
class GoogleContactController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected GoogleContactAccountRepository $googleContactAccountRepository,
|
||||
protected GoogleClientFactory $googleClientFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display the Google Contacts connection settings.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$account = $this->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');
|
||||
}
|
||||
}
|
||||
128
packages/Webkul/Admin/src/Http/Controllers/Settings/GroupController.php
Executable file
128
packages/Webkul/Admin/src/Http/Controllers/Settings/GroupController.php
Executable file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\GroupDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\User\Repositories\GroupRepository;
|
||||
|
||||
class GroupController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected GroupRepository $groupRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Prettus\Repository\Criteria\RequestCriteria;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\AttributeForm;
|
||||
use Webkul\Warehouse\Repositories\LocationRepository;
|
||||
|
||||
class LocationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected LocationRepository $locationRepository) {}
|
||||
|
||||
/**
|
||||
* Search location results
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function search()
|
||||
{
|
||||
$results = $this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings\Marketing;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\Marketing\CampaignDatagrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\EmailTemplate\Repositories\EmailTemplateRepository;
|
||||
use Webkul\Marketing\Repositories\CampaignRepository;
|
||||
use Webkul\Marketing\Repositories\EventRepository;
|
||||
|
||||
class CampaignsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create new a controller instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected CampaignRepository $campaignRepository,
|
||||
protected EventRepository $eventRepository,
|
||||
protected EmailTemplateRepository $emailTemplateRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the marketing campaigns.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings\Marketing;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\Marketing\EventDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Marketing\Repositories\EventRepository;
|
||||
|
||||
class EventController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*/
|
||||
public function __construct(protected EventRepository $eventRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the marketing events.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\PipelineDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\PipelineForm;
|
||||
use Webkul\Lead\Repositories\PipelineRepository;
|
||||
|
||||
class PipelineController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected PipelineRepository $pipelineRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
283
packages/Webkul/Admin/src/Http/Controllers/Settings/RoleController.php
Executable file
283
packages/Webkul/Admin/src/Http/Controllers/Settings/RoleController.php
Executable file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\RoleDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\User\Contracts\Role;
|
||||
use Webkul\User\Repositories\RoleRepository;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected RoleRepository $roleRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Core\Menu\MenuItem;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('admin::settings.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for settings.
|
||||
*/
|
||||
public function search(): ?JsonResponse
|
||||
{
|
||||
$query = strtolower(request()->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<int, MenuItem> $menuItems
|
||||
* @return Collection<int, array<string, mixed>>
|
||||
*/
|
||||
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');
|
||||
}
|
||||
}
|
||||
117
packages/Webkul/Admin/src/Http/Controllers/Settings/SourceController.php
Executable file
117
packages/Webkul/Admin/src/Http/Controllers/Settings/SourceController.php
Executable file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\SourceDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Lead\Repositories\SourceRepository;
|
||||
|
||||
class SourceController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected SourceRepository $sourceRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
177
packages/Webkul/Admin/src/Http/Controllers/Settings/TagController.php
Executable file
177
packages/Webkul/Admin/src/Http/Controllers/Settings/TagController.php
Executable file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Prettus\Repository\Criteria\RequestCriteria;
|
||||
use Webkul\Admin\DataGrids\Settings\TagDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Admin\Http\Resources\TagResource;
|
||||
use Webkul\Tag\Repositories\TagRepository;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected TagRepository $tagRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
packages/Webkul/Admin/src/Http/Controllers/Settings/TypeController.php
Executable file
110
packages/Webkul/Admin/src/Http/Controllers/Settings/TypeController.php
Executable file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\TypeDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Lead\Repositories\TypeRepository;
|
||||
|
||||
class TypeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected TypeRepository $typeRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the type.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
518
packages/Webkul/Admin/src/Http/Controllers/Settings/UserController.php
Executable file
518
packages/Webkul/Admin/src/Http/Controllers/Settings/UserController.php
Executable file
@@ -0,0 +1,518 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\View\View;
|
||||
use Prettus\Repository\Criteria\RequestCriteria;
|
||||
use Webkul\Admin\DataGrids\Settings\UserDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\MassDestroyRequest;
|
||||
use Webkul\Admin\Http\Requests\MassUpdateRequest;
|
||||
use Webkul\Admin\Http\Resources\UserResource;
|
||||
use Webkul\Admin\Notifications\User\Create as UserCreatedNotification;
|
||||
use Webkul\User\Contracts\User;
|
||||
use Webkul\User\Repositories\GroupRepository;
|
||||
use Webkul\User\Repositories\RoleRepository;
|
||||
use Webkul\User\Repositories\UserRepository;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected UserRepository $userRepository,
|
||||
protected GroupRepository $groupRepository,
|
||||
protected RoleRepository $roleRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings\Warehouse;
|
||||
|
||||
use Webkul\Activity\Repositories\ActivityRepository;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Traits\HandlesActivityTimeline;
|
||||
|
||||
class ActivityController extends Controller
|
||||
{
|
||||
use HandlesActivityTimeline;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ActivityRepository $activityRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The pivot linking activities to the warehouse.
|
||||
*/
|
||||
protected function timelinePivot(): array
|
||||
{
|
||||
return ['table' => 'warehouse_activities', 'foreign' => 'warehouse_id'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings\Warehouse;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Warehouse\Repositories\WarehouseRepository;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected WarehouseRepository $warehouseRepository) {}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function attach($id)
|
||||
{
|
||||
Event::dispatch('warehouse.tag.create.before', $id);
|
||||
|
||||
$warehouse = $this->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings\Warehouse;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Prettus\Repository\Criteria\RequestCriteria;
|
||||
use Webkul\Admin\DataGrids\Product\ProductDataGrid;
|
||||
use Webkul\Admin\DataGrids\Settings\WarehouseDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Http\Requests\AttributeForm;
|
||||
use Webkul\Warehouse\Repositories\WarehouseRepository;
|
||||
|
||||
class WarehouseController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected WarehouseRepository $warehouseRepository)
|
||||
{
|
||||
request()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
use Webkul\Lead\Repositories\PipelineRepository;
|
||||
use Webkul\Lead\Repositories\SourceRepository;
|
||||
use Webkul\Lead\Repositories\TypeRepository;
|
||||
use Webkul\WebForm\DataGrids\WebFormDataGrid;
|
||||
use Webkul\WebForm\Repositories\WebFormRepository;
|
||||
|
||||
class WebFormController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected WebFormRepository $webFormRepository,
|
||||
protected PersonRepository $personRepository,
|
||||
protected LeadRepository $leadRepository,
|
||||
protected PipelineRepository $pipelineRepository,
|
||||
protected SourceRepository $sourceRepository,
|
||||
protected TypeRepository $typeRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the email template.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\WebhookDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Requests\WebhookRequest;
|
||||
use Webkul\Automation\Repositories\WebhookRepository;
|
||||
|
||||
class WebhookController extends Controller
|
||||
{
|
||||
public function __construct(protected WebhookRepository $webhookRepository) {}
|
||||
|
||||
/**
|
||||
* Display the listing of the resource.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
119
packages/Webkul/Admin/src/Http/Controllers/Settings/WorkflowController.php
Executable file
119
packages/Webkul/Admin/src/Http/Controllers/Settings/WorkflowController.php
Executable file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\Settings;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\DataGrids\Settings\WorkflowDataGrid;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Automation\Repositories\WorkflowRepository;
|
||||
|
||||
class WorkflowController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected WorkflowRepository $workflowRepository) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the workflow.
|
||||
*/
|
||||
public function index(): View|JsonResponse
|
||||
{
|
||||
if (request()->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Webkul\Core\Traits\Sanitizer;
|
||||
|
||||
class TinyMCEController extends Controller
|
||||
{
|
||||
use Sanitizer;
|
||||
|
||||
/**
|
||||
* Storage folder path.
|
||||
*/
|
||||
private string $storagePath = 'tinymce';
|
||||
|
||||
/**
|
||||
* The image extensions allowed to be uploaded through the editor.
|
||||
*/
|
||||
private string $allowedExtensions = 'bmp,gif,jpeg,jpg,png,svg,webp';
|
||||
|
||||
/**
|
||||
* Upload file from tinymce.
|
||||
*/
|
||||
public function upload(): JsonResponse
|
||||
{
|
||||
$this->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),
|
||||
];
|
||||
}
|
||||
}
|
||||
98
packages/Webkul/Admin/src/Http/Controllers/User/AccountController.php
Executable file
98
packages/Webkul/Admin/src/Http/Controllers/User/AccountController.php
Executable file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\User;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function edit()
|
||||
{
|
||||
$user = auth()->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();
|
||||
}
|
||||
}
|
||||
76
packages/Webkul/Admin/src/Http/Controllers/User/ForgotPasswordController.php
Executable file
76
packages/Webkul/Admin/src/Http/Controllers/User/ForgotPasswordController.php
Executable file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\User;
|
||||
|
||||
use Illuminate\Contracts\Auth\PasswordBroker;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Admin\Notifications\User\UserResetPassword;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
use SendsPasswordResetEmails;
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if (auth()->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');
|
||||
}
|
||||
}
|
||||
110
packages/Webkul/Admin/src/Http/Controllers/User/ResetPasswordController.php
Executable file
110
packages/Webkul/Admin/src/Http/Controllers/User/ResetPasswordController.php
Executable file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\User;
|
||||
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\PasswordBroker;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
use ResetsPasswords;
|
||||
|
||||
/**
|
||||
* Display the password reset view for the given token.
|
||||
*
|
||||
* If no token is present, display the link request form.
|
||||
*
|
||||
* @param string|null $token
|
||||
* @return Factory|View
|
||||
*/
|
||||
public function create($token = null)
|
||||
{
|
||||
return view('admin::sessions.reset-password')->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');
|
||||
}
|
||||
}
|
||||
187
packages/Webkul/Admin/src/Http/Controllers/User/SessionController.php
Executable file
187
packages/Webkul/Admin/src/Http/Controllers/User/SessionController.php
Executable file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Controllers\User;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use Webkul\Admin\Http\Controllers\Controller;
|
||||
use Webkul\Core\Menu\MenuItem;
|
||||
|
||||
class SessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Number of failed attempts allowed per minute, per set of credentials and per address.
|
||||
*/
|
||||
private const MAX_ATTEMPTS_PER_CREDENTIAL = 5;
|
||||
|
||||
private const MAX_ATTEMPTS_PER_ADDRESS = 20;
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create(): RedirectResponse|View
|
||||
{
|
||||
if (auth()->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;
|
||||
}
|
||||
}
|
||||
191
packages/Webkul/Admin/src/Http/Middleware/Bouncer.php
Executable file
191
packages/Webkul/Admin/src/Http/Middleware/Bouncer.php
Executable file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class Bouncer
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string|null $guard
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, \Closure $next, $guard = 'user')
|
||||
{
|
||||
if (! auth()->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;
|
||||
}
|
||||
}
|
||||
40
packages/Webkul/Admin/src/Http/Middleware/Locale.php
Normal file
40
packages/Webkul/Admin/src/Http/Middleware/Locale.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Locale
|
||||
{
|
||||
/**
|
||||
* The middleware instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
Application $app,
|
||||
Request $request
|
||||
) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
60
packages/Webkul/Admin/src/Http/Middleware/SanitizeUrl.php
Normal file
60
packages/Webkul/Admin/src/Http/Middleware/SanitizeUrl.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Webkul\Email\Enums\SupportedFolderEnum;
|
||||
|
||||
class SanitizeUrl
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($request->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);
|
||||
}
|
||||
}
|
||||
120
packages/Webkul/Admin/src/Http/Requests/AttributeForm.php
Normal file
120
packages/Webkul/Admin/src/Http/Requests/AttributeForm.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Admin\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Attribute\Repositories\AttributeValueRepository;
|
||||
use Webkul\Core\Contracts\Validations\Decimal;
|
||||
|
||||
class AttributeForm extends FormRequest
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $rules = [];
|
||||
|
||||
/**
|
||||
* Create a new form request instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected AttributeValueRepository $attributeValueRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Determine if the product 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()
|
||||
{
|
||||
$attributes = $this->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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user