feat: sem codebase
This commit is contained in:
parent
b63462fc9e
commit
221d3f8173
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[compose.yaml]
|
||||||
|
indent_size = 4
|
||||||
65
.env.example
Normal file
65
.env.example
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
README.md export-ignore
|
||||||
|
.github/workflows/browser-tests.yml export-ignore
|
||||||
45
.github/workflows/lint.yml
vendored
Normal file
45
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: linter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||||
|
npm install
|
||||||
|
|
||||||
|
- name: Run Pint
|
||||||
|
run: vendor/bin/pint
|
||||||
|
|
||||||
|
- name: Format Frontend
|
||||||
|
run: npm run format
|
||||||
|
|
||||||
|
- name: Lint Frontend
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
# - name: Commit Changes
|
||||||
|
# uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
|
# with:
|
||||||
|
# commit_message: fix code style
|
||||||
|
# commit_options: '--no-verify'
|
||||||
50
.github/workflows/tests.yml
vendored
Normal file
50
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: 8.4
|
||||||
|
tools: composer:v2
|
||||||
|
coverage: xdebug
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Node Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
|
|
||||||
|
- name: Build Assets
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Copy Environment File
|
||||||
|
run: cp .env.example .env
|
||||||
|
|
||||||
|
- name: Generate Application Key
|
||||||
|
run: php artisan key:generate
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: ./vendor/bin/pest
|
||||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/.phpunit.cache
|
||||||
|
/bootstrap/ssr
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/resources/js/actions
|
||||||
|
/resources/js/routes
|
||||||
|
/resources/js/wayfinder
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
/auth.json
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
resources/js/components/ui/*
|
||||||
|
resources/views/mail/*
|
||||||
25
.prettierrc
Normal file
25
.prettierrc
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"printWidth": 80,
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-organize-imports",
|
||||||
|
"prettier-plugin-tailwindcss"
|
||||||
|
],
|
||||||
|
"tailwindFunctions": [
|
||||||
|
"clsx",
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"tailwindStylesheet": "resources/css/app.css",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "**/*.yml",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
39
app/Actions/Fortify/CreateNewUser.php
Normal file
39
app/Actions/Fortify/CreateNewUser.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||||
|
|
||||||
|
class CreateNewUser implements CreatesNewUsers
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and create a newly registered user.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function create(array $input): User
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class),
|
||||||
|
],
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
return User::create([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'email' => $input['email'],
|
||||||
|
'password' => $input['password'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/Actions/Fortify/PasswordValidationRules.php
Normal file
18
app/Actions/Fortify/PasswordValidationRules.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
trait PasswordValidationRules
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules used to validate passwords.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
protected function passwordRules(): array
|
||||||
|
{
|
||||||
|
return ['required', 'string', Password::default(), 'confirmed'];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Actions/Fortify/ResetUserPassword.php
Normal file
28
app/Actions/Fortify/ResetUserPassword.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||||
|
|
||||||
|
class ResetUserPassword implements ResetsUserPasswords
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and reset the user's forgotten password.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function reset(User $user, array $input): void
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => $input['password'],
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
110
app/Console/Commands/CampaignEndingNotifiy.php
Normal file
110
app/Console/Commands/CampaignEndingNotifiy.php
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use App\Models\ClientUserAssignation;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class CampaignEndingNotifiy extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'campaign:ending-notify';
|
||||||
|
protected $description = 'Notify about campaigns that are ending soon';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
|
||||||
|
$today = Carbon::today();
|
||||||
|
$threshold = Carbon::today()->addDays(30);
|
||||||
|
|
||||||
|
// Get all assignations with users
|
||||||
|
$assignations = ClientUserAssignation::with('user')->where('role',ClientUserAssignation::ROLE_ASSIGNED_PERSON)->get();
|
||||||
|
|
||||||
|
// Group assignations by user_id and role
|
||||||
|
$groupedAssignations = $assignations->groupBy(['user_id', 'role']);
|
||||||
|
|
||||||
|
foreach ($groupedAssignations as $userId => $roles) {
|
||||||
|
foreach ($roles as $role => $assigns) {
|
||||||
|
$clientIds = $assigns->pluck('client_id');
|
||||||
|
$clients = Client::whereIn('id', $clientIds)->where('status', 'ENABLED')->get();
|
||||||
|
|
||||||
|
$endingCampaigns = [];
|
||||||
|
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = $adsService->listCampaigns($client->customer_id);
|
||||||
|
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
if (empty($campaign['end_date'])) {
|
||||||
|
continue; // skip if no end date
|
||||||
|
}
|
||||||
|
|
||||||
|
$endDate = Carbon::parse($campaign['end_date']);
|
||||||
|
|
||||||
|
// Condition: ending within 30 days AND not already ended
|
||||||
|
if ($endDate->between($today, $threshold)) {
|
||||||
|
$endingCampaigns[] = [
|
||||||
|
'client' => $client,
|
||||||
|
'campaign' => $campaign,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the user if there are ending campaigns
|
||||||
|
if (!empty($endingCampaigns)) {
|
||||||
|
$user = $assigns->first()->user;
|
||||||
|
$roleLabel = $role == ClientUserAssignation::ROLE_ASSIGNED_PERSON ? 'Assigned Person' : 'Sales Person';
|
||||||
|
|
||||||
|
if ($user && !empty($user->email)) {
|
||||||
|
$mailData = [
|
||||||
|
'user_name' => $user->name,
|
||||||
|
'role_label' => $roleLabel,
|
||||||
|
'to_expiry' => array_map(function ($item) use ($today) {
|
||||||
|
return [
|
||||||
|
'id'=>$item['client']->customer_id,
|
||||||
|
'title' => $item['client']->name,
|
||||||
|
'name' => $item['campaign']['name'],
|
||||||
|
'iteration_end_date' => $item['end_date']->format('Y-m-d'),
|
||||||
|
'number_of_days' => $today->diffInDays($item['end_date']),
|
||||||
|
];
|
||||||
|
}, $endingCampaigns),
|
||||||
|
];
|
||||||
|
|
||||||
|
Mail::send('mail.campaign_expiry', ['data' => $mailData], function ($message) use ($user) {
|
||||||
|
$message->to($user->email, $user->name)
|
||||||
|
->subject('Campaign Expiry Alert');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->info("Sent campaign expiry email to {$user->email} ({$user->name}, {$roleLabel}).");
|
||||||
|
} else {
|
||||||
|
$this->warn("Skipping email for assignation group without valid email: user_id={$userId}, role={$role}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
Log::error('Error getting campaign details: ' . $e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Console/Commands/CompareCustomer.php
Normal file
47
app/Console/Commands/CompareCustomer.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Customers;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class CompareCustomer extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'customer:compare-customers';
|
||||||
|
protected $description = 'Compare customer details';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$customers = Customers::all();
|
||||||
|
|
||||||
|
foreach ($customers as $customer) {
|
||||||
|
$cleanedInput = preg_replace('/[^A-Za-z0-9]/', '', $customer->company_name);
|
||||||
|
$client = Client::whereRaw("REGEXP_REPLACE(name, '[^A-Za-z0-9]', '') = ?", [$cleanedInput])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($client) {
|
||||||
|
// Update exact match
|
||||||
|
$client->update([
|
||||||
|
'sql_acc_code' => $customer->sql_acc_code,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$this->info("Skipped Match Found for: {$customer->company_name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error project linkage : '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
126
app/Console/Commands/CreateClientInvoice.php
Normal file
126
app/Console/Commands/CreateClientInvoice.php
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Customers;
|
||||||
|
use App\Models\ClientInvoice;
|
||||||
|
use App\Models\ClientUserAssignation;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\ClientInvoiceApprovalService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Rap2hpoutre\FastExcel\FastExcel;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class CreateClientInvoice extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'customer:create-invoice';
|
||||||
|
protected $description = 'Create client invoice';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$approvalService = new ClientInvoiceApprovalService();
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$collection = (new FastExcel)->import(storage_path('app/public/csv/Fixed_EJ.csv'));
|
||||||
|
$array = $collection->toArray();
|
||||||
|
foreach ($array as $row) {
|
||||||
|
$startDate = Carbon::parse($row['start_date'])->format('Y-m-d');
|
||||||
|
$endDate = Carbon::parse($row['end_date'])->format('Y-m-d');
|
||||||
|
$client = Client::where('customer_id', str_replace('-', '', $row['customer_id']))->first();
|
||||||
|
if ($client) {
|
||||||
|
$client->update([
|
||||||
|
'industry' => $row['industry'],
|
||||||
|
]);
|
||||||
|
$salesUser = User::where('name', $row['sales'])->first();
|
||||||
|
$pic = User::where('name', $row['pic'])->first();
|
||||||
|
if ($pic) {
|
||||||
|
ClientUserAssignation::updateOrCreate(
|
||||||
|
[
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'user_id' => $pic->id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($salesUser) {
|
||||||
|
ClientUserAssignation::updateOrCreate(
|
||||||
|
[
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'role' => ClientUserAssignation::ROLE_SALES_PERSON,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'user_id' => $salesUser->id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$row['client_id'] = $client->id;
|
||||||
|
// if ($client->status != 'CANCELED') {
|
||||||
|
// $campaigns = $adsService->listCampaigns($row['customer_id']);
|
||||||
|
// Log::info('Hydrated client data', [
|
||||||
|
// 'campaigns' => $campaigns,
|
||||||
|
// ]);
|
||||||
|
// foreach ($campaigns as $campaign) {
|
||||||
|
// Log::info('Hydrated client data', [
|
||||||
|
// 'campaigns' => $campaign['id'],
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// if (empty($invoice->start_date) || empty($invoice->end_date)) {
|
||||||
|
// $totalSpend = 0;
|
||||||
|
// $spend += number_format($totalSpend, 2, '.', '');
|
||||||
|
// } else {
|
||||||
|
// $metrics = $adsService->listCampaignsMetricsById(
|
||||||
|
// $row['customer_id'],
|
||||||
|
// $campaign['id'],
|
||||||
|
// $startDate,
|
||||||
|
// $endDate
|
||||||
|
// );
|
||||||
|
// Log::info('Hydrated client data', [
|
||||||
|
// 'metrics' => $metrics,
|
||||||
|
// ]);
|
||||||
|
// $totalSpend = array_sum(array_column($metrics, 'actual_spend'));
|
||||||
|
// $spend += number_format($totalSpend, 2, '.', '');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
$spend = 0;
|
||||||
|
// }
|
||||||
|
$invoice = ClientInvoice::updateOrCreate(
|
||||||
|
['invoice_no' => $row['invoice_no']],
|
||||||
|
[
|
||||||
|
'client_id' => $row['client_id'],
|
||||||
|
'is_credit_card' => intval(str_replace(',', '',$row['media_fee'])) == 0 ? 1 : 0,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'management_fee' => intval(str_replace(',', '',$row['management_fee'])) ?? 0,
|
||||||
|
'media_fee' => intval(str_replace(',', '',$row['media_fee'])) ?? 0,
|
||||||
|
'tax_percent' => 8,
|
||||||
|
'nett_amount' => intval(str_replace(',', '',$row['media_fee'])) > 0 ? intval(str_replace(',', '',$row['media_fee'])) / 1.08 : 0,
|
||||||
|
'total_spending' => $spend,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$approvalService->approve($invoice);
|
||||||
|
} else {
|
||||||
|
Log::warning('Client not found for customer_id: '.str_replace('-', '', $row['customer_id']));
|
||||||
|
continue; // Skip this row if client not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error project linkage : '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Console/Commands/GetGoogleAdDetails.php
Normal file
73
app/Console/Commands/GetGoogleAdDetails.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleAd;
|
||||||
|
use App\Models\GoogleAdGroup;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleAdDetails extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-ads-details';
|
||||||
|
protected $description = 'Get ads details from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$adGroups = GoogleAdGroup::where('google_campaign_id', $campaign->id)->get();
|
||||||
|
if (! empty($adGroups)) {
|
||||||
|
foreach ($adGroups as $adGroup) {
|
||||||
|
$ads = $adsService->listAdsByAdGroupId($client->customer_id, $adGroup->ad_group_id);
|
||||||
|
if (! empty($ads)) {
|
||||||
|
foreach ($ads as $ad) {
|
||||||
|
GoogleAd::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_ad_group_id' => $adGroup->id,
|
||||||
|
'ad_id' => $ad['ad_id'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => $ad['type'],
|
||||||
|
'status'=> $ad['status'],
|
||||||
|
'approval_status' => $ad['approval_status'],
|
||||||
|
'final_urls' => json_encode($ad['final_urls']),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Fetched Ad ID: {$ad['ad_id']} for Ad Group: {$adGroup->name} in Campaign: {$campaign->name}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No ads found for Client ID: {$client->customer_id}, Ad Group Name: {$adGroup->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No ad group found for Client ID: {$client->customer_id}, Campaign Name: {$campaign->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/Console/Commands/GetGoogleAdGroupDetails.php
Normal file
66
app/Console/Commands/GetGoogleAdGroupDetails.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleAdGroup;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleAdGroupDetails extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-adgroup-details';
|
||||||
|
protected $description = 'Get ad group details from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
//Beginning of campaign
|
||||||
|
$adGroups = $adsService->listAdGroupsByCampaignId($client->customer_id, $campaign->campaign_id);
|
||||||
|
if (! empty($adGroups)) {
|
||||||
|
foreach ($adGroups as $adGroup) {
|
||||||
|
GoogleAdGroup::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_campaign_id' => $campaign->id,
|
||||||
|
'ad_group_id' => $adGroup['ad_group_id'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => $adGroup['ad_group_name'],
|
||||||
|
'status' => $adGroup['status'],
|
||||||
|
'type' => $adGroup['type'],
|
||||||
|
'cpc_bid_micros' => $adGroup['cpc_bid_micros'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Saved Ad Group: {$adGroup['ad_group_name']} for Campaign: {$campaign->name}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No ad group found for Client ID: {$client->customer_id}, Campaign Name: {$campaign->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/Console/Commands/GetGoogleAdGroupMetric.php
Normal file
80
app/Console/Commands/GetGoogleAdGroupMetric.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleAd;
|
||||||
|
use App\Models\GoogleAdGroup;
|
||||||
|
use App\Models\GoogleAdGroupMetric;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleCampaignMetric;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use App\Models\GoogleKeywordMetric;
|
||||||
|
use App\Models\GoogleKeywords;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleAdGroupMetric extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-adgroup-metric';
|
||||||
|
protected $description = 'Get ad group metric from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->where('customer_id', '2048068576')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$adGroups = GoogleAdGroup::where('google_campaign_id', $campaign->id)->get();
|
||||||
|
if ($adGroups->isEmpty()) {
|
||||||
|
$this->info("No ad groups found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($adGroups as $adGroup) {
|
||||||
|
$metrics = $adsService->getAdGroupMetricsById($client->customer_id, $adGroup->ad_group_id, '1970-01-01');
|
||||||
|
// $metrics = $adsService->getAdGroupMetricsById($client->customer_id, $adGroup->ad_group_id);
|
||||||
|
if (! empty($metrics)) {
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
GoogleAdGroupMetric::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_ad_group_id' => $adGroup->id,
|
||||||
|
'date' => $metric['date'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'impressions' => $metric['impressions'],
|
||||||
|
'clicks' => $metric['clicks'],
|
||||||
|
'actual_spend' => $metric['actual_spend'],
|
||||||
|
'conversions' => $metric['conversions'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Ad Group Metric for Ad Group ID: {$adGroup->ad_group_id} under Client ID: {$client->customer_id} for Date: {$metric['date']} has been updated/created.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->info("No metrics found for Ad Group ID: {$adGroup->ad_group_id} under Client ID: {$client->customer_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
88
app/Console/Commands/GetGoogleAdsMetric.php
Normal file
88
app/Console/Commands/GetGoogleAdsMetric.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleAd;
|
||||||
|
use App\Models\GoogleAdGroup;
|
||||||
|
use App\Models\GoogleAdGroupMetric;
|
||||||
|
use App\Models\GoogleAdMetric;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleCampaignMetric;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use App\Models\GoogleKeywordMetric;
|
||||||
|
use App\Models\GoogleKeywords;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleAdsMetric extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-ads-metric';
|
||||||
|
protected $description = 'Get ads metric from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->where('customer_id', '2048068576')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$adGroups = GoogleAdGroup::where('google_campaign_id', $campaign->id)->get();
|
||||||
|
if ($adGroups->isEmpty()) {
|
||||||
|
$this->info("No ad groups found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($adGroups as $adGroup) {
|
||||||
|
$ads = GoogleAd::where('google_ad_group_id', $adGroup->id)->get();
|
||||||
|
if ($ads->isEmpty()) {
|
||||||
|
$this->info("No ads found for Ad Group ID: {$adGroup->ad_group_id} under Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($ads as $ad) {
|
||||||
|
$metrics = $adsService->getAdMetricsById($client->customer_id, $ad->ad_id, '1970-01-01');
|
||||||
|
// $metrics = $adsService->getAdMetricsById($client->customer_id, $ad->ad_id);
|
||||||
|
if (! empty($metrics)) {
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
GoogleAdMetric::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_ad_id' => $ad->id,
|
||||||
|
'date' => $metric['date'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'impressions' => $metric['impressions'],
|
||||||
|
'clicks' => $metric['clicks'],
|
||||||
|
'actual_spend' => $metric['spend'],
|
||||||
|
'conversions' => $metric['conversions'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Ad Metric for Ad ID: {$ad->ad_id} under Client ID: {$client->customer_id} for Date: {$metric['date']} has been updated/created.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No metrics found for Ad ID: {$ad->ad_id} under Client ID: {$client->customer_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Console/Commands/GetGoogleAdsRefreshToken.php
Normal file
65
app/Console/Commands/GetGoogleAdsRefreshToken.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class GetGoogleAdsRefreshToken extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-refresh-token';
|
||||||
|
protected $description = 'Generate a Google Ads API refresh token';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$client_id = $this->ask('Enter your Google OAuth Client ID');
|
||||||
|
$client_secret = $this->ask('Enter your Google OAuth Client Secret');
|
||||||
|
$redirect_uri = $this->ask('Enter your Redirect URI (e.g., http://localhost/oauth2callback)');
|
||||||
|
|
||||||
|
$authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query([
|
||||||
|
'scope' => 'https://www.googleapis.com/auth/adwords',
|
||||||
|
'access_type' => 'offline',
|
||||||
|
'include_granted_scopes' => 'true',
|
||||||
|
'response_type' => 'code',
|
||||||
|
'client_id' => $client_id,
|
||||||
|
'redirect_uri' => $redirect_uri
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->info("Open this URL in your browser:");
|
||||||
|
$this->line($authUrl);
|
||||||
|
$this->info("After approving access, you will be redirected to your redirect URI with a 'code' parameter.");
|
||||||
|
|
||||||
|
$code = $this->ask('Enter the code from the URL');
|
||||||
|
|
||||||
|
// Exchange code for refresh token
|
||||||
|
$response = $this->postToken($client_id, $client_secret, $redirect_uri, $code);
|
||||||
|
|
||||||
|
if(isset($response['refresh_token'])) {
|
||||||
|
$this->info("Success! Your refresh token is:");
|
||||||
|
$this->line($response['refresh_token']);
|
||||||
|
} else {
|
||||||
|
$this->error('Failed to get refresh token.');
|
||||||
|
$this->line(json_encode($response, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function postToken($client_id, $client_secret, $redirect_uri, $code)
|
||||||
|
{
|
||||||
|
$ch = curl_init('https://oauth2.googleapis.com/token');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
|
||||||
|
'code' => $code,
|
||||||
|
'client_id' => $client_id,
|
||||||
|
'client_secret' => $client_secret,
|
||||||
|
'redirect_uri' => $redirect_uri,
|
||||||
|
'grant_type' => 'authorization_code'
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return json_decode($response, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
app/Console/Commands/GetGoogleAssetDetails.php
Normal file
71
app/Console/Commands/GetGoogleAssetDetails.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleAsset;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleCampaignMetric;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleAssetDetails extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-asset-details';
|
||||||
|
protected $description = 'Get asset details from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->where('customer_id', '4744166776')->get();
|
||||||
|
// $clients = Client::where('status', 'ENABLED')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$assets = $adsService->listAssetsByCampaignId($client->customer_id, $campaign->campaign_id);
|
||||||
|
if (! empty($assets)) {
|
||||||
|
foreach ($assets as $asset) {
|
||||||
|
// $this->info('Here:'.$asset['status']);
|
||||||
|
GoogleAsset::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_campaign_id' => $campaign->id,
|
||||||
|
'asset_id' => $asset['id'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => $asset['name'],
|
||||||
|
'type' => $asset['type'],
|
||||||
|
'added_by' => $asset['added_by'],
|
||||||
|
'use_case' => $asset['use_case'],
|
||||||
|
'approval_status' => $asset['approval_status'],
|
||||||
|
'review_status' => $asset['review_status'],
|
||||||
|
'status' => $asset['status'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Fetched Asset: {$asset['name']} (Type: {$asset['type']}) for Campaign: {$campaign->name}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No assets found for Client ID: {$client->customer_id}, Campaign Name: {$campaign->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
75
app/Console/Commands/GetGoogleAssetMetric.php
Normal file
75
app/Console/Commands/GetGoogleAssetMetric.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleAsset;
|
||||||
|
use App\Models\GoogleAssetMetric;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleCampaignMetric;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleAssetMetric extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-asset-metrics';
|
||||||
|
protected $description = 'Get asset metrics from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->where('customer_id', '4744166776')->get();
|
||||||
|
// $clients = Client::where('status', 'ENABLED')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$assets = GoogleAsset::where('google_campaign_id', $campaign->id)->get();
|
||||||
|
if (! empty($assets)) {
|
||||||
|
foreach ($assets as $asset) {
|
||||||
|
$metrics = $adsService->getAssetMetricsById($client->customer_id, $asset->asset_id, '1970-01-01');
|
||||||
|
if (! empty($metrics)) {
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
GoogleAssetMetric::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_asset_id' => $asset->id,
|
||||||
|
'date' => $metric['date'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'impressions' => $metric['impressions'],
|
||||||
|
'clicks' => $metric['clicks'],
|
||||||
|
'actual_spend' => $metric['cost'],
|
||||||
|
'conversions' => $metric['conversions'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Fetched Metric for Asset: {$asset->name} on Date: {$metric['date']} for Campaign: {$campaign->name}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No metrics found for Asset ID: {$asset->asset_id} in Campaign: {$campaign->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No assets found for Client ID: {$client->customer_id}, Campaign Name: {$campaign->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Console/Commands/GetGoogleCampaignDetails.php
Normal file
53
app/Console/Commands/GetGoogleCampaignDetails.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleCampaignDetails extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-campaign-details';
|
||||||
|
protected $description = 'Get campaign details from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
// $clients = Client::where('status','ENABLED')->get();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->where('customer_id', '4744166776')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = $adsService->listCampaigns($client->customer_id);
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$campaign = GoogleCampaign::updateOrCreate(
|
||||||
|
['client_id' => $client->id, 'campaign_id' => $campaign['id']],
|
||||||
|
[
|
||||||
|
'campaign_id' => $campaign['id'],
|
||||||
|
'name' => $campaign['name'],
|
||||||
|
'status' => $campaign['status'],
|
||||||
|
'channel' => $campaign['channel'],
|
||||||
|
'start_date' => $campaign['start_date'],
|
||||||
|
'end_date' => $campaign['end_date'],
|
||||||
|
'sub_channel' => $campaign['sub_channel'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Client ID: {$client->customer_id}, Campaign Name: {$campaign['name']}, Status: {$campaign['status']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
74
app/Console/Commands/GetGoogleCampaignMetric.php
Normal file
74
app/Console/Commands/GetGoogleCampaignMetric.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleCampaignMetric;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleCampaignMetric extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-campaign-metric';
|
||||||
|
protected $description = 'Get campaign metric from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
// $clients = Client::where('status', 'ENABLED')->get();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->where('customer_id', '4744166776')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
//Beginning of campaign
|
||||||
|
$metrics = $adsService->listCampaignsMetricsById($client->customer_id, $campaign->campaign_id, '1970-01-01');
|
||||||
|
// $metrics = $adsService->listCampaignsMetricsById($client->customer_id, $campaign->campaign_id);
|
||||||
|
if (! empty($metrics)) {
|
||||||
|
$this->info("Found metrics for Client ID: {$client->customer_id}, Campaign Name: {$campaign->name}");
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
GoogleCampaignMetric::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_campaign_id' => $campaign->id,
|
||||||
|
'date' => $metric['date'],
|
||||||
|
'google_campaign_metric_id' => $metric['id']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// 'google_campaign_metric_id' => $metric['id'],
|
||||||
|
'impressions' => $metric['impressions'],
|
||||||
|
'date' => $metric['date'],
|
||||||
|
'clicks' => $metric['clicks'],
|
||||||
|
'daily_budget' => $metric['daily_budget'],
|
||||||
|
'actual_spend' => $metric['actual_spend'],
|
||||||
|
'conversions' => $metric['conversions'],
|
||||||
|
'interactions' => $metric['interactions'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Client ID: {$client->customer_id}, Campaign Name: {$campaign->name},Date: {$metric['date']} , Impressions: {$metric['impressions']}, Clicks: {$metric['clicks']}, Cost: {$metric['actual_spend']}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No metrics found for Client ID: {$client->customer_id}, Campaign Name: {$campaign->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Console/Commands/GetGoogleCompanyDetails.php
Normal file
44
app/Console/Commands/GetGoogleCompanyDetails.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleCompanyDetails extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-company-details';
|
||||||
|
protected $description = 'Get company details from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching company details from Google Ads...");
|
||||||
|
$mccCustomerId = env('GOOGLE_ADS_LOGIN_CUSTOMER_ID'); // Manager ID without dashes
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$accounts = $adsService->listAccounts();
|
||||||
|
foreach ($accounts as $account) {
|
||||||
|
$company = Client::updateOrCreate(
|
||||||
|
['customer_id' => $account['id']],
|
||||||
|
[
|
||||||
|
'name' => $account['name'],
|
||||||
|
'status' => $account['status'],
|
||||||
|
'time_zone' => $account['time_zone'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Customer ID: {$account['customer_id']}, Name: {$account['name']}, Status: {$account['status']}");
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error creating project iteration: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Console/Commands/GetGoogleKeywordsDetails.php
Normal file
73
app/Console/Commands/GetGoogleKeywordsDetails.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleAd;
|
||||||
|
use App\Models\GoogleAdGroup;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use App\Models\GoogleKeywords;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleKeywordsDetails extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-keywords-details';
|
||||||
|
protected $description = 'Get keywords details from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->where('customer_id', '2048068576')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$adGroups = GoogleAdGroup::where('google_campaign_id', $campaign->id)->get();
|
||||||
|
if (! empty($adGroups)) {
|
||||||
|
foreach ($adGroups as $adGroup) {
|
||||||
|
$keywords = $adsService->listKeywordsByAdGroupId($client->customer_id, $adGroup->ad_group_id);
|
||||||
|
if (! empty($keywords)) {
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
GoogleKeywords::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_ad_group_id' => $adGroup->id,
|
||||||
|
'keyword_id' => $keyword['keyword_id'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'text' => $keyword['text'],
|
||||||
|
'match_type' => $keyword['match_type'],
|
||||||
|
'status' => $keyword['status'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Fetched Keyword: {$keyword['text']} (Match Type: {$keyword['match_type']}) for Ad Group: {$adGroup->name} in Campaign: {$campaign->name}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No keywords found for Client ID: {$client->customer_id}, Ad Group Name: {$adGroup->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No ad group found for Client ID: {$client->customer_id}, Campaign Name: {$campaign->name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
85
app/Console/Commands/GetGoogleKeywordsMetric.php
Normal file
85
app/Console/Commands/GetGoogleKeywordsMetric.php
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\GoogleAdGroup;
|
||||||
|
use App\Models\GoogleCampaign;
|
||||||
|
use App\Models\GoogleCampaignMetric;
|
||||||
|
use App\Models\GoogleClient;
|
||||||
|
use App\Models\GoogleKeywordMetric;
|
||||||
|
use App\Models\GoogleKeywords;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GetGoogleKeywordsMetric extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'google-ads:get-keywords-metric';
|
||||||
|
protected $description = 'Get keywords metric from Google Ads';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
$this->info("Fetching campaign details from Google Ads...");
|
||||||
|
$adsService = new GoogleAdsService();
|
||||||
|
$clients = Client::where('status', 'ENABLED')->where('customer_id', '2048068576')->get();
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$campaigns = GoogleCampaign::where('client_id', $client->id)->get();
|
||||||
|
if ($campaigns->isEmpty()) {
|
||||||
|
$this->info("No campaigns found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$adGroups = GoogleAdGroup::where('google_campaign_id', $campaign->id)->get();
|
||||||
|
if ($adGroups->isEmpty()) {
|
||||||
|
$this->info("No ad groups found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($adGroups as $adGroup) {
|
||||||
|
$keywords = GoogleKeywords::where('google_ad_group_id', $adGroup->id)->get();
|
||||||
|
if ($keywords->isEmpty()) {
|
||||||
|
$this->info("No keywords found for Client ID: {$client->customer_id}");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
$metrics = $adsService->getKeywordMetricsById($client->customer_id, $keyword->keyword_id, '1970-01-01');
|
||||||
|
// $metrics = $adsService->getKeywordMetricsById($client->customer_id, $keyword->keyword_id);
|
||||||
|
if (! empty($metrics)) {
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
GoogleKeywordMetric::updateOrCreate(
|
||||||
|
[
|
||||||
|
'google_keyword_id' => $keyword->id,
|
||||||
|
'date' => $metric['date'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'impressions' => $metric['impressions'],
|
||||||
|
'clicks' => $metric['clicks'],
|
||||||
|
'actual_spend' => $metric['actual_spend'],
|
||||||
|
'conversions' => $metric['conversions'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->info("Client ID: {$client->customer_id}, Keyword: {$keyword->text}, Date: {$metric['date']} , Impressions: {$metric['impressions']}, Clicks: {$metric['clicks']}, Cost: {$metric['actual_spend']}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info("No metrics found for Client ID: {$client->customer_id}, Keyword: {$keyword->text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error getting campaign details: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Console/Commands/MigrateClientCustomer.php
Normal file
42
app/Console/Commands/MigrateClientCustomer.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Customers;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class MigrateClientCustomer extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'customer:migrate-client-customers';
|
||||||
|
protected $description = 'Migrate client customer data';
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$clients = Client::where('sql_acc_code', '!=', null)->get();
|
||||||
|
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$customers = explode(',',$client->sql_acc_code);
|
||||||
|
foreach($customers as $customer){
|
||||||
|
$client->customers()->create([
|
||||||
|
// 'company_name' => $customer,
|
||||||
|
'sql_acc_code' => $customer,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error project linkage : '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
app/Console/Commands/PendingProjectActivitiesNotify.php
Normal file
156
app/Console/Commands/PendingProjectActivitiesNotify.php
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\ClientProjectActivities;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
|
class PendingProjectActivitiesNotify extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'project-activities:pending-notify {--test-email= : Send a sample reminder email to this address only}';
|
||||||
|
|
||||||
|
protected $description = 'Notify users about pending project activities based on estimated completion date';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($this->option('test-email')) {
|
||||||
|
return $this->sendTestEmail($this->option('test-email'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$today = Carbon::today();
|
||||||
|
$tomorrow = Carbon::tomorrow();
|
||||||
|
|
||||||
|
$activities = ClientProjectActivities::query()
|
||||||
|
->with(['client:id,name,customer_id', 'user:id,name,email'])
|
||||||
|
->whereNull('completed_at')
|
||||||
|
->whereNotNull('estimated_completed_at')
|
||||||
|
->whereDate('estimated_completed_at', '<=', $tomorrow)
|
||||||
|
->orderBy('estimated_completed_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$activities
|
||||||
|
->groupBy('user_id')
|
||||||
|
->each(function ($userActivities, $userId) use ($today, $tomorrow) {
|
||||||
|
$user = $userActivities->first()->user;
|
||||||
|
|
||||||
|
if (! $user || empty($user->email)) {
|
||||||
|
$this->warn("Skipping project activity reminder for user_id={$userId}; no valid email found.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$overdue = $userActivities->filter(function (ClientProjectActivities $activity) use ($today) {
|
||||||
|
return $activity->estimated_completed_at->lt($today);
|
||||||
|
});
|
||||||
|
|
||||||
|
$dueToday = $userActivities->filter(function (ClientProjectActivities $activity) use ($today) {
|
||||||
|
return $activity->estimated_completed_at->isSameDay($today);
|
||||||
|
});
|
||||||
|
|
||||||
|
$dueTomorrow = $userActivities->filter(function (ClientProjectActivities $activity) use ($tomorrow) {
|
||||||
|
return $activity->estimated_completed_at->isSameDay($tomorrow);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($overdue->isEmpty() && $dueToday->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailData = [
|
||||||
|
'user_name' => $user->name,
|
||||||
|
'role_label' => 'Activity Owner',
|
||||||
|
'overdue' => $overdue->map(fn (ClientProjectActivities $activity) => $this->formatActivity($activity, $today))->values()->all(),
|
||||||
|
'due_today' => $dueToday->map(fn (ClientProjectActivities $activity) => $this->formatActivity($activity, $today))->values()->all(),
|
||||||
|
'due_tomorrow' => $dueTomorrow->map(fn (ClientProjectActivities $activity) => $this->formatActivity($activity, $today))->values()->all(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Mail::send('mail.project_activity_reminder', ['data' => $mailData], function ($message) use ($user) {
|
||||||
|
$message->to($user->email, $user->name)
|
||||||
|
->subject('Pending Project Activity Reminder');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->info("Sent pending project activity reminder to {$user->email} ({$user->name}).");
|
||||||
|
});
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Error sending pending project activity reminders: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->error('Failed to send pending project activity reminders.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatActivity(ClientProjectActivities $activity, Carbon $today): array
|
||||||
|
{
|
||||||
|
$estimatedDate = $activity->estimated_completed_at->copy()->startOfDay();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'activity_no' => $activity->activity_no,
|
||||||
|
'customer_id' => $activity->client?->customer_id ?? '-',
|
||||||
|
'customer' => $activity->client?->name ?? '-',
|
||||||
|
'activity_type' => $activity->activity_type ?? '-',
|
||||||
|
'task_description' => $activity->task_description ?? '-',
|
||||||
|
'estimated_completed_at' => $estimatedDate->format('Y-m-d'),
|
||||||
|
'number_of_days' => (int) $today->diffInDays($estimatedDate, false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendTestEmail(string $email): int
|
||||||
|
{
|
||||||
|
$today = Carbon::today();
|
||||||
|
|
||||||
|
$mailData = [
|
||||||
|
'user_name' => 'Brian',
|
||||||
|
'role_label' => 'Activity Owner',
|
||||||
|
'overdue' => [
|
||||||
|
[
|
||||||
|
'activity_no' => 'ACT0001',
|
||||||
|
'customer_id' => '1234567890',
|
||||||
|
'customer' => 'Example Client A',
|
||||||
|
'activity_type' => 'scheduled',
|
||||||
|
'task_description' => 'Review SEM performance report and update client notes.',
|
||||||
|
'estimated_completed_at' => $today->copy()->subDays(2)->format('Y-m-d'),
|
||||||
|
'number_of_days' => -2,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'due_today' => [
|
||||||
|
[
|
||||||
|
'activity_no' => 'ACT0002',
|
||||||
|
'customer_id' => '1234567891',
|
||||||
|
'customer' => 'Example Client B',
|
||||||
|
'activity_type' => 'scheduled',
|
||||||
|
'task_description' => 'Prepare optimisation checklist for campaign review.',
|
||||||
|
'estimated_completed_at' => $today->format('Y-m-d'),
|
||||||
|
'number_of_days' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'due_tomorrow' => [
|
||||||
|
[
|
||||||
|
'activity_no' => 'ACT0003',
|
||||||
|
'customer_id' => '1234567892',
|
||||||
|
'customer' => 'Example Client C',
|
||||||
|
'activity_type' => 'scheduled',
|
||||||
|
'task_description' => 'Follow up on budget pacing and next-month planning.',
|
||||||
|
'estimated_completed_at' => $today->copy()->addDay()->format('Y-m-d'),
|
||||||
|
'number_of_days' => 1,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
Mail::send('mail.project_activity_reminder', ['data' => $mailData], function ($message) use ($email) {
|
||||||
|
$message->to($email)
|
||||||
|
->subject('Test: Pending Project Activity Reminder');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->info("Sent test pending project activity reminder to {$email}.");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
app/Http/Controllers/ActivityController.php
Normal file
119
app/Http/Controllers/ActivityController.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientProjectActivities;
|
||||||
|
use App\Models\ClientUserAssignation;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\UserHierarchyService;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Log as FacadesLog;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ActivityController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private UserHierarchyService $hierarchyService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function completeActivity(Request $request, int $activity_id)
|
||||||
|
{
|
||||||
|
$activity = ClientProjectActivities::findOrFail($activity_id);
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $activity->client), 403);
|
||||||
|
|
||||||
|
$activity->completed_at = Carbon::now();
|
||||||
|
$activity->save();
|
||||||
|
return redirect()->back()
|
||||||
|
->with('message-info', 'Activity has been completed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeActivity(Request $request, int $project_id): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'task_description' => ['required', 'string'],
|
||||||
|
'category' => ['required_if:activity_type,report,scheduled'],
|
||||||
|
'activity_type' => ['required', 'in:report,scheduled,customer_notes'],
|
||||||
|
'estimated_completed_at' => [
|
||||||
|
'nullable',
|
||||||
|
'required_if:activity_type,scheduled',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$project = Client::findOrFail($project_id);
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $project), 403);
|
||||||
|
|
||||||
|
$activity = $project->activitiesList()->create([
|
||||||
|
'task_description' => $request->task_description,
|
||||||
|
'activity_type' => $request->category,
|
||||||
|
'estimated_completed_at' => $request->activity_type == 'report' ? null : $request->estimated_completed_at,
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'completed_at' => $request->activity_type == 'report' ? now() : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('google-ads.accounts.show', ['id' => $project->customer_id, 'tab' => 'activities'])
|
||||||
|
->with('message-info', 'Activity has been created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateActivity($id, Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$activity = ClientProjectActivities::findOrFail($id);
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $activity->client), 403);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'task_description' => ['required', 'string', 'min:0'],
|
||||||
|
'category' => ['required_if:activity_type,report,scheduled'],
|
||||||
|
'activity_type' => ['required', 'in:report,scheduled,customer_notes'],
|
||||||
|
'estimated_completed_at' => [
|
||||||
|
'nullable',
|
||||||
|
'required_if:activity_type,scheduled',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$activity->update([
|
||||||
|
'task_description' => $request->task_description,
|
||||||
|
'activity_type' => $request->category,
|
||||||
|
'estimated_completed_at' => $request->activity_type == 'report' ? null : $request->estimated_completed_at,
|
||||||
|
'completed_at' => $request->activity_type == 'report' ? (! empty($activity->completed_at) ? $activity->completed_at : now()) : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('message-info', 'Activity has been updated successfully.');
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
FacadesLog::error('Error updated project activity: '.$e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->withInput()
|
||||||
|
->with('message-error', 'Something went wrong while updating the project activity.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteActivity($id): RedirectResponse
|
||||||
|
{
|
||||||
|
$activity = ClientProjectActivities::findOrFail($id);
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $activity->client), 403);
|
||||||
|
|
||||||
|
$clientId = $activity->client_id;
|
||||||
|
$activity->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('google-ads.accounts.show', ['id' => $clientId, 'tab' => 'activities'])
|
||||||
|
->with('message-info', 'Activity has been deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
304
app/Http/Controllers/Api/ClientInvoiceController.php
Normal file
304
app/Http/Controllers/Api/ClientInvoiceController.php
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ClientInvoice;
|
||||||
|
use App\Services\ClientLookupService;
|
||||||
|
use App\Services\ClientInvoiceApprovalService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class ClientInvoiceController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ClientInvoiceApprovalService $approvalService,
|
||||||
|
private ClientLookupService $clientLookupService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pending(): JsonResponse
|
||||||
|
{
|
||||||
|
$invoices = ClientInvoice::query()
|
||||||
|
->with('client:id,name,customer_id')
|
||||||
|
->whereNull('approved_at')
|
||||||
|
->latest('id')
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'client_id',
|
||||||
|
'pending_sql_acc_code',
|
||||||
|
'pending_client_name',
|
||||||
|
'invoice_no',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'payment_no',
|
||||||
|
'management_fee',
|
||||||
|
'media_fee',
|
||||||
|
'nett_amount',
|
||||||
|
'total_spending',
|
||||||
|
'created_at',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'count' => $invoices->count(),
|
||||||
|
'invoices' => $invoices->map(function (ClientInvoice $invoice) {
|
||||||
|
$previousPayments = $this->previousPaymentsForInvoice($invoice);
|
||||||
|
$invoiceBillingTotals = $this->invoiceBillingTotalsFromPayments($previousPayments);
|
||||||
|
|
||||||
|
return [
|
||||||
|
...$invoice->toArray(),
|
||||||
|
'requires_client' => $invoice->client_id === null,
|
||||||
|
'previous_payments' => $previousPayments,
|
||||||
|
'invoice_billing_totals' => $invoiceBillingTotals,
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'client_id' => ['nullable', 'exists:clients,id'],
|
||||||
|
'sql_acc_code' => ['required_without:client_id', 'nullable', 'string'],
|
||||||
|
'client_name' => ['nullable', 'string'],
|
||||||
|
'invoice_no' => ['required', 'string'],
|
||||||
|
'linked_invoice_id' => ['nullable', 'integer'],
|
||||||
|
'is_credit_card' => ['nullable', 'boolean'],
|
||||||
|
'is_paid' => ['nullable', 'boolean'],
|
||||||
|
'payment_no' => ['nullable', 'string'],
|
||||||
|
'start_date' => ['nullable', 'date'],
|
||||||
|
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
|
||||||
|
'management_fee' => ['required', 'numeric', 'min:0'],
|
||||||
|
'media_fee' => ['required', 'numeric', 'min:0'],
|
||||||
|
'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||||
|
'total_spending' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mediaFee = $validated['media_fee'];
|
||||||
|
$taxPercent = $validated['tax_percent'] ?? 0;
|
||||||
|
$nettAmount = $mediaFee - ($mediaFee * ($taxPercent / 100));
|
||||||
|
$sqlAccCode = $this->clientLookupService->normalizeSqlAccCode($validated['sql_acc_code'] ?? null);
|
||||||
|
$client = ! empty($validated['client_id'])
|
||||||
|
? \App\Models\Client::find($validated['client_id'])
|
||||||
|
: $this->clientLookupService->findBySqlAccCode($sqlAccCode);
|
||||||
|
|
||||||
|
if (! empty($validated['linked_invoice_id'])) {
|
||||||
|
$linkedInvoiceExists = $client !== null && ClientInvoice::query()
|
||||||
|
->where('id', $validated['linked_invoice_id'])
|
||||||
|
->where('client_id', $client->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $linkedInvoiceExists) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'linked_invoice_id' => 'The linked invoice must belong to the resolved client.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice = ClientInvoice::create([
|
||||||
|
'client_id' => $client?->id,
|
||||||
|
'pending_sql_acc_code' => $client === null ? $sqlAccCode : null,
|
||||||
|
'pending_client_name' => $client === null ? ($validated['client_name'] ?? null) : null,
|
||||||
|
'invoice_no' => $validated['invoice_no'],
|
||||||
|
'linked_invoice_id' => $validated['linked_invoice_id'] ?? null,
|
||||||
|
'is_credit_card' => (bool) ($validated['is_credit_card'] ?? false),
|
||||||
|
'is_paid' => (bool) ($validated['is_paid'] ?? false),
|
||||||
|
'approved_at' => null,
|
||||||
|
'payment_no' => $validated['payment_no'] ?? null,
|
||||||
|
'start_date' => $validated['start_date'] ?? null,
|
||||||
|
'end_date' => $validated['end_date'] ?? null,
|
||||||
|
'management_fee' => $validated['management_fee'],
|
||||||
|
'media_fee' => $validated['media_fee'],
|
||||||
|
'tax_percent' => $taxPercent,
|
||||||
|
'nett_amount' => $nettAmount,
|
||||||
|
'total_spending' => $validated['total_spending'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->approvalService->requireApproval($invoice);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Invoice created and marked for approval.',
|
||||||
|
'invoice' => $invoice->fresh(),
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(ClientInvoice $invoice): JsonResponse
|
||||||
|
{
|
||||||
|
if ($invoice->client_id === null) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Create and link the client before approving this invoice.',
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice = $this->approvalService->approve($invoice);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Invoice approved successfully.',
|
||||||
|
'invoice' => $invoice,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function previousPaymentsForInvoice(ClientInvoice $invoice): array
|
||||||
|
{
|
||||||
|
if (empty($invoice->invoice_no)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::acceptJson()
|
||||||
|
->withHeaders([
|
||||||
|
'X-Secret' => config('app.billing_key'),
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
])
|
||||||
|
->get(config('app.billing_url').'/customer/invoices/getInvoicePaymentDetailsByInvoiceGoogle', [
|
||||||
|
'invoice_number' => $invoice->invoice_no,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
Log::warning('Unable to fetch invoice payment details.', [
|
||||||
|
'invoice_no' => $invoice->invoice_no,
|
||||||
|
'status' => $response->status(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$records = $this->normalizePaymentRecords($response->json('data'));
|
||||||
|
|
||||||
|
return collect($records)
|
||||||
|
->filter(fn (array $payment) => ($payment['payment_number'] ?? null) !== $invoice->payment_no)
|
||||||
|
->map(fn (array $payment) => $this->formatPreviousPayment($payment))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('Unable to fetch invoice payment details.', [
|
||||||
|
'invoice_no' => $invoice->invoice_no,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePaymentRecords(mixed $data): array
|
||||||
|
{
|
||||||
|
if (! is_array($data)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_is_list($data)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$data];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatPreviousPayment(array $payment): array
|
||||||
|
{
|
||||||
|
$items = collect($payment['items'] ?? [])
|
||||||
|
->filter(fn (mixed $paymentItem) => is_array($paymentItem))
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$paymentAmount = (float) ($payment['amount'] ?? 0);
|
||||||
|
$estimatedItemsTotal = $items->sum(
|
||||||
|
fn (array $paymentItem) => $this->paymentItemEstimatedTotal($paymentItem)
|
||||||
|
);
|
||||||
|
$itemAmounts = $items
|
||||||
|
->map(fn (array $paymentItem) => (float) ($paymentItem['amount'] ?? 0))
|
||||||
|
->filter(fn (float $amount) => $amount > 0)
|
||||||
|
->unique()
|
||||||
|
->values();
|
||||||
|
$usesRepeatedPaymentAmount = $items->count() > 1
|
||||||
|
&& $paymentAmount > 0
|
||||||
|
&& $estimatedItemsTotal > 0
|
||||||
|
&& $itemAmounts->count() === 1
|
||||||
|
&& abs($itemAmounts->first() - $paymentAmount) < 0.01;
|
||||||
|
|
||||||
|
$totals = $items->reduce(function (array $totals, array $paymentItem) use ($usesRepeatedPaymentAmount, $paymentAmount, $estimatedItemsTotal) {
|
||||||
|
$sqlAccCode = $this->paymentItemSqlAccCode($paymentItem);
|
||||||
|
$estimatedTotal = $this->paymentItemEstimatedTotal($paymentItem);
|
||||||
|
$exact_tax = $this->paymentItemTax($paymentItem);
|
||||||
|
$taxPercent = $this->paymentTaxPercent($paymentItem)/100 + 1;
|
||||||
|
$amount = $usesRepeatedPaymentAmount
|
||||||
|
? $paymentAmount * ($estimatedTotal / $estimatedItemsTotal)
|
||||||
|
: (float) ($paymentItem['amount'] ?? 0);
|
||||||
|
|
||||||
|
if ($sqlAccCode === 'G03') {
|
||||||
|
$totals['media_fee'] += $amount;
|
||||||
|
$totals['invoice_media_fee'] += $estimatedTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sqlAccCode === 'GOOGLE') {
|
||||||
|
$totals['management_fee'] += $amount;
|
||||||
|
$totals['invoice_management_fee'] += $estimatedTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $totals;
|
||||||
|
}, [
|
||||||
|
'media_fee' => 0.0,
|
||||||
|
'management_fee' => 0.0,
|
||||||
|
'invoice_media_fee' => 0.0,
|
||||||
|
'invoice_management_fee' => 0.0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'payment_number' => $payment['payment_number'] ?? null,
|
||||||
|
'pending_client_name' => $payment['company_name'] ?? null,
|
||||||
|
'status' => $payment['status'] ?? null,
|
||||||
|
'sql_created_at' => $payment['sql_created_at'] ?? null,
|
||||||
|
'amount' => $payment['amount'] ?? null,
|
||||||
|
'media_fee' => $totals['media_fee']/1.08,
|
||||||
|
'management_fee' => $totals['management_fee']/1.08,
|
||||||
|
'invoice_media_fee' => $totals['invoice_media_fee']/1.08,
|
||||||
|
'invoice_management_fee' => $totals['invoice_management_fee']/1.08,
|
||||||
|
'invoice_number' => data_get($payment, 'invoice.invoice_number'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function paymentItemSqlAccCode(array $paymentItem): ?string
|
||||||
|
{
|
||||||
|
$sqlAccCode = data_get($paymentItem, 'item.item.sql_acc_code')
|
||||||
|
?? data_get($paymentItem, 'item.sql_acc_code')
|
||||||
|
?? data_get($paymentItem, 'sql_acc_code');
|
||||||
|
|
||||||
|
return is_string($sqlAccCode) ? strtoupper(trim($sqlAccCode)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function paymentItemEstimatedTotal(array $paymentItem): float
|
||||||
|
{
|
||||||
|
return (float) (
|
||||||
|
data_get($paymentItem, 'item.estimated_total')
|
||||||
|
?? data_get($paymentItem, 'estimated_total')
|
||||||
|
?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function paymentItemTax(array $paymentItem): float
|
||||||
|
{
|
||||||
|
return (float) (
|
||||||
|
data_get($paymentItem, 'exact_tax')
|
||||||
|
?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function paymentTaxPercent(array $paymentItem): float
|
||||||
|
{
|
||||||
|
return (float) (
|
||||||
|
data_get($paymentItem, 'item.sql_acc_tax_percent')
|
||||||
|
?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function invoiceBillingTotalsFromPayments(array $payments): array
|
||||||
|
{
|
||||||
|
$payment = $payments[0] ?? null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'media_fee' => $payment['invoice_media_fee'] ?? 0,
|
||||||
|
'management_fee' => $payment['invoice_management_fee'] ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Http/Controllers/Api/NotificationController.php
Normal file
31
app/Http/Controllers/Api/NotificationController.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ClientInvoice;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class NotificationController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$pendingInvoiceCount = ClientInvoice::query()
|
||||||
|
->whereNull('approved_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$notifications = collect([
|
||||||
|
[
|
||||||
|
'type' => 'pending_invoice',
|
||||||
|
'title' => 'Pending invoice approvals',
|
||||||
|
'description' => 'Client invoices waiting for approval',
|
||||||
|
'count' => $pendingInvoiceCount,
|
||||||
|
],
|
||||||
|
])->filter(fn (array $notification) => $notification['count'] > 0)->values();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'count' => $notifications->sum('count'),
|
||||||
|
'notifications' => $notifications,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
app/Http/Controllers/CampaignController.php
Normal file
91
app/Http/Controllers/CampaignController.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Campaign;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Industry;
|
||||||
|
use App\Models\CampaignManager;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class CampaignController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$campaigns = Campaign::with(['client','consultant','campaignManager'])
|
||||||
|
->latest()->get();
|
||||||
|
|
||||||
|
return Inertia::render('campaigns/index', [
|
||||||
|
'campaigns' => $campaigns,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return Inertia::render('campaigns/create', [
|
||||||
|
'clients' => Client::all(),
|
||||||
|
'consultants' => User::all(),
|
||||||
|
'campaignManagers' => User::all()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
// $request->validate([
|
||||||
|
// 'client_id' => 'required|exists:clients,id',
|
||||||
|
// 'consultant_id' => 'required|exists:users,id',
|
||||||
|
// 'campaign_manager_id' => 'required|exists:campaign_managers,id',
|
||||||
|
// 'industry_id' => 'required|exists:industries,id',
|
||||||
|
// 'campaign_name' => 'required|string|max:255',
|
||||||
|
// 'landing_page' => 'nullable|url|max:255',
|
||||||
|
// 'status' => 'required|in:active,paused,ended,draft',
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
Campaign::create($request->all());
|
||||||
|
|
||||||
|
return redirect()->route('campaigns.index')->with('success', 'Campaign created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Campaign $campaign)
|
||||||
|
{
|
||||||
|
return Inertia::render('campaigns/edit', [
|
||||||
|
'campaign' => $campaign->load(['client', 'consultant', 'campaignManager', 'industry']),
|
||||||
|
'clients' => Client::all(),
|
||||||
|
'consultants' => User::all(),
|
||||||
|
'campaignManagers' => User::all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Campaign $campaign)
|
||||||
|
{
|
||||||
|
// $request->validate([
|
||||||
|
// 'client_id' => 'required|exists:clients,id',
|
||||||
|
// 'consultant_id' => 'required|exists:users,id',
|
||||||
|
// 'campaign_manager_id' => 'required|exists:campaign_managers,id',
|
||||||
|
// 'industry_id' => 'required|exists:industries,id',
|
||||||
|
// 'campaign_name' => 'required|string|max:255',
|
||||||
|
// 'landing_page' => 'nullable|url|max:255',
|
||||||
|
// 'status' => 'required|in:active,paused,ended,draft',
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
$campaign->update($request->all());
|
||||||
|
|
||||||
|
return redirect()->route('campaigns.index')->with('success', 'Campaign updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Campaign $campaign)
|
||||||
|
{
|
||||||
|
$campaign->delete();
|
||||||
|
|
||||||
|
return redirect()->route('campaigns.index')->with('success', 'Campaign deleted successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Campaign $campaign)
|
||||||
|
{
|
||||||
|
return Inertia::render('campaigns/show', [
|
||||||
|
'campaign' => $campaign->load(['client', 'consultant', 'campaignManager', 'industry', 'finances', 'reports', 'remarks']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Http/Controllers/ClientInvoiceAdjustmentController.php
Normal file
53
app/Http/Controllers/ClientInvoiceAdjustmentController.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ClientInvoiceAdjustment;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Services\UserHierarchyService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ClientInvoiceAdjustmentController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private UserHierarchyService $hierarchyService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, Client $client)
|
||||||
|
{
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'entry_type' => ['required', 'string', Rule::in([
|
||||||
|
ClientInvoiceAdjustment::TYPE_DEBIT,
|
||||||
|
ClientInvoiceAdjustment::TYPE_CREDIT,
|
||||||
|
])],
|
||||||
|
'amount' => ['required', 'numeric', 'min:0'],
|
||||||
|
'remark' => ['nullable', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
ClientInvoiceAdjustment::create([
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'entry_type' => $validated['entry_type'],
|
||||||
|
'amount' => $validated['amount'],
|
||||||
|
'remark' => $validated['remark'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('message-info', 'Adjustment added successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(ClientInvoiceAdjustment $adjustment)
|
||||||
|
{
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $adjustment->client), 403);
|
||||||
|
|
||||||
|
$adjustment->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('message-info', 'Adjustment deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
369
app/Http/Controllers/ClientInvoiceController.php
Normal file
369
app/Http/Controllers/ClientInvoiceController.php
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientCustomer;
|
||||||
|
use App\Models\ClientInvoice;
|
||||||
|
use App\Models\ClientUserAssignation;
|
||||||
|
use App\Services\ClientInvoiceApprovalService;
|
||||||
|
use App\Services\ClientLookupService;
|
||||||
|
use App\Services\UserHierarchyService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class ClientInvoiceController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ClientInvoiceApprovalService $approvalService,
|
||||||
|
private UserHierarchyService $hierarchyService,
|
||||||
|
private ClientLookupService $clientLookupService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request): Response
|
||||||
|
{
|
||||||
|
$clientId = $request->query('client');
|
||||||
|
$customerId = $request->query('customer_id');
|
||||||
|
|
||||||
|
if (! $clientId || ! $customerId) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = Client::findOrFail($clientId);
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
|
||||||
|
|
||||||
|
$availableInvoices = ClientInvoice::query()
|
||||||
|
->where('client_id', $clientId)
|
||||||
|
->orderBy('invoice_no')
|
||||||
|
->get(['id', 'invoice_no', 'linked_invoice_id']);
|
||||||
|
|
||||||
|
return Inertia::render('client-invoices/create', [
|
||||||
|
'clientId' => $clientId,
|
||||||
|
'customerId' => $customerId,
|
||||||
|
'availableInvoices' => $availableInvoices,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(ClientInvoice $invoice): Response
|
||||||
|
{
|
||||||
|
abort_if($invoice->client === null, 409, 'Create the client before editing this invoice.');
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
||||||
|
|
||||||
|
$availableInvoices = ClientInvoice::query()
|
||||||
|
->where('client_id', $invoice->client_id)
|
||||||
|
->where('id', '!=', $invoice->id)
|
||||||
|
->orderBy('invoice_no')
|
||||||
|
->get(['id', 'invoice_no', 'linked_invoice_id']);
|
||||||
|
|
||||||
|
return Inertia::render('client-invoices/edit', [
|
||||||
|
'invoice' => $invoice->load('client'),
|
||||||
|
'availableInvoices' => $availableInvoices,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'client_id' => ['required', 'exists:clients,id'],
|
||||||
|
'customer_id' => ['required', 'string'],
|
||||||
|
'invoice_no' => ['required', 'string'],
|
||||||
|
'linked_invoice_id' => [
|
||||||
|
'nullable',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('client_invoices', 'id')->where(function ($query) use ($request) {
|
||||||
|
return $query->where('client_id', $request->integer('client_id'));
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
'is_credit_card' => ['nullable', 'boolean'],
|
||||||
|
'is_paid' => ['nullable', 'boolean'],
|
||||||
|
'payment_no' => ['nullable', 'string'],
|
||||||
|
'start_date' => ['nullable', 'date'],
|
||||||
|
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
|
||||||
|
'management_fee' => ['required', 'numeric', 'min:0'],
|
||||||
|
'media_fee' => ['required', 'numeric', 'min:0'],
|
||||||
|
'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||||
|
'total_spending' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Client::findOrFail($validated['client_id']);
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
|
||||||
|
|
||||||
|
$mediaFee = $validated['media_fee'];
|
||||||
|
$taxPercent = $validated['tax_percent'] ?? 0;
|
||||||
|
$nettAmount = $mediaFee - ($mediaFee * ($taxPercent / 100));
|
||||||
|
|
||||||
|
$invoice = ClientInvoice::create([
|
||||||
|
'client_id' => $validated['client_id'],
|
||||||
|
'invoice_no' => $validated['invoice_no'],
|
||||||
|
'linked_invoice_id' => $validated['linked_invoice_id'] ?? null,
|
||||||
|
'is_credit_card' => (bool) ($validated['is_credit_card'] ?? false),
|
||||||
|
'is_paid' => (bool) ($validated['is_paid'] ?? false),
|
||||||
|
'approved_at' => null,
|
||||||
|
'payment_no' => $validated['payment_no'] ?? null,
|
||||||
|
'start_date' => $validated['start_date'] ?? null,
|
||||||
|
'end_date' => $validated['end_date'] ?? null,
|
||||||
|
'management_fee' => $validated['management_fee'],
|
||||||
|
'media_fee' => $validated['media_fee'],
|
||||||
|
'tax_percent' => $taxPercent,
|
||||||
|
'nett_amount' => $nettAmount,
|
||||||
|
'total_spending' => $validated['total_spending'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->approvalService->approve($invoice);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('google-ads.accounts.show', ['id' => $validated['customer_id']])
|
||||||
|
->with('message-info', 'Invoice created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, ClientInvoice $invoice)
|
||||||
|
{
|
||||||
|
abort_if($invoice->client === null, 409, 'Create the client before updating this invoice.');
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'invoice_no' => ['required', 'string'],
|
||||||
|
'linked_invoice_id' => [
|
||||||
|
'nullable',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('client_invoices', 'id')->where(function ($query) use ($invoice) {
|
||||||
|
return $query
|
||||||
|
->where('client_id', $invoice->client_id)
|
||||||
|
->where('id', '!=', $invoice->id);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
'is_credit_card' => ['nullable', 'boolean'],
|
||||||
|
'is_paid' => ['nullable', 'boolean'],
|
||||||
|
'payment_no' => ['nullable', 'string'],
|
||||||
|
'start_date' => ['nullable', 'date'],
|
||||||
|
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
|
||||||
|
'amount' => ['required', 'numeric', 'min:0'],
|
||||||
|
'management_fee' => ['required', 'numeric', 'min:0'],
|
||||||
|
'media_fee' => ['required', 'numeric', 'min:0'],
|
||||||
|
'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||||
|
'total_spending' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$managementFee = $validated['management_fee'];
|
||||||
|
$mediaFee = $validated['media_fee'];
|
||||||
|
$taxPercent = $validated['tax_percent'] ?? 0;
|
||||||
|
$nettAmount = $mediaFee - ($mediaFee * ($taxPercent / 100));
|
||||||
|
|
||||||
|
$invoice->update([
|
||||||
|
'invoice_no' => $validated['invoice_no'],
|
||||||
|
'linked_invoice_id' => $validated['linked_invoice_id'] ?? null,
|
||||||
|
'is_credit_card' => (bool) ($validated['is_credit_card'] ?? false),
|
||||||
|
'is_paid' => (bool) ($validated['is_paid'] ?? false),
|
||||||
|
'payment_no' => $validated['payment_no'] ?? null,
|
||||||
|
'start_date' => $validated['start_date'] ?? null,
|
||||||
|
'end_date' => $validated['end_date'] ?? null,
|
||||||
|
'amount' => $validated['amount'],
|
||||||
|
'management_fee' => $managementFee,
|
||||||
|
'media_fee' => $validated['media_fee'],
|
||||||
|
'tax_percent' => $taxPercent,
|
||||||
|
'nett_amount' => $nettAmount,
|
||||||
|
'total_spending' => $validated['total_spending'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('google-ads.accounts.show', ['id' => $invoice->client->customer_id])
|
||||||
|
->with('message-info', 'Invoice updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(ClientInvoice $invoice)
|
||||||
|
{
|
||||||
|
abort_if($invoice->client === null, 409, 'Create the client before approving this invoice.');
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
||||||
|
|
||||||
|
$this->approvalService->approve($invoice);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('message-info', 'Invoice approved successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(ClientInvoice $invoice)
|
||||||
|
{
|
||||||
|
if ($invoice->client !== null) {
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('message-info', 'Invoice deleted successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPdfInvoice($id)
|
||||||
|
{
|
||||||
|
$invoice = ClientInvoice::where('id', $id)->first();
|
||||||
|
if (! empty($invoice) && ! empty($invoice->invoice_no)) {
|
||||||
|
if ($invoice->client !== null) {
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'audience' => 'SEM',
|
||||||
|
'invoice_numbers' => [$invoice->invoice_no],
|
||||||
|
];
|
||||||
|
$invoiceResponse = Http::acceptJson()
|
||||||
|
->withHeaders([
|
||||||
|
'X-Secret' => config('app.billing_key'),
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
])
|
||||||
|
->get(config('app.billing_url').'/customer/invoices/', $payload);
|
||||||
|
|
||||||
|
if ($invoiceResponse->successful()) {
|
||||||
|
$invoiceDetails = json_decode($invoiceResponse->body());
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'X-Secret' => config('app.billing_key'),
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
])->get($invoiceDetails->data[0]->pdf_url);
|
||||||
|
if ($response->successful()) {
|
||||||
|
return response()->stream(
|
||||||
|
function () use ($response) {
|
||||||
|
echo $response->body();
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
'Content-Type' => 'application/pdf',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response->throw();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$invoiceResponse->throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createClient(ClientInvoice $invoice): Response|\Illuminate\Http\RedirectResponse
|
||||||
|
{
|
||||||
|
$existingClient = $invoice->client
|
||||||
|
?? $this->clientLookupService->findBySqlAccCode($invoice->pending_sql_acc_code);
|
||||||
|
|
||||||
|
if ($existingClient !== null) {
|
||||||
|
$invoice->update([
|
||||||
|
'client_id' => $existingClient->id,
|
||||||
|
'pending_sql_acc_code' => null,
|
||||||
|
'pending_client_name' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('client-invoices.edit', $invoice)
|
||||||
|
->with('message-info', 'Invoice '.$invoice->invoice_no.' has been linked to '.$existingClient->name.'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$unlinkedClients = Client::query()
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->whereNull('sql_acc_code')
|
||||||
|
->orWhere('sql_acc_code', '');
|
||||||
|
})
|
||||||
|
->whereDoesntHave('customers', function ($query) {
|
||||||
|
$query->whereNotNull('sql_acc_code')
|
||||||
|
->where('sql_acc_code', '!=', '');
|
||||||
|
})
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'customer_id', 'status', 'time_zone'])
|
||||||
|
->map(fn (Client $client) => [
|
||||||
|
'value' => (string) $client->id,
|
||||||
|
'label' => trim($client->name.' ('.$client->customer_id.')'),
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return Inertia::render('client-invoices/create-client', [
|
||||||
|
'invoice' => $invoice->load('client'),
|
||||||
|
'existingClient' => null,
|
||||||
|
'unlinkedClients' => $unlinkedClients,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeClient(Request $request, ClientInvoice $invoice)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'client_id' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists('clients', 'id')->where(function ($query) {
|
||||||
|
$query
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->whereNull('sql_acc_code')
|
||||||
|
->orWhere('sql_acc_code', '');
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
'sql_acc_code' => ['required', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sqlAccCode = $this->clientLookupService->normalizeSqlAccCode($validated['sql_acc_code']);
|
||||||
|
$existingClient = $this->clientLookupService->findBySqlAccCode($sqlAccCode);
|
||||||
|
|
||||||
|
if ($existingClient !== null && $existingClient->id !== (int) $validated['client_id']) {
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['sql_acc_code' => 'This SQL account code is already linked to another client.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedClientIsLinked = Client::query()
|
||||||
|
->where('id', $validated['client_id'])
|
||||||
|
->whereHas('customers', function ($query) {
|
||||||
|
$query->whereNotNull('sql_acc_code')
|
||||||
|
->where('sql_acc_code', '!=', '');
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($selectedClientIsLinked) {
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['client_id' => 'Select a client that does not already have an SQL account code.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = DB::transaction(function () use ($validated, $sqlAccCode, $invoice) {
|
||||||
|
$client = Client::findOrFail($validated['client_id']);
|
||||||
|
|
||||||
|
$client->update([
|
||||||
|
'sql_acc_code' => $sqlAccCode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ClientCustomer::updateOrCreate(
|
||||||
|
[
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'sql_acc_code' => $sqlAccCode,
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
ClientUserAssignation::updateOrCreate(
|
||||||
|
[
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$invoice->update([
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'pending_sql_acc_code' => null,
|
||||||
|
'pending_client_name' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $client;
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('client-invoices.edit', $invoice)
|
||||||
|
->with('message-info', 'Client '.$client->name.' has been linked to invoice '.$invoice->invoice_no.'.');
|
||||||
|
}
|
||||||
|
}
|
||||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
115
app/Http/Controllers/DashboardController.php
Normal file
115
app/Http/Controllers/DashboardController.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientInvoice;
|
||||||
|
use App\Models\ClientProjectActivities;
|
||||||
|
use App\Services\UserHierarchyService;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private UserHierarchyService $hierarchyService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$visibleClients = $this->hierarchyService
|
||||||
|
->scopeClientsVisibleTo(
|
||||||
|
Client::query()->with(['invoices', 'invoiceAdjustments', 'customers']),
|
||||||
|
$user
|
||||||
|
)
|
||||||
|
->get();
|
||||||
|
$visibleClientIds = $visibleClients->pluck('id');
|
||||||
|
$visibleClientQuery = fn (): Builder => Client::query()->whereIn('id', $visibleClientIds);
|
||||||
|
$visibleInvoiceQuery = fn (): Builder => ClientInvoice::query()->whereIn('client_id', $visibleClientIds);
|
||||||
|
$unlinkedPendingInvoiceCount = $user->hasRole('Admin')
|
||||||
|
? ClientInvoice::query()->whereNull('approved_at')->whereNull('client_id')->count()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
$pendingInvoiceCount = $visibleInvoiceQuery()
|
||||||
|
->whereNull('approved_at')
|
||||||
|
->count() + $unlinkedPendingInvoiceCount;
|
||||||
|
|
||||||
|
$pendingActivities = ClientProjectActivities::query()
|
||||||
|
->whereIn('client_id', $visibleClientIds)
|
||||||
|
->whereNull('completed_at')
|
||||||
|
->whereNotNull('estimated_completed_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return Inertia::render('dashboard', [
|
||||||
|
'stats' => [
|
||||||
|
'totalClients' => $visibleClients->count(),
|
||||||
|
'pendingInvoices' => $pendingInvoiceCount,
|
||||||
|
'unlinkedPendingInvoices' => $unlinkedPendingInvoiceCount,
|
||||||
|
'pendingActivities' => $pendingActivities,
|
||||||
|
'clientsMissingSqlCode' => $visibleClientQuery()
|
||||||
|
->where(function (Builder $query) {
|
||||||
|
$query->whereNull('sql_acc_code')
|
||||||
|
->orWhere('sql_acc_code', '');
|
||||||
|
})
|
||||||
|
->whereDoesntHave('customers', function (Builder $query) {
|
||||||
|
$query->whereNotNull('sql_acc_code')
|
||||||
|
->where('sql_acc_code', '!=', '');
|
||||||
|
})
|
||||||
|
->count(),
|
||||||
|
],
|
||||||
|
'clientStatusCounts' => $visibleClients
|
||||||
|
->groupBy('status')
|
||||||
|
->map(fn ($clients) => $clients->count())
|
||||||
|
->sortKeys()
|
||||||
|
->toArray(),
|
||||||
|
'recentInvoices' => $visibleInvoiceQuery()
|
||||||
|
->with('client:id,name,customer_id')
|
||||||
|
->latest('id')
|
||||||
|
->limit(6)
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'client_id',
|
||||||
|
'invoice_no',
|
||||||
|
'approved_at',
|
||||||
|
'management_fee',
|
||||||
|
'media_fee',
|
||||||
|
'nett_amount',
|
||||||
|
'created_at',
|
||||||
|
])
|
||||||
|
->map(fn (ClientInvoice $invoice) => [
|
||||||
|
'id' => $invoice->id,
|
||||||
|
'invoice_no' => $invoice->invoice_no,
|
||||||
|
'approved_at' => $invoice->approved_at?->toDateTimeString(),
|
||||||
|
'management_fee' => $invoice->management_fee,
|
||||||
|
'media_fee' => $invoice->media_fee,
|
||||||
|
'nett_amount' => $invoice->nett_amount,
|
||||||
|
'created_at' => $invoice->created_at?->toDateString(),
|
||||||
|
'client' => $invoice->client,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all(),
|
||||||
|
'pendingActivities' => ClientProjectActivities::query()
|
||||||
|
->with(['client:id,name,customer_id', 'user:id,name'])
|
||||||
|
->whereIn('client_id', $visibleClientIds)
|
||||||
|
->whereNull('completed_at')
|
||||||
|
->whereNotNull('estimated_completed_at')
|
||||||
|
->orderBy('estimated_completed_at')
|
||||||
|
->limit(6)
|
||||||
|
->get()
|
||||||
|
->map(fn (ClientProjectActivities $activity) => [
|
||||||
|
'id' => $activity->id,
|
||||||
|
'activity_no' => $activity->activity_no,
|
||||||
|
'activity_type' => $activity->activity_type,
|
||||||
|
'task_description' => $activity->task_description,
|
||||||
|
'estimated_completed_at' => $activity->estimated_completed_at?->toDateString(),
|
||||||
|
'client' => $activity->client,
|
||||||
|
'assigned_person' => $activity->user?->name,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
490
app/Http/Controllers/GoogleAdsController.php
Normal file
490
app/Http/Controllers/GoogleAdsController.php
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientProjectActivities;
|
||||||
|
use App\Models\ClientUserAssignation;
|
||||||
|
use App\Models\ClientInvoiceAdjustment;
|
||||||
|
use App\Models\ClientCustomer;
|
||||||
|
use App\Models\GoogleCampaignMetric;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\ClientInvoice;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use App\Services\UserHierarchyService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Log as FacadesLog;
|
||||||
|
use Rap2hpoutre\FastExcel\FastExcel;
|
||||||
|
|
||||||
|
class GoogleAdsController extends Controller
|
||||||
|
{
|
||||||
|
protected $adsService;
|
||||||
|
private const GOOGLE_COMPANY_SYNC_LOCK = 'google-ads:get-company-details:running';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
GoogleAdsService $adsService,
|
||||||
|
private UserHierarchyService $hierarchyService,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$this->adsService = $adsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all accounts under MCC
|
||||||
|
public function accounts()
|
||||||
|
{
|
||||||
|
// $accounts = collect($this->adsService->listAccounts())
|
||||||
|
// ->map(function (array $account) {
|
||||||
|
// $account['customer_id'] = (string) ($account['customer_id'] ?? $account['id']);
|
||||||
|
// return $account;
|
||||||
|
// });
|
||||||
|
// $customerMap = $accounts->keyBy('customer_id');
|
||||||
|
$localClients = $this->hierarchyService
|
||||||
|
->scopeClientsVisibleTo(Client::query(), Auth::user())
|
||||||
|
->with('assignations.user', 'customers', 'invoices', 'invoiceAdjustments')
|
||||||
|
->get();
|
||||||
|
$customerMap = $localClients->map(function ($data) {
|
||||||
|
$assignedPerson = $data->assignations->firstWhere('role', ClientUserAssignation::ROLE_ASSIGNED_PERSON);
|
||||||
|
$salesPerson = $data->assignations->firstWhere('role', ClientUserAssignation::ROLE_SALES_PERSON);
|
||||||
|
$data['industry'] = $data->industry;
|
||||||
|
$data['sql_acc_code'] = implode(',', $data->customers->pluck('sql_acc_code')->toArray());
|
||||||
|
$data['assigned_person'] = $assignedPerson?->user?->name;
|
||||||
|
$data['sales_person'] = $salesPerson?->user?->name;
|
||||||
|
$data['latest_remaining_amount'] = $data->latestRemainingAmount(function (ClientInvoice $invoice) use ($data) {
|
||||||
|
if (empty($invoice->start_date) || empty($invoice->end_date)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GoogleCampaignMetric::query()
|
||||||
|
->join('google_campaigns', 'google_campaign_metrics.google_campaign_id', '=', 'google_campaigns.id')
|
||||||
|
->where('google_campaigns.client_id', $data->id)
|
||||||
|
->whereNull('google_campaigns.deleted_at')
|
||||||
|
->whereNull('google_campaign_metrics.deleted_at')
|
||||||
|
->whereBetween('google_campaign_metrics.date', [
|
||||||
|
$invoice->start_date?->toDateString(),
|
||||||
|
$invoice->end_date?->toDateString(),
|
||||||
|
])
|
||||||
|
->sum('google_campaign_metrics.actual_spend');
|
||||||
|
});
|
||||||
|
return $data;
|
||||||
|
});
|
||||||
|
return Inertia::render('campaigns/index', [
|
||||||
|
'clients' => $customerMap->values()->all(),
|
||||||
|
'googleCompanySyncRunning' => Cache::has(self::GOOGLE_COMPANY_SYNC_LOCK),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncGoogleCompanyDetails(): RedirectResponse
|
||||||
|
{
|
||||||
|
$started = Cache::add(self::GOOGLE_COMPANY_SYNC_LOCK, [
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'started_at' => now()->toDateTimeString(),
|
||||||
|
], now()->addHour());
|
||||||
|
|
||||||
|
if (! $started) {
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('message-warning', 'Google account sync is already running. Please wait for it to finish.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$exitCode = Artisan::call('google-ads:get-company-details');
|
||||||
|
|
||||||
|
if ($exitCode !== 0) {
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('message-error', 'Google account sync failed. Please check the logs.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('message-info', 'Google account records have been synced.');
|
||||||
|
} finally {
|
||||||
|
Cache::forget(self::GOOGLE_COMPANY_SYNC_LOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
// $account = $this->adsService->getAccountDetails($id);
|
||||||
|
$client = Client::where('customer_id', $id)->firstOrFail();
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
|
||||||
|
|
||||||
|
$account = [
|
||||||
|
'id' => $client->customer_id,
|
||||||
|
'name' => $client->name,
|
||||||
|
'status' => $client->status,
|
||||||
|
'time_zone' => $client->time_zone,
|
||||||
|
];
|
||||||
|
[$localClient, $assignments, $users, $invoices, $activityList, $pendingActivities, $completedActivities, $lifeTimeSpending, $clientAdjustments] =
|
||||||
|
$this->hydrateClient($account);
|
||||||
|
$account = array_merge($account, [
|
||||||
|
'industry' => $localClient->industry,
|
||||||
|
'sql_acc_code' => $localClient->sql_acc_code,
|
||||||
|
'activities_list' => $activityList,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Inertia::render('campaigns/show', [
|
||||||
|
'id' => $id,
|
||||||
|
'client' => $account,
|
||||||
|
'invoices' => $invoices,
|
||||||
|
'localClientId' => $localClient->id,
|
||||||
|
'clientAssignmentRoles' => ClientUserAssignation::roles(),
|
||||||
|
'clientAssignments' => $assignments,
|
||||||
|
'assignmentUsers' => $users,
|
||||||
|
'clientInvoices' => $invoices,
|
||||||
|
'clientAdjustments' => $clientAdjustments,
|
||||||
|
'pendingActivities' => $pendingActivities,
|
||||||
|
'completedActivities' => $completedActivities,
|
||||||
|
'lifeTimeSpending' => number_format($lifeTimeSpending, 2, '.', ','),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
$account = $this->adsService->getAccountDetails($id);
|
||||||
|
[$localClient, $assignments, $users, $invoices] = $this->hydrateClient($account);
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $localClient), 403);
|
||||||
|
|
||||||
|
$account = array_merge($account, [
|
||||||
|
'name' => $localClient->name,
|
||||||
|
'customer_id' => $localClient->customer_id,
|
||||||
|
'status' => $localClient->status,
|
||||||
|
'time_zone' => $localClient->time_zone,
|
||||||
|
'industry' => $localClient->industry,
|
||||||
|
'sql_acc_code' => $localClient->sql_acc_code,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Inertia::render('campaigns/account/edit', [
|
||||||
|
'id' => $id,
|
||||||
|
'client' => $account,
|
||||||
|
'clientAssignmentRoles' => ClientUserAssignation::roles(),
|
||||||
|
'clientAssignments' => $assignments,
|
||||||
|
'assignmentUsers' => $users,
|
||||||
|
'clientInvoices' => $invoices,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateAccount(Request $request, $id)
|
||||||
|
{
|
||||||
|
[$localClient] = $this->hydrateClient($this->adsService->getAccountDetails($id));
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $localClient), 403);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string'],
|
||||||
|
'customer_id' => ['required', 'string', 'unique:clients,customer_id,'.$localClient->id],
|
||||||
|
'industry' => ['nullable', 'string'],
|
||||||
|
'sql_acc_code' => ['nullable', 'string'],
|
||||||
|
'assigned_person' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
'sales_person' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$localClient->update([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'customer_id' => $validated['customer_id'],
|
||||||
|
'industry' => $validated['industry'] ?? null,
|
||||||
|
'sql_acc_code' => $validated['sql_acc_code'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$localClient->customers()->delete();
|
||||||
|
|
||||||
|
if (! empty($validated['sql_acc_code'])) {
|
||||||
|
collect(explode(',', $validated['sql_acc_code']))
|
||||||
|
->map(fn (string $sqlAccCode) => trim($sqlAccCode))
|
||||||
|
->filter()
|
||||||
|
->unique()
|
||||||
|
->each(function (string $sqlAccCode) use ($localClient) {
|
||||||
|
ClientCustomer::create([
|
||||||
|
'client_id' => $localClient->id,
|
||||||
|
'sql_acc_code' => $sqlAccCode,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignmentValues = [
|
||||||
|
ClientUserAssignation::ROLE_ASSIGNED_PERSON => $validated['assigned_person'] ?? null,
|
||||||
|
ClientUserAssignation::ROLE_SALES_PERSON => $validated['sales_person'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($assignmentValues as $role => $userId) {
|
||||||
|
if ($userId === null) {
|
||||||
|
$localClient->assignations()->where('role', $role)->delete();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$localClient->assignations()->updateOrCreate(
|
||||||
|
['role' => $role],
|
||||||
|
['user_id' => $userId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('google-ads.accounts.edit', ['id' => $localClient->customer_id])
|
||||||
|
->with('message-info', 'Account details updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function campaigns($id)
|
||||||
|
{
|
||||||
|
$campaigns = $this->adsService->listCampaigns($id);
|
||||||
|
return response()->json($campaigns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listCampaignsMetrics($id, $startDate, $endDate)
|
||||||
|
{
|
||||||
|
$campaigns = $this->adsService->listCampaignsMetrics($id, $startDate, $endDate);
|
||||||
|
return response()->json($campaigns);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrateClient(array $account): array
|
||||||
|
{
|
||||||
|
$localClient = Client::updateOrCreate(
|
||||||
|
['customer_id' => $account['id']],
|
||||||
|
[
|
||||||
|
'name' => $account['name'],
|
||||||
|
'status' => $account['status'],
|
||||||
|
'time_zone' => $account['time_zone'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$localClient->load(['assignations.user', 'invoices']);
|
||||||
|
|
||||||
|
$assignments = $localClient->assignations
|
||||||
|
->mapWithKeys(function (ClientUserAssignation $assignation) {
|
||||||
|
return [$assignation->role => $assignation->user_id];
|
||||||
|
})
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$users = User::orderBy('name')
|
||||||
|
->get(['id', 'name', 'email'])
|
||||||
|
->map(function (User $user) {
|
||||||
|
return [
|
||||||
|
'value' => (string) $user->id,
|
||||||
|
'label' => trim($user->name.' ('.$user->email.')'),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$clientAdjustments = ClientInvoiceAdjustment::query()
|
||||||
|
->where('client_id', $localClient->id)
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->get()
|
||||||
|
->map(function (ClientInvoiceAdjustment $adjustment) {
|
||||||
|
return [
|
||||||
|
'id' => $adjustment->id,
|
||||||
|
'client_id' => $adjustment->client_id,
|
||||||
|
'entry_type' => $adjustment->entry_type,
|
||||||
|
'amount' => $adjustment->amount,
|
||||||
|
'remark' => $adjustment->remark,
|
||||||
|
'created_at' => $adjustment->created_at?->toDateTimeString(),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$invoices = $localClient->invoices
|
||||||
|
->map(function ($invoice) use ($account) {
|
||||||
|
$campaigns = $this->adsService->listCampaigns($account['id']);
|
||||||
|
$totalInvoiceSpend = 0;
|
||||||
|
FacadesLog::info('Hydrated client data', [
|
||||||
|
'campaigns' => $campaigns,
|
||||||
|
]);
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
FacadesLog::info('Hydrated client data', [
|
||||||
|
'campaigns' => $campaign['id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($invoice->start_date) || empty($invoice->end_date)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
$metrics = $this->adsService->listCampaignsMetricsById(
|
||||||
|
$account['id'],
|
||||||
|
$campaign['id'],
|
||||||
|
$invoice->start_date?->toDateString() ?? null,
|
||||||
|
$invoice->end_date?->toDateString() ?? null
|
||||||
|
);
|
||||||
|
FacadesLog::info('Hydrated client data', [
|
||||||
|
'metrics' => $metrics,
|
||||||
|
]);
|
||||||
|
$totalSpend = array_sum(array_column($metrics, 'actual_spend'));
|
||||||
|
$totalInvoiceSpend += $totalSpend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $invoice->id,
|
||||||
|
'client_id' => $invoice->client_id,
|
||||||
|
'invoice_no' => $invoice->invoice_no,
|
||||||
|
'linked_invoice_id' => $invoice->linked_invoice_id,
|
||||||
|
'is_credit_card' => $invoice->is_credit_card,
|
||||||
|
'is_paid' => $invoice->is_paid,
|
||||||
|
'start_date' => $invoice->start_date?->toDateString(),
|
||||||
|
'end_date' => $invoice->end_date?->toDateString(),
|
||||||
|
'payment_no' => $invoice->payment_no,
|
||||||
|
'amount' => $invoice->amount,
|
||||||
|
'total_spend' => number_format($totalInvoiceSpend, 2, '.', ''),
|
||||||
|
'management_fee' => $invoice->management_fee,
|
||||||
|
'media_fee' => $invoice->media_fee,
|
||||||
|
'tax_percent' => $invoice->tax_percent,
|
||||||
|
'nett_amount' => $invoice->nett_amount,
|
||||||
|
'total_spending' => $invoice->total_spending,
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
// dd($invoices);
|
||||||
|
|
||||||
|
$campaigns = $this->adsService->listCampaigns($localClient->customer_id);
|
||||||
|
|
||||||
|
$lifeTimeSpend = 0;
|
||||||
|
|
||||||
|
if (! empty($campaigns)) {
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$metrics = $this->adsService->listCampaignsMetricsById(
|
||||||
|
$localClient->customer_id,
|
||||||
|
$campaign['id'],
|
||||||
|
'1970-01-01', // Start date far in the past to capture all historical data
|
||||||
|
now()->toDateString() // End date as today`
|
||||||
|
);
|
||||||
|
$totalSpend = array_sum(array_column($metrics, 'actual_spend'));
|
||||||
|
$lifeTimeSpend += number_format($totalSpend, 2, '.', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$activities = $localClient->activitiesList()
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->get()
|
||||||
|
->map(function (ClientProjectActivities $activity) {
|
||||||
|
return [
|
||||||
|
'id' => $activity->id,
|
||||||
|
'activity_no' => $activity->activity_no,
|
||||||
|
'activity_type' => $activity->activity_type,
|
||||||
|
'task_description' => $activity->task_description,
|
||||||
|
'estimated_completed_at' => data_get($activity, 'estimated_completed_at'),
|
||||||
|
'completed_at' => data_get($activity, 'completed_at'),
|
||||||
|
'notification_sent_at' => data_get($activity, 'notification_sent_at'),
|
||||||
|
'notification_status' => data_get($activity, 'notification_status'),
|
||||||
|
'note_to_customer' => data_get($activity, 'note_to_customer'),
|
||||||
|
'assignation' => null,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$activitiesList = $activities->values()->all();
|
||||||
|
$pendingActivities = $activities
|
||||||
|
->filter(function (array $activity) {
|
||||||
|
return empty($activity['completed_at']) && ! empty($activity['estimated_completed_at']);
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
$completedActivities = $activities
|
||||||
|
->filter(function (array $activity) {
|
||||||
|
return ! empty($activity['completed_at']);
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
FacadesLog::info('Hydrated client data', [
|
||||||
|
'localClient' => $localClient->toArray(),
|
||||||
|
'assignments' => $assignments,
|
||||||
|
'users' => $users,
|
||||||
|
'invoices' => $invoices,
|
||||||
|
]);
|
||||||
|
return [
|
||||||
|
$localClient,
|
||||||
|
$assignments,
|
||||||
|
$users,
|
||||||
|
$invoices,
|
||||||
|
$activitiesList,
|
||||||
|
$pendingActivities,
|
||||||
|
$completedActivities,
|
||||||
|
$lifeTimeSpend,
|
||||||
|
$clientAdjustments,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insertCSVDataToDB()
|
||||||
|
{
|
||||||
|
$collection = (new FastExcel)->import(storage_path('app/public/csv/Fixed_HJ.csv'));
|
||||||
|
$array = $collection->toArray();
|
||||||
|
foreach ($array as $row) {
|
||||||
|
$startDate = Carbon::parse($row['start_date'])->format('Y-m-d');
|
||||||
|
$endDate = Carbon::parse($row['end_date'])->format('Y-m-d');
|
||||||
|
$client = Client::where('customer_id', str_replace('-', '', $row['customer_id']))->first();
|
||||||
|
// dd($row);
|
||||||
|
if ($client) {
|
||||||
|
$salesUser = User::where('name', $row['sales'])->first();
|
||||||
|
$pic = User::where('name', $row['pic'])->first();
|
||||||
|
if ($pic) {
|
||||||
|
ClientUserAssignation::updateOrCreate(
|
||||||
|
[
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'user_id' => $pic->id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($salesUser) {
|
||||||
|
ClientUserAssignation::updateOrCreate(
|
||||||
|
[
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'role' => ClientUserAssignation::ROLE_SALES_PERSON,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'user_id' => $salesUser->id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$row['client_id'] = $client->id;
|
||||||
|
$spend = 0;
|
||||||
|
if ($client->status != 'CANCELED') {
|
||||||
|
$campaigns = $this->adsService->listCampaigns($row['customer_id']);
|
||||||
|
FacadesLog::info('Hydrated client data', [
|
||||||
|
'campaigns' => $campaigns,
|
||||||
|
]);
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
FacadesLog::info('Hydrated client data', [
|
||||||
|
'campaigns' => $campaign['id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($startDate) || empty($endDate)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
$metrics = $this->adsService->listCampaignsMetricsById(
|
||||||
|
$row['customer_id'],
|
||||||
|
$campaign['id'],
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
FacadesLog::info('Hydrated client data', [
|
||||||
|
'metrics' => $metrics,
|
||||||
|
]);
|
||||||
|
$totalSpend = array_sum(array_column($metrics, 'actual_spend'));
|
||||||
|
$spend += $totalSpend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientInvoice::updateOrCreate(
|
||||||
|
['invoice_no' => $row['invoice_no']],
|
||||||
|
[
|
||||||
|
'client_id' => $row['client_id'],
|
||||||
|
'is_credit_card' => intval($row['media_fee']) == 0 ? 1 : 0,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'management_fee' => intval($row['management_fee']),
|
||||||
|
'media_fee' => intval($row['media_fee']),
|
||||||
|
'tax_percent' => 8,
|
||||||
|
'nett_amount' => intval($row['media_fee']) > 0 ? intval($row['media_fee']) / 1.08 : 0,
|
||||||
|
'total_spending' => $spend,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
FacadesLog::warning('Client not found for customer_id: '.str_replace('-', '', $row['customer_id']));
|
||||||
|
continue; // Skip this row if client not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
app/Http/Controllers/GoogleController.php
Normal file
86
app/Http/Controllers/GoogleController.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Services\GoogleAdsService;
|
||||||
|
use App\Services\UserHierarchyService;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
// use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class GoogleController extends Controller
|
||||||
|
{
|
||||||
|
protected $adsService;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
GoogleAdsService $adsService,
|
||||||
|
private UserHierarchyService $hierarchyService,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$this->adsService = $adsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function campaigns($id)
|
||||||
|
{
|
||||||
|
$campaigns = $this->adsService->listCampaigns($id);
|
||||||
|
return response()->json($campaigns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listCampaignsMetrics(Request $request)
|
||||||
|
{
|
||||||
|
$clientCustomerId = $request->clientCustomerId;
|
||||||
|
$startDate = $request->startDate;
|
||||||
|
$endDate = $request->endDate;
|
||||||
|
|
||||||
|
$client = Client::where('customer_id', $clientCustomerId)->firstOrFail();
|
||||||
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
|
||||||
|
|
||||||
|
$campaigns = $this->adsService->listCampaigns($clientCustomerId);
|
||||||
|
$campaignsWithMetrics = [];
|
||||||
|
foreach ($campaigns as $campaign) {
|
||||||
|
$metrics = $this->adsService->listCampaignsMetrics(
|
||||||
|
$clientCustomerId,
|
||||||
|
$campaign['id'],
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
$campaign['total_actual_spend'] = 0;
|
||||||
|
$campaign['total_impressions'] = 0;
|
||||||
|
$campaign['total_clicks'] = 0;
|
||||||
|
$campaign['metrics'] = [];
|
||||||
|
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
$campaign['total_actual_spend'] += $metric['actual_spend'] ?? 0;
|
||||||
|
$campaign['total_impressions'] += $metric['impressions'] ?? 0;
|
||||||
|
$campaign['total_clicks'] += $metric['clicks'] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPC calculation
|
||||||
|
$campaign['cpc'] = $campaign['total_clicks'] > 0
|
||||||
|
? $campaign['total_actual_spend'] / $campaign['total_clicks']
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
$campaign['total_actual_spend'] = number_format($campaign['total_actual_spend'], 2, '.', '');
|
||||||
|
|
||||||
|
$campaign['metrics'] = $metrics;
|
||||||
|
|
||||||
|
$campaignsWithMetrics[] = $campaign;
|
||||||
|
}
|
||||||
|
$totalSpend = array_sum(array_column($campaignsWithMetrics, 'total_actual_spend'));
|
||||||
|
$totalClicks = array_sum(array_column($campaignsWithMetrics, 'total_clicks'));
|
||||||
|
return response()->json([
|
||||||
|
'campaigns' => $campaignsWithMetrics,
|
||||||
|
'summary' => [
|
||||||
|
'total_actual_spend' => number_format(array_sum(array_column($campaignsWithMetrics, 'total_actual_spend')), 2, '.', ''),
|
||||||
|
'total_impressions' => array_sum(array_column($campaignsWithMetrics, 'total_impressions')),
|
||||||
|
'total_clicks' => array_sum(array_column($campaignsWithMetrics, 'total_clicks')),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
app/Http/Controllers/Management/RoleController.php
Normal file
140
app/Http/Controllers/Management/RoleController.php
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Management;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use App\Services\RoleService;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class RoleController extends Controller
|
||||||
|
{
|
||||||
|
protected $roleService;
|
||||||
|
public function __construct(RoleService $roleService)
|
||||||
|
{
|
||||||
|
$this->roleService = $roleService;
|
||||||
|
// $this->middleware('auth:web');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
$roles = $this->roleService->getAllRoles();
|
||||||
|
return Inertia::render('management/roles/index', [
|
||||||
|
'roles' => $roles['data'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function create(): Response
|
||||||
|
// {
|
||||||
|
// $permissions = Permission::all()->mapWithKeys(fn (Permission $permission) => [
|
||||||
|
// $permission->name => false,
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// return Inertia::render('Admin/UserManagement/Role/Create', [
|
||||||
|
// 'permissions' => Arr::undot($permissions),
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function store(Request $request): RedirectResponse
|
||||||
|
// {
|
||||||
|
// $request->validate([
|
||||||
|
// 'name' => ['required', 'string'],
|
||||||
|
// 'permissions' => ['required', 'array'],
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// $permissions = Arr::dot($request->permissions);
|
||||||
|
// $permissions = array_keys(array_filter($permissions, fn ($permission) => $permission === true));
|
||||||
|
|
||||||
|
// $role = Role::create([
|
||||||
|
// 'name' => $request->name,
|
||||||
|
// ]);
|
||||||
|
// $role->syncPermissions($permissions);
|
||||||
|
|
||||||
|
// return redirect()
|
||||||
|
// ->route('admin.user_management.role.index')
|
||||||
|
// ->with('message-info', 'Role ' . $role->name . ' has created successfully.');
|
||||||
|
// }
|
||||||
|
|
||||||
|
public function edit(int $id): Response
|
||||||
|
{
|
||||||
|
$role = Role::findOrFail($id);
|
||||||
|
|
||||||
|
// 1. Get all permissions with their "checked" state
|
||||||
|
$permissions = Permission::all()->map(function ($permission) use ($role) {
|
||||||
|
return [
|
||||||
|
'id' => $permission->id,
|
||||||
|
'name' => $permission->name, // e.g. "user.create"
|
||||||
|
'description' => $permission->description,
|
||||||
|
'checked' => $role->hasPermissionTo($permission->name),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Group them by the prefix (the part before the dot)
|
||||||
|
$grouped = $permissions->groupBy(function ($item) {
|
||||||
|
return explode('.', $item['name'])[0];
|
||||||
|
})->map(function ($group) {
|
||||||
|
// 3. Force it to be a sequential array so JS sees it as []
|
||||||
|
return $group->values()->toArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Inertia::render('management/roles/edit', [
|
||||||
|
'role' => $role,
|
||||||
|
'permissions' => $grouped,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(int $id, Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$role = Role::findOrFail($id);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string'],
|
||||||
|
'permissions' => ['required', 'array'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Extract only the IDs of permissions that are checked
|
||||||
|
$permissionIds = [];
|
||||||
|
foreach ($request->permissions as $group => $items) {
|
||||||
|
foreach ($items as $permission) {
|
||||||
|
if (! empty($permission['checked'])) {
|
||||||
|
$permissionIds[] = $permission['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update Role name
|
||||||
|
$role->update([
|
||||||
|
'name' => $request->name
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 3. Sync permissions using the collected IDs
|
||||||
|
$role->syncPermissions($permissionIds);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('management.roles.index')
|
||||||
|
->with('message-info', 'Role '.$role->name.' has updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public function destroy(int $id): RedirectResponse
|
||||||
|
// {
|
||||||
|
// $role = Role::findOrFail($id);
|
||||||
|
|
||||||
|
// if ($role->name == 'Admin') {
|
||||||
|
// return redirect()
|
||||||
|
// ->route('user_management.user.index')
|
||||||
|
// ->with('message-error', 'User ' . $role->name . ' cannot be delete.');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $role->delete();
|
||||||
|
|
||||||
|
// return redirect()
|
||||||
|
// ->route('admin.user_management.role.index')
|
||||||
|
// ->with('message-info', 'Role has deleted successfully.');
|
||||||
|
// }
|
||||||
|
}
|
||||||
152
app/Http/Controllers/Management/UserController.php
Normal file
152
app/Http/Controllers/Management/UserController.php
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Management;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\UserHierarchyService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private UserHierarchyService $hierarchyService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function __construct()
|
||||||
|
// {
|
||||||
|
// $this->middleware('auth:web');
|
||||||
|
// }
|
||||||
|
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
$users = User::with(['roles', 'manager'])->get();
|
||||||
|
$roles = Role::all();
|
||||||
|
|
||||||
|
return Inertia::render('management/users/index', [
|
||||||
|
'users' => $users,
|
||||||
|
'roles' => $roles,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): Response
|
||||||
|
{
|
||||||
|
$roles = Role::all();
|
||||||
|
$managers = $this->managerOptions();
|
||||||
|
|
||||||
|
return Inertia::render('management/users/create', [
|
||||||
|
'roles' => $roles,
|
||||||
|
'managers' => $managers,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string'],
|
||||||
|
'email' => ['required', 'email', 'unique:users,email'],
|
||||||
|
'password' => ['required', 'string', 'min:8'],
|
||||||
|
'manager_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
'roles' => ['required', 'array'],
|
||||||
|
'roles.*' => ['required', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'manager_id' => $request->manager_id,
|
||||||
|
]);
|
||||||
|
$user->syncRoles($request->roles);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('management.users.index')
|
||||||
|
->with('message-info', 'User ' . $user->name . ' has created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(int $id): Response
|
||||||
|
{
|
||||||
|
$user = User::with(['roles', 'manager'])->findOrFail($id);
|
||||||
|
$roles = Role::all();
|
||||||
|
$managers = $this->managerOptions($user->id);
|
||||||
|
|
||||||
|
return Inertia::render('management/users/edit', [
|
||||||
|
'user' => $user,
|
||||||
|
'roles' => $roles,
|
||||||
|
'managers' => $managers,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(int $id, Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string'],
|
||||||
|
'email' => ['required', 'email', 'unique:users,email,' . $user->id],
|
||||||
|
'password' => ['nullable', 'string', 'min:8'],
|
||||||
|
'manager_id' => [
|
||||||
|
'nullable',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('users', 'id')->where(fn ($query) => $query->where('id', '!=', $user->id)),
|
||||||
|
],
|
||||||
|
'roles' => ['required', 'array'],
|
||||||
|
'roles.*' => ['required', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->hierarchyService->wouldCreateCycle($user, $request->integer('manager_id') ?: null)) {
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['manager_id' => 'A user cannot report to themselves or one of their reports.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->name = $request->name;
|
||||||
|
$user->email = $request->email;
|
||||||
|
$user->manager_id = $request->manager_id;
|
||||||
|
if ($request->password !== null) {
|
||||||
|
$user->password = Hash::make($request->password);
|
||||||
|
}
|
||||||
|
$user->save();
|
||||||
|
$user->syncRoles($request->roles);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('management.users.index')
|
||||||
|
->with('message-info', 'User ' . $user->name . ' has updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
|
if ($user->id === 1) {
|
||||||
|
return redirect()
|
||||||
|
->route('management.users.index')
|
||||||
|
->with('message-error', 'User ' . $user->name . ' cannot be delete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('management.users.index')
|
||||||
|
->with('message-info', 'User ' . $user->name . ' has deleted successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function managerOptions(?int $excludedUserId = null): array
|
||||||
|
{
|
||||||
|
return User::query()
|
||||||
|
->when($excludedUserId !== null, fn ($query) => $query->where('id', '!=', $excludedUserId))
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'email'])
|
||||||
|
->map(fn (User $user) => [
|
||||||
|
'value' => (string) $user->id,
|
||||||
|
'label' => trim($user->name . ' (' . $user->email . ')'),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Http/Controllers/Settings/PasswordController.php
Normal file
38
app/Http/Controllers/Settings/PasswordController.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class PasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the user's password settings page.
|
||||||
|
*/
|
||||||
|
public function edit(): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('settings/password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's password.
|
||||||
|
*/
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => $validated['password'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
}
|
||||||
63
app/Http/Controllers/Settings/ProfileController.php
Normal file
63
app/Http/Controllers/Settings/ProfileController.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Settings\ProfileUpdateRequest;
|
||||||
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the user's profile settings page.
|
||||||
|
*/
|
||||||
|
public function edit(Request $request): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('settings/profile', [
|
||||||
|
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
|
||||||
|
'status' => $request->session()->get('status'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile settings.
|
||||||
|
*/
|
||||||
|
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->user()->fill($request->validated());
|
||||||
|
|
||||||
|
if ($request->user()->isDirty('email')) {
|
||||||
|
$request->user()->email_verified_at = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->save();
|
||||||
|
|
||||||
|
return to_route('profile.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the user's account.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'password' => ['required', 'current_password'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
use Illuminate\Routing\Controllers\Middleware;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
|
||||||
|
class TwoFactorAuthenticationController extends Controller implements HasMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the middleware that should be assigned to the controller.
|
||||||
|
*/
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')
|
||||||
|
? [new Middleware('password.confirm', only: ['show'])]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the user's two-factor authentication settings page.
|
||||||
|
*/
|
||||||
|
public function show(TwoFactorAuthenticationRequest $request): Response
|
||||||
|
{
|
||||||
|
$request->ensureStateIsValid();
|
||||||
|
|
||||||
|
return Inertia::render('settings/two-factor', [
|
||||||
|
'twoFactorEnabled' => $request->user()->hasEnabledTwoFactorAuthentication(),
|
||||||
|
'requiresConfirmation' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Http/Middleware/HandleAppearance.php
Normal file
23
app/Http/Middleware/HandleAppearance.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class HandleAppearance
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
app/Http/Middleware/HandleInertiaRequests.php
Normal file
60
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Inspiring;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Middleware;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
class HandleInertiaRequests extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The root template that's loaded on the first page visit.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/server-side-setup#root-template
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rootView = 'app';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the current asset version.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/asset-versioning
|
||||||
|
*/
|
||||||
|
public function version(Request $request): ?string
|
||||||
|
{
|
||||||
|
return parent::version($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the props that are shared by default.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/shared-data
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function share(Request $request): array
|
||||||
|
{
|
||||||
|
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
|
||||||
|
|
||||||
|
return [
|
||||||
|
...parent::share($request),
|
||||||
|
'name' => config('app.name'),
|
||||||
|
'quote' => ['message' => trim($message), 'author' => trim($author)],
|
||||||
|
'auth' => [
|
||||||
|
'user' => $request->user()?->load('roles'),
|
||||||
|
'permissions' => fn () => $request->user('web') !== null ?
|
||||||
|
Permission::all()->mapWithKeys(fn (Permission $permission) => [$permission->name => $request->user()->hasPermissionTo($permission->name)])
|
||||||
|
: null,
|
||||||
|
],
|
||||||
|
'flash' => [
|
||||||
|
'message-info' => fn () => $request->session()->get('message-info'),
|
||||||
|
'message-warning' => fn () => $request->session()->get('message-warning'),
|
||||||
|
'message-error' => fn () => $request->session()->get('message-error'),
|
||||||
|
],
|
||||||
|
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Http/Requests/Settings/ProfileUpdateRequest.php
Normal file
32
app/Http/Requests/Settings/ProfileUpdateRequest.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Settings;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ProfileUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'lowercase',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class)->ignore($this->user()->id),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Settings;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
use Laravel\Fortify\InteractsWithTwoFactorState;
|
||||||
|
|
||||||
|
class TwoFactorAuthenticationRequest extends FormRequest
|
||||||
|
{
|
||||||
|
use InteractsWithTwoFactorState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return Features::enabled(Features::twoFactorAuthentication());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Models/CampaignFinance.php
Normal file
32
app/Models/CampaignFinance.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class CampaignFinance extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'campaign_id',
|
||||||
|
'total_media_fee',
|
||||||
|
'withholding_tax_rate',
|
||||||
|
'total_media_after_tax',
|
||||||
|
'accumulated_media_fee',
|
||||||
|
'monthly_media_fee',
|
||||||
|
'management_fee',
|
||||||
|
'duration_months',
|
||||||
|
'checking_date',
|
||||||
|
'budget_spent',
|
||||||
|
'balance_as_at_2024_03_01',
|
||||||
|
'additional_sst_2_percent',
|
||||||
|
'balance_reminder',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function campaign()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Campaign::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Models/CampaignRemark.php
Normal file
22
app/Models/CampaignRemark.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class CampaignRemark extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'campaign_id',
|
||||||
|
'remark_date',
|
||||||
|
'remark',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function campaign()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Campaign::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Models/CampaignReport.php
Normal file
23
app/Models/CampaignReport.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class CampaignReport extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'campaign_id',
|
||||||
|
'report_status',
|
||||||
|
'report_date',
|
||||||
|
'report_file_path',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function campaign()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Campaign::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
app/Models/Client.php
Normal file
88
app/Models/Client.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Models\ClientProjectActivities;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Client extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['name','customer_id','status','time_zone','industry','sql_acc_code'];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'latest_remaining_amount',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function campaigns()
|
||||||
|
{
|
||||||
|
return $this->hasMany(GoogleCampaign::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assignations()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ClientUserAssignation::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoices(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ClientInvoice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoiceAdjustments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ClientInvoiceAdjustment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLatestRemainingAmountAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->latestRemainingAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function latestRemainingAmount(?callable $invoiceSpendingResolver = null): string
|
||||||
|
{
|
||||||
|
$invoices = $this->relationLoaded('invoices')
|
||||||
|
? $this->invoices
|
||||||
|
: $this->invoices()->get(['client_id', 'is_credit_card', 'nett_amount', 'total_spending']);
|
||||||
|
|
||||||
|
$adjustments = $this->relationLoaded('invoiceAdjustments')
|
||||||
|
? $this->invoiceAdjustments
|
||||||
|
: $this->invoiceAdjustments()->get(['client_id', 'entry_type', 'amount']);
|
||||||
|
|
||||||
|
$nettAmount = $invoices
|
||||||
|
->sum(fn (ClientInvoice $invoice) => (float) ($invoice->nett_amount ?? 0));
|
||||||
|
|
||||||
|
$billableSpending = $invoices
|
||||||
|
->reject(fn (ClientInvoice $invoice) => $invoice->is_credit_card)
|
||||||
|
->sum(function (ClientInvoice $invoice) use ($invoiceSpendingResolver) {
|
||||||
|
if ($invoiceSpendingResolver !== null) {
|
||||||
|
return (float) $invoiceSpendingResolver($invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) ($invoice->total_spending ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
$adjustmentNet = $adjustments->sum(function (ClientInvoiceAdjustment $adjustment) {
|
||||||
|
$amount = (float) ($adjustment->amount ?? 0);
|
||||||
|
|
||||||
|
return $adjustment->entry_type === ClientInvoiceAdjustment::TYPE_CREDIT
|
||||||
|
? $amount
|
||||||
|
: -$amount;
|
||||||
|
});
|
||||||
|
|
||||||
|
return number_format(max(0, $nettAmount + $adjustmentNet - $billableSpending), 2, '.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activitiesList(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ClientProjectActivities::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function customers()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ClientCustomer::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/Models/ClientCustomer.php
Normal file
10
app/Models/ClientCustomer.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ClientCustomer extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['client_id', 'sql_acc_code'];
|
||||||
|
}
|
||||||
63
app/Models/ClientInvoice.php
Normal file
63
app/Models/ClientInvoice.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class ClientInvoice extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'client_id',
|
||||||
|
'pending_sql_acc_code',
|
||||||
|
'pending_client_name',
|
||||||
|
'invoice_no',
|
||||||
|
'linked_invoice_id',
|
||||||
|
'is_credit_card',
|
||||||
|
'is_paid',
|
||||||
|
'approved_at',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'payment_no',
|
||||||
|
'amount',
|
||||||
|
'tax_percent',
|
||||||
|
'media_fee',
|
||||||
|
'management_fee',
|
||||||
|
'nett_amount',
|
||||||
|
'total_spending',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'start_date' => 'date',
|
||||||
|
'end_date' => 'date',
|
||||||
|
'is_credit_card' => 'boolean',
|
||||||
|
'is_paid' => 'boolean',
|
||||||
|
'approved_at' => 'datetime',
|
||||||
|
'amount' => 'decimal:2',
|
||||||
|
'tax_percent' => 'decimal:2',
|
||||||
|
'media_fee' => 'decimal:2',
|
||||||
|
'management_fee' => 'decimal:2',
|
||||||
|
'nett_amount' => 'decimal:2',
|
||||||
|
'total_spending' => 'decimal:2',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function client(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Client::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function linkedInvoice(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(self::class, 'linked_invoice_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function linkedInvoices(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(self::class, 'linked_invoice_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
app/Models/ClientInvoiceAdjustment.php
Normal file
31
app/Models/ClientInvoiceAdjustment.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ClientInvoiceAdjustment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public const TYPE_DEBIT = 'debit';
|
||||||
|
public const TYPE_CREDIT = 'credit';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'client_id',
|
||||||
|
'entry_type',
|
||||||
|
'amount',
|
||||||
|
'remark',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'amount' => 'decimal:2',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function client(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Client::class, 'client_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Models/ClientProjectActivities.php
Normal file
54
app/Models/ClientProjectActivities.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
|
class ClientProjectActivities extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'client_id',
|
||||||
|
'activity_type',
|
||||||
|
'task_description',
|
||||||
|
'estimated_completed_at',
|
||||||
|
'completed_at',
|
||||||
|
'amount',
|
||||||
|
'notification_status',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'estimated_completed_at' => 'datetime',
|
||||||
|
'completed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = ['activity_no'];
|
||||||
|
|
||||||
|
protected function serializeDate(DateTimeInterface $date): string
|
||||||
|
{
|
||||||
|
return $date->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function client(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Client::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIsCompletedAttribute(): bool
|
||||||
|
{
|
||||||
|
return ! is_null($this->completed_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActivityNoAttribute()
|
||||||
|
{
|
||||||
|
return 'ACT'.str_pad($this->id, 4, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Models/ClientUserAssignation.php
Normal file
41
app/Models/ClientUserAssignation.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ClientUserAssignation extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public const ROLE_ASSIGNED_PERSON = 1;
|
||||||
|
public const ROLE_SALES_PERSON = 2;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'client_id',
|
||||||
|
'user_id',
|
||||||
|
'role',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function roles(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'id' => self::ROLE_ASSIGNED_PERSON,
|
||||||
|
'label' => 'Assigned Person',
|
||||||
|
'field' => 'assigned_person',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::ROLE_SALES_PERSON,
|
||||||
|
'label' => 'Sales Person',
|
||||||
|
'field' => 'sales_person',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/Models/Customers.php
Normal file
13
app/Models/Customers.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Models\ClientProjectActivities;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Customers extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
}
|
||||||
18
app/Models/GoogleAd.php
Normal file
18
app/Models/GoogleAd.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleAd extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_ad_group_id',
|
||||||
|
'ad_id',
|
||||||
|
'name',
|
||||||
|
'status',
|
||||||
|
'type',
|
||||||
|
'final_urls',
|
||||||
|
'approval_status',
|
||||||
|
];
|
||||||
|
}
|
||||||
17
app/Models/GoogleAdGroup.php
Normal file
17
app/Models/GoogleAdGroup.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleAdGroup extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_campaign_id',
|
||||||
|
'ad_group_id',
|
||||||
|
'name',
|
||||||
|
'status',
|
||||||
|
'type',
|
||||||
|
'cpc_bid_micros',
|
||||||
|
];
|
||||||
|
}
|
||||||
19
app/Models/GoogleAdGroupMetric.php
Normal file
19
app/Models/GoogleAdGroupMetric.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleAdGroupMetric extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_ad_group_id',
|
||||||
|
'date',
|
||||||
|
'impressions',
|
||||||
|
'clicks',
|
||||||
|
'actual_spend',
|
||||||
|
'conversions',
|
||||||
|
'ctr',
|
||||||
|
'average_cpc',
|
||||||
|
];
|
||||||
|
}
|
||||||
19
app/Models/GoogleAdMetric.php
Normal file
19
app/Models/GoogleAdMetric.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleAdMetric extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_ad_id',
|
||||||
|
'date',
|
||||||
|
'impressions',
|
||||||
|
'clicks',
|
||||||
|
'actual_spend',
|
||||||
|
'conversions',
|
||||||
|
'ctr',
|
||||||
|
'average_cpc',
|
||||||
|
];
|
||||||
|
}
|
||||||
20
app/Models/GoogleAsset.php
Normal file
20
app/Models/GoogleAsset.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleAsset extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_campaign_id',
|
||||||
|
'asset_id',
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'added_by',
|
||||||
|
'use_case',
|
||||||
|
'approval_status',
|
||||||
|
'review_status',
|
||||||
|
];
|
||||||
|
}
|
||||||
19
app/Models/GoogleAssetMetric.php
Normal file
19
app/Models/GoogleAssetMetric.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleAssetMetric extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_asset_id',
|
||||||
|
'date',
|
||||||
|
'impressions',
|
||||||
|
'clicks',
|
||||||
|
'actual_spend',
|
||||||
|
'conversions',
|
||||||
|
'ctr',
|
||||||
|
'average_cpc',
|
||||||
|
];
|
||||||
|
}
|
||||||
54
app/Models/GoogleCampaign.php
Normal file
54
app/Models/GoogleCampaign.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleCampaign extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'client_id',
|
||||||
|
'campaign_id',
|
||||||
|
'name',
|
||||||
|
'status',
|
||||||
|
'channel',
|
||||||
|
'sub_channel',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'resource_name',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
public function client()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Client::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function consultant()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class, 'consultant_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function campaignManager()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class, 'campaign_manager_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function finances()
|
||||||
|
{
|
||||||
|
return $this->hasOne(CampaignFinance::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reports()
|
||||||
|
{
|
||||||
|
return $this->hasMany(CampaignReport::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remarks()
|
||||||
|
{
|
||||||
|
return $this->hasMany(CampaignRemark::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Models/GoogleCampaignMetric.php
Normal file
22
app/Models/GoogleCampaignMetric.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleCampaignMetric extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_campaign_id',
|
||||||
|
'google_campaign_metric_id',
|
||||||
|
'date',
|
||||||
|
'impressions',
|
||||||
|
'clicks',
|
||||||
|
'daily_budget',
|
||||||
|
'actual_spend',
|
||||||
|
'conversions',
|
||||||
|
'conversions_value',
|
||||||
|
'interactions',
|
||||||
|
'interaction_rate',
|
||||||
|
];
|
||||||
|
}
|
||||||
20
app/Models/GoogleKeywordMetric.php
Normal file
20
app/Models/GoogleKeywordMetric.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleKeywordMetric extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_keyword_id',
|
||||||
|
'date',
|
||||||
|
'google_keyword_metric_id',
|
||||||
|
'impressions',
|
||||||
|
'clicks',
|
||||||
|
'actual_spend',
|
||||||
|
'conversions',
|
||||||
|
'ctr',
|
||||||
|
'average_cpc',
|
||||||
|
];
|
||||||
|
}
|
||||||
16
app/Models/GoogleKeywords.php
Normal file
16
app/Models/GoogleKeywords.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoogleKeywords extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'google_ad_group_id',
|
||||||
|
'keyword_id',
|
||||||
|
'text',
|
||||||
|
'status',
|
||||||
|
'match_type',
|
||||||
|
];
|
||||||
|
}
|
||||||
71
app/Models/User.php
Normal file
71
app/Models/User.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
|
use HasFactory, Notifiable, TwoFactorAuthenticatable, HasRoles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'manager_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'two_factor_secret',
|
||||||
|
'two_factor_recovery_codes',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
'two_factor_confirmed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function manager(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(self::class, 'manager_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function directReports(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(self::class, 'manager_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clientAssignations(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ClientUserAssignation::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Providers/AppServiceProvider.php
Normal file
24
app/Providers/AppServiceProvider.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
91
app/Providers/FortifyServiceProvider.php
Normal file
91
app/Providers/FortifyServiceProvider.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Actions\Fortify\CreateNewUser;
|
||||||
|
use App\Actions\Fortify\ResetUserPassword;
|
||||||
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
use Laravel\Fortify\Fortify;
|
||||||
|
|
||||||
|
class FortifyServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->configureActions();
|
||||||
|
$this->configureViews();
|
||||||
|
$this->configureRateLimiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Fortify actions.
|
||||||
|
*/
|
||||||
|
private function configureActions(): void
|
||||||
|
{
|
||||||
|
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||||
|
Fortify::createUsersUsing(CreateNewUser::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Fortify views.
|
||||||
|
*/
|
||||||
|
private function configureViews(): void
|
||||||
|
{
|
||||||
|
Fortify::loginView(fn (Request $request) => Inertia::render('auth/login', [
|
||||||
|
'canResetPassword' => Features::enabled(Features::resetPasswords()),
|
||||||
|
'canRegister' => Features::enabled(Features::registration()),
|
||||||
|
'status' => $request->session()->get('status'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/reset-password', [
|
||||||
|
'email' => $request->email,
|
||||||
|
'token' => $request->route('token'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Fortify::requestPasswordResetLinkView(fn (Request $request) => Inertia::render('auth/forgot-password', [
|
||||||
|
'status' => $request->session()->get('status'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Fortify::verifyEmailView(fn (Request $request) => Inertia::render('auth/verify-email', [
|
||||||
|
'status' => $request->session()->get('status'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Fortify::registerView(fn () => Inertia::render('auth/register'));
|
||||||
|
|
||||||
|
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge'));
|
||||||
|
|
||||||
|
Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure rate limiting.
|
||||||
|
*/
|
||||||
|
private function configureRateLimiting(): void
|
||||||
|
{
|
||||||
|
RateLimiter::for('two-factor', function (Request $request) {
|
||||||
|
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
RateLimiter::for('login', function (Request $request) {
|
||||||
|
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||||
|
|
||||||
|
return Limit::perMinute(5)->by($throttleKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Services/ClickHouseService.php
Normal file
27
app/Services/ClickHouseService.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use ClickHouseDB\Client;
|
||||||
|
|
||||||
|
class ClickHouseService
|
||||||
|
{
|
||||||
|
protected $client;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->client = new Client([
|
||||||
|
'host' => env('CLICKHOUSE_HOST'),
|
||||||
|
'port' => env('CLICKHOUSE_PORT', 8123),
|
||||||
|
'username' => env('CLICKHOUSE_USER', 'default'),
|
||||||
|
'password' => env('CLICKHOUSE_PASSWORD', ''),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->client->database(env('CLICKHOUSE_DATABASE', 'default'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClient()
|
||||||
|
{
|
||||||
|
return $this->client;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Services/ClientInvoiceApprovalService.php
Normal file
30
app/Services/ClientInvoiceApprovalService.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\ClientInvoice;
|
||||||
|
|
||||||
|
class ClientInvoiceApprovalService
|
||||||
|
{
|
||||||
|
public function approve(ClientInvoice $invoice): ClientInvoice
|
||||||
|
{
|
||||||
|
if ($invoice->approved_at === null) {
|
||||||
|
$invoice->forceFill([
|
||||||
|
'approved_at' => now(),
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $invoice->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requireApproval(ClientInvoice $invoice): ClientInvoice
|
||||||
|
{
|
||||||
|
if ($invoice->approved_at !== null) {
|
||||||
|
$invoice->forceFill([
|
||||||
|
'approved_at' => null,
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $invoice->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Services/ClientLookupService.php
Normal file
35
app/Services/ClientLookupService.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
|
||||||
|
class ClientLookupService
|
||||||
|
{
|
||||||
|
public function findBySqlAccCode(?string $sqlAccCode): ?Client
|
||||||
|
{
|
||||||
|
$sqlAccCode = $this->normalizeSqlAccCode($sqlAccCode);
|
||||||
|
|
||||||
|
if ($sqlAccCode === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Client::query()
|
||||||
|
->where('sql_acc_code', $sqlAccCode)
|
||||||
|
->orWhereHas('customers', function ($query) use ($sqlAccCode) {
|
||||||
|
$query->where('sql_acc_code', $sqlAccCode);
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeSqlAccCode(?string $sqlAccCode): ?string
|
||||||
|
{
|
||||||
|
if ($sqlAccCode === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sqlAccCode = trim($sqlAccCode);
|
||||||
|
|
||||||
|
return $sqlAccCode === '' ? null : $sqlAccCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
819
app/Services/GoogleAdsService.php
Normal file
819
app/Services/GoogleAdsService.php
Normal file
@ -0,0 +1,819 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Google\Ads\GoogleAds\Lib\V22\GoogleAdsClientBuilder;
|
||||||
|
use Google\Ads\GoogleAds\Lib\OAuth2TokenBuilder;
|
||||||
|
use Google\Ads\GoogleAds\V22\Services\SearchGoogleAdsRequest;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\CustomerStatusEnum\CustomerStatus;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\CampaignStatusEnum\CampaignStatus;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AdvertisingChannelSubTypeEnum\AdvertisingChannelSubType;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AdGroupStatusEnum\AdGroupStatus;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AdGroupTypeEnum\AdGroupType;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\PolicyApprovalStatusEnum\PolicyApprovalStatus;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AdGroupAdStatusEnum\AdGroupAdStatus;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AdTypeEnum\AdType;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\KeywordMatchTypeEnum\KeywordMatchType;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AdGroupCriterionStatusEnum\AdGroupCriterionStatus;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AssetSourceEnum\AssetSource;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AssetTypeEnum\AssetType;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AssetFieldTypeEnum\AssetFieldType;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\PolicyReviewStatusEnum\PolicyReviewStatus;
|
||||||
|
use Google\Ads\GoogleAds\V22\Enums\AssetLinkStatusEnum\AssetLinkStatus;
|
||||||
|
use GPBMetadata\Google\Api\Log;
|
||||||
|
use Illuminate\Support\Facades\Log as FacadesLog;
|
||||||
|
|
||||||
|
class GoogleAdsService
|
||||||
|
{
|
||||||
|
protected $oAuth2Credential;
|
||||||
|
protected $developerToken;
|
||||||
|
protected $loginCustomerId;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->oAuth2Credential = (new OAuth2TokenBuilder())
|
||||||
|
->withClientId(env('GOOGLE_ADS_CLIENT_ID'))
|
||||||
|
->withClientSecret(env('GOOGLE_ADS_CLIENT_SECRET'))
|
||||||
|
->withRefreshToken(env('GOOGLE_ADS_REFRESH_TOKEN'))
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$this->developerToken = env('GOOGLE_ADS_DEVELOPER_TOKEN');
|
||||||
|
$this->loginCustomerId = env('GOOGLE_ADS_LOGIN_CUSTOMER_ID'); // MCC ID
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a GoogleAdsClient scoped to a specific customer ID
|
||||||
|
*/
|
||||||
|
protected function buildClient(string $customerId)
|
||||||
|
{
|
||||||
|
return (new GoogleAdsClientBuilder())
|
||||||
|
->withDeveloperToken($this->developerToken)
|
||||||
|
->withOAuth2Credential($this->oAuth2Credential)
|
||||||
|
->withLoginCustomerId($customerId)
|
||||||
|
->withTransport('rest')
|
||||||
|
->build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all accounts under MCC
|
||||||
|
*/
|
||||||
|
public function listAccounts(): array
|
||||||
|
{
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
customer_client.id,
|
||||||
|
customer_client.client_customer,
|
||||||
|
customer_client.level,
|
||||||
|
customer_client.manager,
|
||||||
|
customer_client.descriptive_name,
|
||||||
|
customer_client.status,
|
||||||
|
customer_client.applied_labels,
|
||||||
|
customer_client.resource_name,
|
||||||
|
customer_client.time_zone
|
||||||
|
FROM
|
||||||
|
customer_client
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $this->loginCustomerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
|
||||||
|
$accounts = [];
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$clientData = $row->getCustomerClient();
|
||||||
|
$accounts[] = [
|
||||||
|
'customer_id' => (string) $clientData->getId(),
|
||||||
|
'id' => $clientData->getId(),
|
||||||
|
'name' => $clientData->getDescriptiveName(),
|
||||||
|
'level' => $clientData->getLevel(),
|
||||||
|
'status' => CustomerStatus::name($clientData->getStatus()),
|
||||||
|
'resource_name' => $clientData->getResourceName(),
|
||||||
|
'time_zone' => $clientData->getTimeZone(),
|
||||||
|
'applied_labels' => $clientData->getAppliedLabels(),
|
||||||
|
'manager' => $clientData->getManager(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listCampaigns(string $clientCustomerId): array
|
||||||
|
{
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
campaign.id,
|
||||||
|
campaign.name,
|
||||||
|
campaign.status,
|
||||||
|
campaign.advertising_channel_type,
|
||||||
|
campaign.advertising_channel_sub_type,
|
||||||
|
campaign.start_date,
|
||||||
|
campaign.end_date,
|
||||||
|
campaign.resource_name,
|
||||||
|
campaign.serving_status,
|
||||||
|
campaign.bidding_strategy_type,
|
||||||
|
campaign.campaign_budget,
|
||||||
|
campaign.primary_status,
|
||||||
|
campaign.primary_status_reasons,
|
||||||
|
campaign.payment_mode
|
||||||
|
FROM campaign
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$campaigns = [];
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$c = $row->getCampaign();
|
||||||
|
$campaigns[] = [
|
||||||
|
'id' => $c->getId(),
|
||||||
|
'name' => $c->getName(),
|
||||||
|
'status' => CampaignStatus::name($c->getStatus()),
|
||||||
|
'channel' => AdvertisingChannelType::name($c->getAdvertisingChannelType()),
|
||||||
|
'sub_channel' => AdvertisingChannelSubType::name($c->getAdvertisingChannelSubType()),
|
||||||
|
'start_date' => $c->getStartDate(),
|
||||||
|
'end_date' => $c->getEndDate(),
|
||||||
|
'resource_name' => $c->getResourceName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $campaigns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listCampaignsMetrics(string $clientCustomerId, string $campaignId, string $startDate, string $endDate): array
|
||||||
|
{
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
campaign.id,
|
||||||
|
campaign.name,
|
||||||
|
segments.date,
|
||||||
|
campaign_budget.amount_micros,
|
||||||
|
metrics.impressions,
|
||||||
|
metrics.clicks,
|
||||||
|
metrics.ctr,
|
||||||
|
metrics.average_cpc,
|
||||||
|
metrics.conversions,
|
||||||
|
metrics.conversions_value,
|
||||||
|
metrics.cost_per_conversion,
|
||||||
|
metrics.all_conversions_from_interactions_rate,
|
||||||
|
metrics.interactions,
|
||||||
|
metrics.interaction_rate,
|
||||||
|
metrics.all_conversions,
|
||||||
|
metrics.view_through_conversions,
|
||||||
|
metrics.cost_micros
|
||||||
|
FROM campaign
|
||||||
|
WHERE segments.date BETWEEN '$startDate' AND '$endDate'
|
||||||
|
AND campaign.id = '$campaignId'
|
||||||
|
ORDER BY segments.date ASC
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
$response = $service->search($request);
|
||||||
|
FacadesLog::info('Google Ads Query', [
|
||||||
|
'query' => $query,
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
]);
|
||||||
|
$metrics = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$c = $row->getCampaign();
|
||||||
|
$metrics[] = [
|
||||||
|
'id' => $c->getId(),
|
||||||
|
'name' => $c->getName(),
|
||||||
|
'date' => $row->getSegments()->getDate(),
|
||||||
|
'impressions' => $row->getMetrics()->getImpressions(),
|
||||||
|
'clicks' => $row->getMetrics()->getClicks(),
|
||||||
|
// 'ctr' => round($row->getMetrics()->getCtr() * 100, 2),
|
||||||
|
'daily_budget' => $row->getCampaignBudget()->getAmountMicros() / 1000000,
|
||||||
|
'actual_spend' => number_format($row->getMetrics()->getCostMicros() / 1000000, 2, '.', ''),
|
||||||
|
// 'average_cpc' => round($row->getMetrics()->getAverageCpc() / 1000000, 2),
|
||||||
|
'conversions' => $row->getMetrics()->getConversions(),
|
||||||
|
'conversions_value' => $row->getMetrics()->getConversionsValue(),
|
||||||
|
'cost_per_conversion' =>number_format(( $row->getMetrics()->getCostPerConversion() / 1000000), 2, '.', ''),
|
||||||
|
// 'conversions_from_interactions_rate' => round($row->getMetrics()->getConversionsFromInteractionsRate() * 100, 2),
|
||||||
|
'interactions' => $row->getMetrics()->getInteractions(),
|
||||||
|
'interaction_rate' => number_format($row->getMetrics()->getInteractionRate() * 100, 2, '.', ''),
|
||||||
|
// 'all_conversions' => $row->getMetrics()->getAllConversions(),
|
||||||
|
// 'view_through_conversions' => $row->getMetrics()->getViewThroughConversions()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listCampaignsMetricsById(string $clientCustomerId, string $campaignId, string $startDate = '', string $endDate = ''): array
|
||||||
|
{
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
|
||||||
|
if (empty($startDate)) {
|
||||||
|
$startDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($endDate)) {
|
||||||
|
$endDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
campaign.id,
|
||||||
|
campaign.name,
|
||||||
|
campaign.start_date,
|
||||||
|
segments.date,
|
||||||
|
campaign_budget.amount_micros,
|
||||||
|
metrics.impressions,
|
||||||
|
metrics.clicks,
|
||||||
|
metrics.cost_micros,
|
||||||
|
metrics.conversions,
|
||||||
|
metrics.conversions_value,
|
||||||
|
metrics.interactions,
|
||||||
|
metrics.interaction_rate
|
||||||
|
FROM campaign
|
||||||
|
WHERE campaign.id = $campaignId
|
||||||
|
AND segments.date BETWEEN '$startDate' AND '$endDate'
|
||||||
|
ORDER BY segments.date ASC
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$metrics = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$c = $row->getCampaign();
|
||||||
|
$metrics[] = [
|
||||||
|
'id' => $c->getId(),
|
||||||
|
'name' => $c->getName(),
|
||||||
|
'start_date' => $c->getStartDate(), // Actual launch date
|
||||||
|
'date' => $row->getSegments()->getDate(),
|
||||||
|
'impressions' => $row->getMetrics()->getImpressions(),
|
||||||
|
'clicks' => $row->getMetrics()->getClicks(),
|
||||||
|
'daily_budget' => round($row->getCampaignBudget()->getAmountMicros() / 1000000, 2),
|
||||||
|
'actual_spend' => round($row->getMetrics()->getCostMicros() / 1000000, 2),
|
||||||
|
'conversions' => $row->getMetrics()->getConversions(),
|
||||||
|
'conversions_value' => $row->getMetrics()->getConversionsValue(),
|
||||||
|
'interactions' => $row->getMetrics()->getInteractions(),
|
||||||
|
'interaction_rate' => round($row->getMetrics()->getInteractionRate() * 100, 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listAdGroupsByCampaignId(string $clientCustomerId, string $campaignId): array
|
||||||
|
{
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
ad_group.id,
|
||||||
|
ad_group.name,
|
||||||
|
ad_group.status,
|
||||||
|
ad_group.type,
|
||||||
|
ad_group.cpc_bid_micros,
|
||||||
|
campaign.id,
|
||||||
|
campaign.name
|
||||||
|
FROM ad_group
|
||||||
|
WHERE campaign.id = $campaignId
|
||||||
|
AND ad_group.status IN ('ENABLED', 'PAUSED', 'REMOVED')
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$adGroups = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$ag = $row->getAdGroup();
|
||||||
|
$campaign = $row->getCampaign();
|
||||||
|
|
||||||
|
$adGroups[] = [
|
||||||
|
'ad_group_id' => $ag->getId(),
|
||||||
|
'ad_group_name' => $ag->getName(),
|
||||||
|
'status' => AdGroupStatus::name($ag->getStatus()),
|
||||||
|
'type' => AdGroupType::name($ag->getType()),
|
||||||
|
'cpc_bid_micros' => round($ag->getCpcBidMicros() / 1000000, 2),
|
||||||
|
'parent_campaign_id' => $campaign->getId(),
|
||||||
|
'parent_campaign_name' => $campaign->getName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $adGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listAdsByAdGroupId(string $clientCustomerId, string $adGroupId): array
|
||||||
|
{
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
// Query the ad_group_ad resource
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
ad_group_ad.ad.id,
|
||||||
|
ad_group_ad.status,
|
||||||
|
ad_group_ad.ad.type,
|
||||||
|
ad_group_ad.ad.final_urls,
|
||||||
|
ad_group_ad.policy_summary.approval_status,
|
||||||
|
ad_group.id,
|
||||||
|
ad_group.name
|
||||||
|
FROM ad_group_ad
|
||||||
|
WHERE ad_group.id = $adGroupId
|
||||||
|
AND ad_group_ad.status IN ('ENABLED', 'PAUSED', 'REMOVED')
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$ads = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$adGroupAd = $row->getAdGroupAd();
|
||||||
|
$ad = $adGroupAd->getAd();
|
||||||
|
|
||||||
|
$ads[] = [
|
||||||
|
'ad_id' => $ad->getId(),
|
||||||
|
'type' => AdType::name($ad->getType()),
|
||||||
|
'status' => AdGroupAdStatus::name($adGroupAd->getStatus()),
|
||||||
|
'approval_status' => PolicyApprovalStatus::name($adGroupAd->getPolicySummary()->getApprovalStatus()),
|
||||||
|
'final_urls' => iterator_to_array($ad->getFinalUrls()),
|
||||||
|
'ad_group_id' => $row->getAdGroup()->getId()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ads;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listAssetsByCampaignId(string $clientCustomerId, string $campaignId): array
|
||||||
|
{
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
asset.id,
|
||||||
|
asset.name,
|
||||||
|
asset.source,
|
||||||
|
asset.type,
|
||||||
|
asset.policy_summary.approval_status,
|
||||||
|
asset.policy_summary.review_status,
|
||||||
|
campaign_asset.field_type,
|
||||||
|
campaign_asset.status,
|
||||||
|
campaign.id
|
||||||
|
FROM campaign_asset
|
||||||
|
WHERE campaign.id = $campaignId
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$assets = [];
|
||||||
|
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$asset = $row->getAsset();
|
||||||
|
$ca = $row->getCampaignAsset();
|
||||||
|
$policy = $asset->getPolicySummary();
|
||||||
|
|
||||||
|
// 1. Safe access for policy_summary
|
||||||
|
$policy = $asset->getPolicySummary();
|
||||||
|
|
||||||
|
// Check if policy exists before calling methods
|
||||||
|
$approvalStatus = 'NOT_APPLICABLE';
|
||||||
|
$reviewStatus = 'NOT_APPLICABLE';
|
||||||
|
|
||||||
|
if (! is_null($policy)) {
|
||||||
|
$approvalStatus = PolicyApprovalStatus::name($policy->getApprovalStatus());
|
||||||
|
$reviewStatus = PolicyReviewStatus::name($policy->getReviewStatus());
|
||||||
|
}
|
||||||
|
$assets[] = [
|
||||||
|
'id' => $asset->getId(),
|
||||||
|
'name' => $asset->getName(),
|
||||||
|
'added_by' => AssetSource::name($asset->getSource()),
|
||||||
|
'type' => AssetType::name($asset->getType()),
|
||||||
|
'use_case' => AssetFieldType::name($ca->getFieldType()),
|
||||||
|
'approval_status' => $approvalStatus,
|
||||||
|
'review_status' => $reviewStatus,
|
||||||
|
'status' => AssetLinkStatus::name($ca->getStatus()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function listKeywordsByAdGroupId(string $clientCustomerId, string $adGroupId): array
|
||||||
|
{
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
ad_group_criterion.criterion_id,
|
||||||
|
ad_group_criterion.keyword.text,
|
||||||
|
ad_group_criterion.keyword.match_type,
|
||||||
|
ad_group_criterion.status,
|
||||||
|
ad_group_criterion.cpc_bid_micros,
|
||||||
|
ad_group.id
|
||||||
|
FROM ad_group_criterion
|
||||||
|
WHERE ad_group.id = $adGroupId
|
||||||
|
AND ad_group_criterion.type = 'KEYWORD'
|
||||||
|
AND ad_group_criterion.status IN ('ENABLED', 'PAUSED', 'REMOVED')
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$keywords = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$criterion = $row->getAdGroupCriterion();
|
||||||
|
$keywordInfo = $criterion->getKeyword();
|
||||||
|
|
||||||
|
$keywords[] = [
|
||||||
|
'keyword_id' => $criterion->getCriterionId(),
|
||||||
|
'text' => $keywordInfo->getText(),
|
||||||
|
'match_type' => KeywordMatchType::name($keywordInfo->getMatchType()),
|
||||||
|
'status' => AdGroupCriterionStatus::name($criterion->getStatus()),
|
||||||
|
'cpc_bid' => round($criterion->getCpcBidMicros() / 1000000, 2),
|
||||||
|
'ad_group_id' => $row->getAdGroup()->getId()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keywords;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function listAdGroupMetrics(string $dateFrom, string $dateTo): array
|
||||||
|
{
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
ad_group.resource_name,
|
||||||
|
segments.date,
|
||||||
|
metrics.impressions,
|
||||||
|
metrics.clicks,
|
||||||
|
metrics.cost_micros,
|
||||||
|
metrics.conversions,
|
||||||
|
metrics.ctr,
|
||||||
|
metrics.average_cpc
|
||||||
|
FROM ad_group
|
||||||
|
WHERE segments.date BETWEEN '$dateFrom' AND '$dateTo'
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $this->loginCustomerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
|
||||||
|
$metrics = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$ag = $row->getAdGroup();
|
||||||
|
$m = $row->getMetrics();
|
||||||
|
$segments = $row->getSegments();
|
||||||
|
|
||||||
|
$metrics[] = [
|
||||||
|
'ad_group_resource_name' => $ag->getResourceName(),
|
||||||
|
'date' => $segments->getDate(),
|
||||||
|
'impressions' => $m->getImpressions(),
|
||||||
|
'clicks' => $m->getClicks(),
|
||||||
|
'cost_micros' => $m->getCostMicros(),
|
||||||
|
'conversions' => $m->getConversions(),
|
||||||
|
'ctr' => $m->getCtr(),
|
||||||
|
'average_cpc' => $m->getAverageCpc(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordMetricsById(string $clientCustomerId, string $keywordId, string $startDate = '', string $endDate = ''): array
|
||||||
|
{
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
if (empty($startDate)) {
|
||||||
|
$startDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($endDate)) {
|
||||||
|
$endDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
ad_group_criterion.criterion_id,
|
||||||
|
ad_group_criterion.keyword.text,
|
||||||
|
ad_group_criterion.keyword.match_type,
|
||||||
|
metrics.impressions,
|
||||||
|
metrics.clicks,
|
||||||
|
metrics.cost_micros,
|
||||||
|
metrics.conversions,
|
||||||
|
segments.date
|
||||||
|
FROM keyword_view
|
||||||
|
WHERE ad_group_criterion.criterion_id = $keywordId
|
||||||
|
AND segments.date BETWEEN '$startDate' AND '$endDate'
|
||||||
|
ORDER BY segments.date ASC
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$metrics = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$criterion = $row->getAdGroupCriterion();
|
||||||
|
$m = $row->getMetrics();
|
||||||
|
|
||||||
|
$metrics[] = [
|
||||||
|
'keyword_id' => $criterion->getCriterionId(),
|
||||||
|
'text' => $criterion->getKeyword()->getText(),
|
||||||
|
'match_type' => KeywordMatchType::name($criterion->getKeyword()->getMatchType()),
|
||||||
|
'date' => $row->getSegments()->getDate(),
|
||||||
|
'impressions' => $m->getImpressions(),
|
||||||
|
'clicks' => $m->getClicks(),
|
||||||
|
'actual_spend' => round($m->getCostMicros() / 1000000, 2),
|
||||||
|
'conversions' => $m->getConversions(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAdGroupMetricsById(string $clientCustomerId, string $adGroupId, string $startDate = '', string $endDate = ''): array
|
||||||
|
{
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
if (empty($startDate)) {
|
||||||
|
$startDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($endDate)) {
|
||||||
|
$endDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
ad_group.id,
|
||||||
|
ad_group.name,
|
||||||
|
ad_group.status,
|
||||||
|
metrics.impressions,
|
||||||
|
metrics.clicks,
|
||||||
|
metrics.cost_micros,
|
||||||
|
metrics.conversions,
|
||||||
|
metrics.conversions_value,
|
||||||
|
segments.date
|
||||||
|
FROM ad_group
|
||||||
|
WHERE ad_group.id = $adGroupId
|
||||||
|
AND segments.date BETWEEN '$startDate' AND '$endDate'
|
||||||
|
ORDER BY segments.date ASC
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$ag = $row->getAdGroup();
|
||||||
|
$m = $row->getMetrics();
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'ad_group_id' => $ag->getId(),
|
||||||
|
'ad_group_name' => $ag->getName(),
|
||||||
|
'date' => $row->getSegments()->getDate(),
|
||||||
|
'status' => AdGroupStatus::name($ag->getStatus()),
|
||||||
|
'impressions' => $m->getImpressions(),
|
||||||
|
'clicks' => $m->getClicks(),
|
||||||
|
'actual_spend' => round($m->getCostMicros() / 1000000, 2),
|
||||||
|
'conversions' => $m->getConversions(),
|
||||||
|
'conversion_value' => $m->getConversionsValue(),
|
||||||
|
'ctr' => $m->getImpressions() > 0
|
||||||
|
? round(($m->getClicks() / $m->getImpressions()) * 100, 2)
|
||||||
|
: 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAdMetricsById(string $clientCustomerId, string $adId, string $startDate = '', string $endDate = ''): array
|
||||||
|
{
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
if (empty($startDate)) {
|
||||||
|
$startDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($endDate)) {
|
||||||
|
$endDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
ad_group_ad.ad.id,
|
||||||
|
ad_group_ad.ad.type,
|
||||||
|
ad_group_ad.status,
|
||||||
|
ad_group.id,
|
||||||
|
metrics.impressions,
|
||||||
|
metrics.clicks,
|
||||||
|
metrics.cost_micros,
|
||||||
|
metrics.conversions,
|
||||||
|
segments.date
|
||||||
|
FROM ad_group_ad
|
||||||
|
WHERE ad_group_ad.ad.id = $adId
|
||||||
|
AND segments.date BETWEEN '$startDate' AND '$endDate'
|
||||||
|
ORDER BY segments.date ASC
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$adGroupAd = $row->getAdGroupAd();
|
||||||
|
$m = $row->getMetrics();
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'ad_id' => $adGroupAd->getAd()->getId(),
|
||||||
|
'ad_type' => AdType::name($adGroupAd->getAd()->getType()),
|
||||||
|
'date' => $row->getSegments()->getDate(),
|
||||||
|
'status' => AdGroupAdStatus::name($adGroupAd->getStatus()),
|
||||||
|
'impressions' => $m->getImpressions(),
|
||||||
|
'clicks' => $m->getClicks(),
|
||||||
|
'spend' => round($m->getCostMicros() / 1000000, 2),
|
||||||
|
'conversions' => $m->getConversions(),
|
||||||
|
'ad_group_id' => $row->getAdGroup()->getId()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAssetMetricsById(string $clientCustomerId, string $assetId, string $startDate = '', string $endDate = ''): array
|
||||||
|
{
|
||||||
|
$customerId = str_replace('-', '', $clientCustomerId);
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
if (empty($startDate)) {
|
||||||
|
$startDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($endDate)) {
|
||||||
|
$endDate = date('Y-m-d');
|
||||||
|
}
|
||||||
|
$query = <<<QUERY
|
||||||
|
SELECT
|
||||||
|
asset.id,
|
||||||
|
asset.name,
|
||||||
|
campaign_asset.field_type,
|
||||||
|
metrics.impressions,
|
||||||
|
metrics.clicks,
|
||||||
|
metrics.cost_micros,
|
||||||
|
metrics.conversions,
|
||||||
|
segments.date,
|
||||||
|
campaign.id
|
||||||
|
FROM campaign_asset
|
||||||
|
WHERE asset.id = $assetId
|
||||||
|
AND segments.date BETWEEN '$startDate' AND '$endDate'
|
||||||
|
ORDER BY segments.date ASC
|
||||||
|
QUERY;
|
||||||
|
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($response->iterateAllElements() as $row) {
|
||||||
|
$asset = $row->getAsset();
|
||||||
|
$ca = $row->getCampaignAsset();
|
||||||
|
$m = $row->getMetrics();
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'asset_id' => $asset->getId(),
|
||||||
|
'asset_name' => $asset->getName(),
|
||||||
|
'date' => $row->getSegments()->getDate(),
|
||||||
|
'field_type' => AssetFieldType::name($ca->getFieldType()), // e.g., SITELINK, CALLOUT
|
||||||
|
'impressions' => $m->getImpressions(),
|
||||||
|
'clicks' => $m->getClicks(),
|
||||||
|
'cost' => round($m->getCostMicros() / 1000000, 2),
|
||||||
|
'conversions' => $m->getConversions(),
|
||||||
|
'campaign_id' => $row->getCampaign()->getId()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccountDetails(string $customerId): array
|
||||||
|
{
|
||||||
|
$client = $this->buildClient($this->loginCustomerId);
|
||||||
|
$service = $client->getGoogleAdsServiceClient();
|
||||||
|
|
||||||
|
// Querying 'customer' gives you specific settings for that ID
|
||||||
|
$query = "SELECT
|
||||||
|
customer.id,
|
||||||
|
customer.descriptive_name,
|
||||||
|
customer.currency_code,
|
||||||
|
customer.time_zone,
|
||||||
|
customer.tracking_url_template,
|
||||||
|
customer.auto_tagging_enabled,
|
||||||
|
customer.manager,
|
||||||
|
customer.test_account,
|
||||||
|
customer.status
|
||||||
|
FROM customer";
|
||||||
|
|
||||||
|
// When querying the 'customer' resource, the customer_id in the request
|
||||||
|
// must match the ID you are querying.
|
||||||
|
$request = new SearchGoogleAdsRequest([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $service->search($request);
|
||||||
|
|
||||||
|
// Since we are querying a specific ID, we only need the first result
|
||||||
|
$row = $response->iterateAllElements()->current();
|
||||||
|
|
||||||
|
if (! $row) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = $row->getCustomer();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $customer->getId(),
|
||||||
|
'name' => $customer->getDescriptiveName(),
|
||||||
|
'status' => CustomerStatus::name($customer->getStatus()),
|
||||||
|
'currency' => $customer->getCurrencyCode(),
|
||||||
|
'time_zone' => $customer->getTimeZone(),
|
||||||
|
'is_manager' => $customer->getManager(),
|
||||||
|
'is_test_account' => $customer->getTestAccount(),
|
||||||
|
'auto_tagging' => $customer->getAutoTaggingEnabled(),
|
||||||
|
'tracking_template' => $customer->getTrackingUrlTemplate(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
89
app/Services/RoleService.php
Normal file
89
app/Services/RoleService.php
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Enums\UserStatus;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use App\Models\Task;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
// use App\Helpers\GeneralHelper;
|
||||||
|
use App\Models\User;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class RoleService
|
||||||
|
{
|
||||||
|
protected $helper;
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// $this->helper = new GeneralHelper();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllRoles()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$roles = Role::all();
|
||||||
|
return ['success' => true, 'message' => 'Roles fetched successfully', 'data' => $roles];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['success' => false, 'message' => $e->getMessage(), 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createRole($data)
|
||||||
|
{
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
$role = Role::create([
|
||||||
|
'name' => $data->name,
|
||||||
|
'guard_name' => 'web'
|
||||||
|
]);
|
||||||
|
foreach ($data->permissions as $permissions) {
|
||||||
|
foreach ($permissions as $permission) {
|
||||||
|
if ($permission['checked']) {
|
||||||
|
$role->givePermissionTo($permission['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
return ['success' => true, 'message' => 'Role created successfully', 'data' => $role];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
return ['success' => false, 'message' => $e->getMessage(), 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoleById($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return ['success' => true, 'message' => 'User created successfully', 'data' => []];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['success' => false, 'message' => $e->getMessage(), 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateRoleById($id, $data)
|
||||||
|
{
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
$role = Role::findOrFail($id);
|
||||||
|
$role->update([
|
||||||
|
'name' => $data->name,
|
||||||
|
]);
|
||||||
|
foreach ($data->permissions as $permissions) {
|
||||||
|
foreach ($permissions as $permission) {
|
||||||
|
if ($permission['checked']) {
|
||||||
|
$role->givePermissionTo($permission['name']);
|
||||||
|
} else {
|
||||||
|
$role->revokePermissionTo($permission['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::commit();
|
||||||
|
return ['success' => true, 'message' => 'Role updated successfully', 'data' => $role];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
return ['success' => false, 'message' => $e->getMessage(), 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
app/Services/UserHierarchyService.php
Normal file
94
app/Services/UserHierarchyService.php
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class UserHierarchyService
|
||||||
|
{
|
||||||
|
public function visibleUserIds(User $user): array
|
||||||
|
{
|
||||||
|
if ($user->hasRole('Admin')) {
|
||||||
|
return User::query()->pluck('id')->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
$visibleIds = [$user->id];
|
||||||
|
$frontier = [$user->id];
|
||||||
|
|
||||||
|
while ($frontier !== []) {
|
||||||
|
$directReportIds = User::query()
|
||||||
|
->whereIn('manager_id', $frontier)
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$directReportIds = array_values(array_diff($directReportIds, $visibleIds));
|
||||||
|
|
||||||
|
if ($directReportIds === []) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$visibleIds = array_merge($visibleIds, $directReportIds);
|
||||||
|
$frontier = $directReportIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $visibleIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeClientsVisibleTo(Builder $query, User $user): Builder
|
||||||
|
{
|
||||||
|
if ($user->hasRole('Admin')) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$visibleUserIds = $this->visibleUserIds($user);
|
||||||
|
|
||||||
|
return $query->whereHas('assignations', function (Builder $query) use ($visibleUserIds) {
|
||||||
|
$query->whereIn('user_id', $visibleUserIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canViewClient(User $user, Client $client): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('Admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $client->assignations()
|
||||||
|
->whereIn('user_id', $this->visibleUserIds($user))
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wouldCreateCycle(User $user, ?int $managerId): bool
|
||||||
|
{
|
||||||
|
if ($managerId === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->id === $managerId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$frontier = [$user->id];
|
||||||
|
$checkedIds = [$user->id];
|
||||||
|
|
||||||
|
while ($frontier !== []) {
|
||||||
|
$directReportIds = User::query()
|
||||||
|
->whereIn('manager_id', $frontier)
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$directReportIds = array_values(array_diff($directReportIds, $checkedIds));
|
||||||
|
|
||||||
|
if (in_array($managerId, $directReportIds, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkedIds = array_merge($checkedIds, $directReportIds);
|
||||||
|
$frontier = $directReportIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
artisan
Executable file
18
artisan
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
28
bootstrap/app.php
Normal file
28
bootstrap/app.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\HandleAppearance;
|
||||||
|
use App\Http\Middleware\HandleInertiaRequests;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
||||||
|
|
||||||
|
$middleware->web(append: [
|
||||||
|
HandleAppearance::class,
|
||||||
|
HandleInertiaRequests::class,
|
||||||
|
AddLinkHeadersForPreloadedAssets::class,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
//
|
||||||
|
})->create();
|
||||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
6
bootstrap/providers.php
Normal file
6
bootstrap/providers.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\FortifyServiceProvider::class,
|
||||||
|
];
|
||||||
21
components.json
Normal file
21
components.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "resources/css/app.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
105
composer.json
Normal file
105
composer.json
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/react-starter-kit",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"framework"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"googleads/google-ads-php": "^31.1",
|
||||||
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
|
"laravel/fortify": "^1.30",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/tinker": "^2.10.1",
|
||||||
|
"laravel/wayfinder": "^0.1.9",
|
||||||
|
"rap2hpoutre/fast-excel": "^5.7",
|
||||||
|
"smi2/phpclickhouse": "^1.26",
|
||||||
|
"spatie/laravel-permission": "^7.2",
|
||||||
|
"tightenco/ziggy": "^2.6"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/boost": "^1.8",
|
||||||
|
"laravel/pail": "^1.2.2",
|
||||||
|
"laravel/pint": "^1.24",
|
||||||
|
"laravel/sail": "^1.41",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"pestphp/pest": "^4.1",
|
||||||
|
"pestphp/pest-plugin-laravel": "^4.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
|
],
|
||||||
|
"dev:ssr": [
|
||||||
|
"npm run build:ssr",
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr --kill-others"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
|
||||||
|
"@php artisan boost:update --ansi"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
10940
composer.lock
generated
Normal file
10940
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
130
config/app.php
Normal file
130
config/app.php
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => 'Asia/Kuala_Lumpur',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'billing_key' => env('BILLINGBEARERTOKEN', '1|D4sTWrBYsr8aNbNxY4H6cQCRKGoN87zDuUspTtc5fb7776aa'),
|
||||||
|
'billing_url' => env('BILLINGAPIURL', 'http://127.0.0.1:8001/api/v1'),
|
||||||
|
'email_notification' => env('EMAILNOTIFICATION', false),
|
||||||
|
|
||||||
|
];
|
||||||
115
config/auth.php
Normal file
115
config/auth.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the number of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
117
config/cache.php
Normal file
117
config/cache.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "octane",
|
||||||
|
| "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'stores' => [
|
||||||
|
'database',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
|
];
|
||||||
183
config/database.php
Normal file
183
config/database.php
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => 'prefer',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
80
config/filesystems.php
Normal file
80
config/filesystems.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => env('APP_URL').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
157
config/fortify.php
Normal file
157
config/fortify.php
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Guard
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which authentication guard Fortify will use while
|
||||||
|
| authenticating users. This value should correspond with one of your
|
||||||
|
| guards that is already present in your "auth" configuration file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => 'web',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Password Broker
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which password broker Fortify can use when a user
|
||||||
|
| is resetting their password. This configured value should match one
|
||||||
|
| of your password brokers setup in your "auth" configuration file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => 'users',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Username / Email
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value defines which model attribute should be considered as your
|
||||||
|
| application's "username" field. Typically, this might be the email
|
||||||
|
| address of the users but you are free to change this value here.
|
||||||
|
|
|
||||||
|
| Out of the box, Fortify expects forgot password and reset password
|
||||||
|
| requests to have a field named 'email'. If the application uses
|
||||||
|
| another name for the field you may define it below as needed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'username' => 'email',
|
||||||
|
|
||||||
|
'email' => 'email',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Lowercase Usernames
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value defines whether usernames should be lowercased before saving
|
||||||
|
| them in the database, as some database system string fields are case
|
||||||
|
| sensitive. You may disable this for your application if necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lowercase_usernames' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Home Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the path where users will get redirected during
|
||||||
|
| authentication or password reset when the operations are successful
|
||||||
|
| and the user is authenticated. You are free to change this value.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'home' => '/dashboard',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Routes Prefix / Subdomain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which prefix Fortify will assign to all the routes
|
||||||
|
| that it registers with the application. If necessary, you may change
|
||||||
|
| subdomain under which all of the Fortify routes will be available.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => '',
|
||||||
|
|
||||||
|
'domain' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Routes Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which middleware Fortify will assign to the routes
|
||||||
|
| that it registers with the application. If necessary, you may change
|
||||||
|
| these middleware but typically this provided default is preferred.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Rate Limiting
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By default, Fortify will throttle logins to five requests per minute for
|
||||||
|
| every email and IP address combination. However, if you would like to
|
||||||
|
| specify a custom rate limiter to call then you may specify it here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'limiters' => [
|
||||||
|
'login' => 'login',
|
||||||
|
'two-factor' => 'two-factor',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register View Routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify if the routes returning views should be disabled as
|
||||||
|
| you may not need them when building your own application. This may be
|
||||||
|
| especially true if you're writing a custom single-page application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'views' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Features
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some of the Fortify features are optional. You may disable the features
|
||||||
|
| by removing them from this array. You're free to only remove some of
|
||||||
|
| these features, or you can even remove all of these if you need to.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'features' => [
|
||||||
|
Features::registration(),
|
||||||
|
Features::resetPasswords(),
|
||||||
|
Features::emailVerification(),
|
||||||
|
Features::twoFactorAuthentication([
|
||||||
|
'confirm' => true,
|
||||||
|
'confirmPassword' => true,
|
||||||
|
// 'window' => 0
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
6
config/google_ads_php.ini
Normal file
6
config/google_ads_php.ini
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[GOOGLE_ADS]
|
||||||
|
developerToken = "PC1utvNtBsbs5rclB3EWRQ"
|
||||||
|
clientId = "281925192292-jkt1jnv09evm9fh88hbi7637qhj0fhv0.apps.googleusercontent.com"
|
||||||
|
clientSecret = "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"
|
||||||
|
refreshToken = "1//0gZs_jqqrN-rlCgYIARAAGBASNwF-L9Irk8smW7z25CdkofFvf1AiZeSfudbPjh-PkEceQ6w1yxuja7qb2aHwvASyU-FyO5iemI4"
|
||||||
|
loginCustomerId = "3623740547" ; your Manager (MCC) ID
|
||||||
55
config/inertia.php
Normal file
55
config/inertia.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Server Side Rendering
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configures if and how Inertia uses Server Side Rendering
|
||||||
|
| to pre-render each initial request made to your application's pages
|
||||||
|
| so that server rendered HTML is delivered for the user's browser.
|
||||||
|
|
|
||||||
|
| See: https://inertiajs.com/server-side-rendering
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'ssr' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'url' => 'http://127.0.0.1:13714',
|
||||||
|
// 'bundle' => base_path('bootstrap/ssr/ssr.mjs'),
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Testing
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The values described here are used to locate Inertia components on the
|
||||||
|
| filesystem. For instance, when using `assertInertia`, the assertion
|
||||||
|
| attempts to locate the component as a file relative to the paths.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'testing' => [
|
||||||
|
|
||||||
|
'ensure_pages_exist' => true,
|
||||||
|
|
||||||
|
'page_paths' => [
|
||||||
|
resource_path('js/pages'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'page_extensions' => [
|
||||||
|
'js',
|
||||||
|
'jsx',
|
||||||
|
'svelte',
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'vue',
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
132
config/logging.php
Normal file
132
config/logging.php
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
118
config/mail.php
Normal file
118
config/mail.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
202
config/permission.php
Normal file
202
config/permission.php
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'models' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||||
|
* is often just the "Permission" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Permission model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Permission` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permission' => Spatie\Permission\Models\Permission::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||||
|
* is often just the "Role" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Role model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Role` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role' => Spatie\Permission\Models\Role::class,
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
'table_names' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'roles' => 'roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your permissions. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permissions' => 'permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_permissions' => 'model_has_permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models roles. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_roles' => 'model_has_roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role_has_permissions' => 'role_has_permissions',
|
||||||
|
],
|
||||||
|
|
||||||
|
'column_names' => [
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related pivots other than defaults
|
||||||
|
*/
|
||||||
|
'role_pivot_key' => null, // default 'role_id',
|
||||||
|
'permission_pivot_key' => null, // default 'permission_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related model primary key other than
|
||||||
|
* `model_id`.
|
||||||
|
*
|
||||||
|
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||||
|
* that case, name this `model_uuid`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_morph_key' => 'model_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to use the teams feature and your related model's
|
||||||
|
* foreign key is other than `team_id`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'team_foreign_key' => 'team_id',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the method for checking permissions will be registered on the gate.
|
||||||
|
* Set this to false if you want to implement custom logic for checking permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'register_permission_check_method' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||||
|
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||||
|
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||||
|
*/
|
||||||
|
'register_octane_reset_listener' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Events will fire when a role or permission is assigned/unassigned:
|
||||||
|
* \Spatie\Permission\Events\RoleAttached
|
||||||
|
* \Spatie\Permission\Events\RoleDetached
|
||||||
|
* \Spatie\Permission\Events\PermissionAttached
|
||||||
|
* \Spatie\Permission\Events\PermissionDetached
|
||||||
|
*
|
||||||
|
* To enable, set to true, and then create listeners to watch these events.
|
||||||
|
*/
|
||||||
|
'events_enabled' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Teams Feature.
|
||||||
|
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||||
|
* If you want the migrations to register the 'team_foreign_key', you must
|
||||||
|
* set this to true before doing the migration.
|
||||||
|
* If you already did the migration then you must make a new migration to also
|
||||||
|
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||||
|
* (view the latest version of this package's migration file)
|
||||||
|
*/
|
||||||
|
|
||||||
|
'teams' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use to resolve the permissions team id
|
||||||
|
*/
|
||||||
|
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Passport Client Credentials Grant
|
||||||
|
* When set to true the package will use Passports Client to check permissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use_passport_client_credentials' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required permission names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_permission_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required role names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_role_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default wildcard permission lookups are disabled.
|
||||||
|
* See documentation to understand supported syntax.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enable_wildcard_permission' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use for interpreting wildcard permissions.
|
||||||
|
* If you need to modify delimiters, override the class and specify its name here.
|
||||||
|
*/
|
||||||
|
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||||
|
|
||||||
|
/* Cache-specific settings */
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default all permissions are cached for 24 hours to speed up performance.
|
||||||
|
* When permissions or roles are updated the cache is flushed automatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The cache key used to store all permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'key' => 'spatie.permission.cache',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* You may optionally indicate a specific cache driver to use for permission and
|
||||||
|
* role caching using any of the `store` drivers listed in the cache.php config
|
||||||
|
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => 'default',
|
||||||
|
],
|
||||||
|
];
|
||||||
129
config/queue.php
Normal file
129
config/queue.php
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the connection options for every queue backend
|
||||||
|
| used by your application. An example configuration is provided for
|
||||||
|
| each backend supported by Laravel. You're also free to add more.
|
||||||
|
|
|
||||||
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||||
|
| "deferred", "background", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sync' => [
|
||||||
|
'driver' => 'sync',
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||||
|
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||||
|
'queue' => env('DB_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'beanstalkd' => [
|
||||||
|
'driver' => 'beanstalkd',
|
||||||
|
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||||
|
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => 0,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqs' => [
|
||||||
|
'driver' => 'sqs',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||||
|
'queue' => env('SQS_QUEUE', 'default'),
|
||||||
|
'suffix' => env('SQS_SUFFIX'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => null,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'deferred' => [
|
||||||
|
'driver' => 'deferred',
|
||||||
|
],
|
||||||
|
|
||||||
|
'background' => [
|
||||||
|
'driver' => 'background',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'connections' => [
|
||||||
|
'database',
|
||||||
|
'deferred',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Batching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following options configure the database and table that store job
|
||||||
|
| batching information. These options can be updated to any database
|
||||||
|
| connection and table which has been defined by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'batching' => [
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'job_batches',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Failed Queue Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure the behavior of failed queue job logging so you
|
||||||
|
| can control how and where failed jobs are stored. Laravel ships with
|
||||||
|
| support for storing failed jobs in a simple file or in a database.
|
||||||
|
|
|
||||||
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'failed' => [
|
||||||
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'failed_jobs',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
38
config/services.php
Normal file
38
config/services.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'key' => env('RESEND_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'notifications' => [
|
||||||
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
217
config/session.php
Normal file
217
config/session.php
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Lifetime
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the number of minutes that you wish the session
|
||||||
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
|
| to expire immediately when the browser is closed then you may
|
||||||
|
| indicate that via the expire_on_close configuration option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Encryption
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to easily specify that all of your session data
|
||||||
|
| should be encrypted before it's stored. All encryption is performed
|
||||||
|
| automatically by Laravel and you may use the session like normal.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session File Location
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the "file" session driver, the session files are placed
|
||||||
|
| on disk. The default storage location is defined here; however, you
|
||||||
|
| are free to provide another location where they should be stored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" or "redis" session drivers, you may specify a
|
||||||
|
| connection that should be used to manage these sessions. This should
|
||||||
|
| correspond to a connection in your database configuration options.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connection' => env('SESSION_CONNECTION'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" session driver, you may specify the table to
|
||||||
|
| be used to store sessions. Of course, a sensible default is defined
|
||||||
|
| for you; however, you're welcome to change this to another table.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using one of the framework's cache driven session backends, you may
|
||||||
|
| define the cache store which should be used to store the session data
|
||||||
|
| between requests. This must match one of your defined cache stores.
|
||||||
|
|
|
||||||
|
| Affects: "dynamodb", "memcached", "redis"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Sweeping Lottery
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some session drivers must manually sweep their storage location to get
|
||||||
|
| rid of old sessions from storage. Here are the chances that it will
|
||||||
|
| happen on a given request. By default, the odds are 2 out of 100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may change the name of the session cookie that is created by
|
||||||
|
| the framework. Typically, you should not need to change this value
|
||||||
|
| since doing so does not grant a meaningful security improvement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cookie' => env(
|
||||||
|
'SESSION_COOKIE',
|
||||||
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The session cookie path determines the path for which the cookie will
|
||||||
|
| be regarded as available. Typically, this will be the root path of
|
||||||
|
| your application, but you're free to change this when necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the domain and subdomains the session cookie is
|
||||||
|
| available to. By default, the cookie will be available to the root
|
||||||
|
| domain and all subdomains. Typically, this shouldn't be changed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTPS Only Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By setting this option to true, session cookies will only be sent back
|
||||||
|
| to the server if the browser has a HTTPS connection. This will keep
|
||||||
|
| the cookie from being sent to you when it can't be done securely.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTP Access Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
|
| value of the cookie and the cookie will only be accessible through
|
||||||
|
| the HTTP protocol. It's unlikely you should disable this option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Same-Site Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines how your cookies behave when cross-site requests
|
||||||
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
|
| will set this value to "lax" to permit secure cross-site requests.
|
||||||
|
|
|
||||||
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||||
|
|
|
||||||
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Partitioned Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will tie the cookie to the top-level site for
|
||||||
|
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||||
|
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
|
];
|
||||||
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
||||||
37
database/factories/CampaignFactory.php
Normal file
37
database/factories/CampaignFactory.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Campaign;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Industry;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class CampaignFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Campaign::class;
|
||||||
|
|
||||||
|
public function definition()
|
||||||
|
{
|
||||||
|
// Get random consultant (role = consultant) and campaign manager
|
||||||
|
$consultant = User::find(2);
|
||||||
|
$manager = User::find(2);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'client_id' => Client::inRandomOrder()->first()->id,
|
||||||
|
'consultant_id' => 2,
|
||||||
|
'campaign_manager_id' => 2,
|
||||||
|
'campaign_name' => $this->faker->sentence(3),
|
||||||
|
'landing_page' => $this->faker->url,
|
||||||
|
'status' => $this->faker->randomElement(['draft', 'active', 'paused', 'ended']),
|
||||||
|
'conversion' => $this->faker->numberBetween(0, 100),
|
||||||
|
'countdown_enabled' => $this->faker->boolean,
|
||||||
|
'campaign_paused_flag' => $this->faker->boolean,
|
||||||
|
'campaign_paused_reason' => $this->faker->sentence(),
|
||||||
|
'activated_date' => $this->faker->dateTimeBetween('-1 year', 'now'),
|
||||||
|
'end_date' => $this->faker->dateTimeBetween('now', '+6 months'),
|
||||||
|
'countdown_days' => $this->faker->numberBetween(1, 30),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user