feat: Invoice Changes

This commit is contained in:
brian-inspiren 2026-05-22 16:09:17 +08:00
parent 3f319aae4f
commit 6edadb1b04
21 changed files with 506 additions and 125 deletions

View File

@ -92,17 +92,26 @@ public function handle()
// } else { // } else {
$spend = 0; $spend = 0;
// } // }
$managementFee = intval(str_replace(',', '', $row['management_fee'])) ?? 0;
$mediaFee = intval(str_replace(',', '', $row['media_fee'])) ?? 0;
$managementFeeAmount = $managementFee > 0 ? $managementFee / 1.08 : 0;
$mediaFeeAmount = $mediaFee > 0 ? $mediaFee / 1.08 : 0;
$invoice = ClientInvoice::updateOrCreate( $invoice = ClientInvoice::updateOrCreate(
['invoice_no' => $row['invoice_no']], ['invoice_no' => $row['invoice_no']],
[ [
'client_id' => $row['client_id'], 'client_id' => $row['client_id'],
'is_credit_card' => intval(str_replace(',', '',$row['media_fee'])) == 0 ? 1 : 0, 'is_credit_card' => $mediaFee == 0 ? 1 : 0,
'start_date' => $startDate, 'start_date' => $startDate,
'end_date' => $endDate, 'end_date' => $endDate,
'management_fee' => intval(str_replace(',', '',$row['management_fee'])) ?? 0, 'management_fee' => $managementFee,
'media_fee' => intval(str_replace(',', '',$row['media_fee'])) ?? 0, 'management_fee_amount' => $managementFeeAmount,
'management_fee_tax' => $managementFee - $managementFeeAmount,
'media_fee' => $mediaFee,
'media_fee_amount' => $mediaFeeAmount,
'media_fee_tax' => $mediaFee - $mediaFeeAmount,
'tax_percent' => 8, 'tax_percent' => 8,
'nett_amount' => intval(str_replace(',', '',$row['media_fee'])) > 0 ? intval(str_replace(',', '',$row['media_fee'])) / 1.08 : 0, 'nett_amount' => $mediaFeeAmount,
'total_spending' => $spend, 'total_spending' => $spend,
] ]
); );

View File

@ -20,6 +20,7 @@ public function handle()
$mccCustomerId = env('GOOGLE_ADS_LOGIN_CUSTOMER_ID'); // Manager ID without dashes $mccCustomerId = env('GOOGLE_ADS_LOGIN_CUSTOMER_ID'); // Manager ID without dashes
$adsService = new GoogleAdsService(); $adsService = new GoogleAdsService();
$accounts = $adsService->listAccounts(); $accounts = $adsService->listAccounts();
Log::info('Fetched accounts from Google Ads', ['accounts' => $accounts]);
foreach ($accounts as $account) { foreach ($accounts as $account) {
$company = Client::updateOrCreate( $company = Client::updateOrCreate(
['customer_id' => $account['id']], ['customer_id' => $account['id']],

View File

@ -36,7 +36,11 @@ public function pending(): JsonResponse
'end_date', 'end_date',
'payment_no', 'payment_no',
'management_fee', 'management_fee',
'management_fee_amount',
'management_fee_tax',
'media_fee', 'media_fee',
'media_fee_amount',
'media_fee_tax',
'nett_amount', 'nett_amount',
'total_spending', 'total_spending',
'created_at', 'created_at',
@ -72,14 +76,18 @@ public function store(Request $request): JsonResponse
'start_date' => ['nullable', 'date'], 'start_date' => ['nullable', 'date'],
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'], 'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
'management_fee' => ['required', 'numeric', 'min:0'], 'management_fee' => ['required', 'numeric', 'min:0'],
'management_fee_amount' => ['nullable', 'numeric', 'min:0'],
'management_fee_tax' => ['nullable', 'numeric', 'min:0'],
'media_fee' => ['required', 'numeric', 'min:0'], 'media_fee' => ['required', 'numeric', 'min:0'],
'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'], 'media_fee_amount' => ['nullable', 'numeric', 'min:0'],
'media_fee_tax' => ['nullable', 'numeric', 'min:0'],
'total_spending' => ['nullable', 'numeric', 'min:0'], 'total_spending' => ['nullable', 'numeric', 'min:0'],
]); ]);
$mediaFee = $validated['media_fee']; $mediaFee = $validated['media_fee'];
$taxPercent = $validated['tax_percent'] ?? 0;
$nettAmount = $mediaFee - ($mediaFee * ($taxPercent / 100)); $taxPercent = (float) ($validated['tax_percent'] ?? 0);
$nettAmount = $mediaFee / (1 + ($taxPercent / 100));
$sqlAccCode = $this->clientLookupService->normalizeSqlAccCode($validated['sql_acc_code'] ?? null); $sqlAccCode = $this->clientLookupService->normalizeSqlAccCode($validated['sql_acc_code'] ?? null);
$client = ! empty($validated['client_id']) $client = ! empty($validated['client_id'])
? \App\Models\Client::find($validated['client_id']) ? \App\Models\Client::find($validated['client_id'])
@ -111,8 +119,12 @@ public function store(Request $request): JsonResponse
'start_date' => $validated['start_date'] ?? null, 'start_date' => $validated['start_date'] ?? null,
'end_date' => $validated['end_date'] ?? null, 'end_date' => $validated['end_date'] ?? null,
'management_fee' => $validated['management_fee'], 'management_fee' => $validated['management_fee'],
'management_fee_amount' => $validated['management_fee_amount'] ?? null,
'management_fee_tax' => $validated['management_fee_tax'] ?? null,
'media_fee' => $validated['media_fee'], 'media_fee' => $validated['media_fee'],
'tax_percent' => $taxPercent, 'media_fee_amount' => $validated['media_fee_amount'] ?? null,
'media_fee_tax' => $validated['media_fee_tax'] ?? null,
'tax_percent' => null,
'nett_amount' => $nettAmount, 'nett_amount' => $nettAmount,
'total_spending' => $validated['total_spending'] ?? null, 'total_spending' => $validated['total_spending'] ?? null,
]); ]);
@ -221,7 +233,7 @@ private function formatPreviousPayment(array $payment): array
$sqlAccCode = $this->paymentItemSqlAccCode($paymentItem); $sqlAccCode = $this->paymentItemSqlAccCode($paymentItem);
$estimatedTotal = $this->paymentItemEstimatedTotal($paymentItem); $estimatedTotal = $this->paymentItemEstimatedTotal($paymentItem);
$exact_tax = $this->paymentItemTax($paymentItem); $exact_tax = $this->paymentItemTax($paymentItem);
$taxPercent = $this->paymentTaxPercent($paymentItem)/100 + 1; $taxPercent = $this->paymentTaxPercent($paymentItem) / 100 + 1;
$amount = $usesRepeatedPaymentAmount $amount = $usesRepeatedPaymentAmount
? $paymentAmount * ($estimatedTotal / $estimatedItemsTotal) ? $paymentAmount * ($estimatedTotal / $estimatedItemsTotal)
: (float) ($paymentItem['amount'] ?? 0); : (float) ($paymentItem['amount'] ?? 0);
@ -250,10 +262,10 @@ private function formatPreviousPayment(array $payment): array
'status' => $payment['status'] ?? null, 'status' => $payment['status'] ?? null,
'sql_created_at' => $payment['sql_created_at'] ?? null, 'sql_created_at' => $payment['sql_created_at'] ?? null,
'amount' => $payment['amount'] ?? null, 'amount' => $payment['amount'] ?? null,
'media_fee' => $totals['media_fee']/1.08, 'media_fee' => $totals['media_fee'] / 1.08,
'management_fee' => $totals['management_fee']/1.08, 'management_fee' => $totals['management_fee'] / 1.08,
'invoice_media_fee' => $totals['invoice_media_fee']/1.08, 'invoice_media_fee' => $totals['invoice_media_fee'] / 1.08,
'invoice_management_fee' => $totals['invoice_management_fee']/1.08, 'invoice_management_fee' => $totals['invoice_management_fee'] / 1.08,
'invoice_number' => data_get($payment, 'invoice.invoice_number'), 'invoice_number' => data_get($payment, 'invoice.invoice_number'),
]; ];
} }

View File

@ -86,7 +86,11 @@ public function store(Request $request)
'start_date' => ['nullable', 'date'], 'start_date' => ['nullable', 'date'],
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'], 'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
'management_fee' => ['required', 'numeric', 'min:0'], 'management_fee' => ['required', 'numeric', 'min:0'],
'management_fee_amount' => ['nullable', 'numeric', 'min:0'],
'management_fee_tax' => ['nullable', 'numeric', 'min:0'],
'media_fee' => ['required', 'numeric', 'min:0'], 'media_fee' => ['required', 'numeric', 'min:0'],
'media_fee_amount' => ['nullable', 'numeric', 'min:0'],
'media_fee_tax' => ['nullable', 'numeric', 'min:0'],
'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'], 'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
'total_spending' => ['nullable', 'numeric', 'min:0'], 'total_spending' => ['nullable', 'numeric', 'min:0'],
]); ]);
@ -95,8 +99,8 @@ public function store(Request $request)
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403); abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
$mediaFee = $validated['media_fee']; $mediaFee = $validated['media_fee'];
$taxPercent = $validated['tax_percent'] ?? 0; $taxPercent = (float) ($validated['tax_percent'] ?? 0);
$nettAmount = $mediaFee - ($mediaFee * ($taxPercent / 100)); $nettAmount = $mediaFee / (1 + ($taxPercent / 100));
$invoice = ClientInvoice::create([ $invoice = ClientInvoice::create([
'client_id' => $validated['client_id'], 'client_id' => $validated['client_id'],
@ -109,7 +113,11 @@ public function store(Request $request)
'start_date' => $validated['start_date'] ?? null, 'start_date' => $validated['start_date'] ?? null,
'end_date' => $validated['end_date'] ?? null, 'end_date' => $validated['end_date'] ?? null,
'management_fee' => $validated['management_fee'], 'management_fee' => $validated['management_fee'],
'management_fee_amount' => $validated['management_fee_amount'] ?? null,
'management_fee_tax' => $validated['management_fee_tax'] ?? null,
'media_fee' => $validated['media_fee'], 'media_fee' => $validated['media_fee'],
'media_fee_amount' => $validated['media_fee_amount'] ?? null,
'media_fee_tax' => $validated['media_fee_tax'] ?? null,
'tax_percent' => $taxPercent, 'tax_percent' => $taxPercent,
'nett_amount' => $nettAmount, 'nett_amount' => $nettAmount,
'total_spending' => $validated['total_spending'] ?? null, 'total_spending' => $validated['total_spending'] ?? null,
@ -145,15 +153,20 @@ public function update(Request $request, ClientInvoice $invoice)
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'], 'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
'amount' => ['required', 'numeric', 'min:0'], 'amount' => ['required', 'numeric', 'min:0'],
'management_fee' => ['required', 'numeric', 'min:0'], 'management_fee' => ['required', 'numeric', 'min:0'],
'management_fee_amount' => ['nullable', 'numeric', 'min:0'],
'management_fee_tax' => ['nullable', 'numeric', 'min:0'],
'media_fee' => ['required', 'numeric', 'min:0'], 'media_fee' => ['required', 'numeric', 'min:0'],
'media_fee_amount' => ['nullable', 'numeric', 'min:0'],
'media_fee_tax' => ['nullable', 'numeric', 'min:0'],
'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'], 'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
'nett_amount' => ['nullable', 'numeric', 'min:0'],
'total_spending' => ['nullable', 'numeric', 'min:0'], 'total_spending' => ['nullable', 'numeric', 'min:0'],
]); ]);
$managementFee = $validated['management_fee']; $managementFee = $validated['management_fee'];
$mediaFee = $validated['media_fee']; $mediaFee = $validated['media_fee'];
$taxPercent = (float) ($validated['tax_percent'] ?? 0); $taxPercent = (float) ($validated['tax_percent'] ?? 0);
$nettAmount = $mediaFee / (1 + ($taxPercent / 100)); $nettAmount = $validated['nett_amount'] ?? ($mediaFee / (1 + ($taxPercent / 100)));
$invoice->update([ $invoice->update([
'invoice_no' => $validated['invoice_no'], 'invoice_no' => $validated['invoice_no'],
@ -171,6 +184,10 @@ public function update(Request $request, ClientInvoice $invoice)
'total_spending' => $validated['total_spending'] ?? null, 'total_spending' => $validated['total_spending'] ?? null,
]); ]);
if(empty($invoice->approved_at)) {
$this->approvalService->approve($invoice);
}
return redirect() return redirect()
->route('google-ads.accounts.show', ['id' => $invoice->client->customer_id]) ->route('google-ads.accounts.show', ['id' => $invoice->client->customer_id])
->with('message-info', 'Invoice updated successfully.'); ->with('message-info', 'Invoice updated successfully.');
@ -315,10 +332,6 @@ public function storeClient(Request $request, ClientInvoice $invoice)
$selectedClientIsLinked = Client::query() $selectedClientIsLinked = Client::query()
->where('id', $validated['client_id']) ->where('id', $validated['client_id'])
->whereHas('customers', function ($query) {
$query->whereNotNull('sql_acc_code')
->where('sql_acc_code', '!=', '');
})
->exists(); ->exists();
if ($selectedClientIsLinked) { if ($selectedClientIsLinked) {

View File

@ -250,7 +250,7 @@ private function hydrateClient(array $account): array
'time_zone' => $account['time_zone'], 'time_zone' => $account['time_zone'],
] ]
); );
// dd($localClient);
$localClient->load(['assignations.user', 'invoices']); $localClient->load(['assignations.user', 'invoices']);
$assignments = $localClient->assignations $assignments = $localClient->assignations
@ -327,7 +327,11 @@ private function hydrateClient(array $account): array
'amount' => $invoice->amount, 'amount' => $invoice->amount,
'total_spend' => number_format($totalInvoiceSpend, 2, '.', ''), 'total_spend' => number_format($totalInvoiceSpend, 2, '.', ''),
'management_fee' => $invoice->management_fee, 'management_fee' => $invoice->management_fee,
'management_fee_amount' => $invoice->management_fee_amount,
'management_fee_tax' => $invoice->management_fee_tax,
'media_fee' => $invoice->media_fee, 'media_fee' => $invoice->media_fee,
'media_fee_amount' => $invoice->media_fee_amount,
'media_fee_tax' => $invoice->media_fee_tax,
'tax_percent' => $invoice->tax_percent, 'tax_percent' => $invoice->tax_percent,
'nett_amount' => $invoice->nett_amount, 'nett_amount' => $invoice->nett_amount,
'total_spending' => $invoice->total_spending, 'total_spending' => $invoice->total_spending,
@ -467,17 +471,26 @@ public function insertCSVDataToDB()
} }
} }
} }
$managementFee = intval($row['management_fee']);
$mediaFee = intval($row['media_fee']);
$managementFeeAmount = $managementFee > 0 ? $managementFee / 1.08 : 0;
$mediaFeeAmount = $mediaFee > 0 ? $mediaFee / 1.08 : 0;
ClientInvoice::updateOrCreate( ClientInvoice::updateOrCreate(
['invoice_no' => $row['invoice_no']], ['invoice_no' => $row['invoice_no']],
[ [
'client_id' => $row['client_id'], 'client_id' => $row['client_id'],
'is_credit_card' => intval($row['media_fee']) == 0 ? 1 : 0, 'is_credit_card' => $mediaFee == 0 ? 1 : 0,
'start_date' => $startDate, 'start_date' => $startDate,
'end_date' => $endDate, 'end_date' => $endDate,
'management_fee' => intval($row['management_fee']), 'management_fee' => $managementFee,
'media_fee' => intval($row['media_fee']), 'management_fee_amount' => $managementFeeAmount,
'management_fee_tax' => $managementFee - $managementFeeAmount,
'media_fee' => $mediaFee,
'media_fee_amount' => $mediaFeeAmount,
'media_fee_tax' => $mediaFee - $mediaFeeAmount,
'tax_percent' => 8, 'tax_percent' => 8,
'nett_amount' => intval($row['media_fee']) > 0 ? intval($row['media_fee']) / 1.08 : 0, 'nett_amount' => $mediaFeeAmount,
'total_spending' => $spend, 'total_spending' => $spend,
] ]
); );

View File

@ -64,21 +64,28 @@ public function edit(int $id): Response
{ {
$role = Role::findOrFail($id); $role = Role::findOrFail($id);
// 1. Get all permissions with their "checked" state $permissions = Permission::query()
$permissions = Permission::all()->map(function ($permission) use ($role) { ->orderBy('group')
->orderBy('name')
->get()
->map(function ($permission) use ($role) {
$group = $permission->group
?: $permission->group_name
?: str($permission->name)->before('.')->headline()->toString();
return [ return [
'id' => $permission->id, 'id' => $permission->id,
'name' => $permission->name, // e.g. "user.create" 'name' => $permission->name,
'group' => $group,
'group_name' => $permission->group_name,
'description' => $permission->description, 'description' => $permission->description,
'checked' => $role->hasPermissionTo($permission->name), 'checked' => $role->hasPermissionTo($permission->name),
]; ];
}); });
// 2. Group them by the prefix (the part before the dot)
$grouped = $permissions->groupBy(function ($item) { $grouped = $permissions->groupBy(function ($item) {
return explode('.', $item['name'])[0]; return $item['group'];
})->map(function ($group) { })->map(function ($group) {
// 3. Force it to be a sequential array so JS sees it as []
return $group->values()->toArray(); return $group->values()->toArray();
}); });

View File

@ -26,7 +26,11 @@ class ClientInvoice extends Model
'amount', 'amount',
'tax_percent', 'tax_percent',
'media_fee', 'media_fee',
'media_fee_amount',
'media_fee_tax',
'management_fee', 'management_fee',
'management_fee_amount',
'management_fee_tax',
'nett_amount', 'nett_amount',
'total_spending', 'total_spending',
]; ];
@ -40,7 +44,11 @@ class ClientInvoice extends Model
'amount' => 'decimal:2', 'amount' => 'decimal:2',
'tax_percent' => 'decimal:2', 'tax_percent' => 'decimal:2',
'media_fee' => 'decimal:2', 'media_fee' => 'decimal:2',
'media_fee_amount' => 'decimal:2',
'media_fee_tax' => 'decimal:2',
'management_fee' => 'decimal:2', 'management_fee' => 'decimal:2',
'management_fee_amount' => 'decimal:2',
'management_fee_tax' => 'decimal:2',
'nett_amount' => 'decimal:2', 'nett_amount' => 'decimal:2',
'total_spending' => 'decimal:2', 'total_spending' => 'decimal:2',
]; ];

View File

@ -108,7 +108,6 @@ public function listCampaigns(string $clientCustomerId): array
{ {
$client = $this->buildClient($this->loginCustomerId); $client = $this->buildClient($this->loginCustomerId);
$service = $client->getGoogleAdsServiceClient(); $service = $client->getGoogleAdsServiceClient();
$customerId = str_replace('-', '', $clientCustomerId); $customerId = str_replace('-', '', $clientCustomerId);
$query = <<<QUERY $query = <<<QUERY
@ -136,6 +135,7 @@ public function listCampaigns(string $clientCustomerId): array
]); ]);
$response = $service->search($request); $response = $service->search($request);
$campaigns = []; $campaigns = [];
foreach ($response->iterateAllElements() as $row) { foreach ($response->iterateAllElements() as $row) {
$c = $row->getCampaign(); $c = $row->getCampaign();

View File

@ -6,6 +6,8 @@
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets; use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
use Spatie\Permission\Middleware\PermissionMiddleware;
use Spatie\Permission\Middleware\RoleMiddleware;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -15,6 +17,11 @@
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'permission' => PermissionMiddleware::class,
'role' => RoleMiddleware::class,
]);
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']); $middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
$middleware->web(append: [ $middleware->web(append: [

View File

@ -0,0 +1,68 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! Schema::hasColumn('client_invoices', 'management_fee_amount')) {
Schema::table('client_invoices', function (Blueprint $table) {
$table->decimal('management_fee_amount', 10, 2)->nullable()->after('management_fee');
});
}
if (! Schema::hasColumn('client_invoices', 'management_fee_tax')) {
Schema::table('client_invoices', function (Blueprint $table) {
$table->decimal('management_fee_tax', 10, 2)->nullable()->after('management_fee_amount');
});
}
if (! Schema::hasColumn('client_invoices', 'media_fee_amount')) {
Schema::table('client_invoices', function (Blueprint $table) {
$table->decimal('media_fee_amount', 10, 2)->nullable()->after('media_fee');
});
}
if (! Schema::hasColumn('client_invoices', 'media_fee_tax')) {
Schema::table('client_invoices', function (Blueprint $table) {
$table->decimal('media_fee_tax', 10, 2)->nullable()->after('media_fee_amount');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasColumn('client_invoices', 'media_fee_tax')) {
Schema::table('client_invoices', function (Blueprint $table) {
$table->dropColumn('media_fee_tax');
});
}
if (Schema::hasColumn('client_invoices', 'media_fee_amount')) {
Schema::table('client_invoices', function (Blueprint $table) {
$table->dropColumn('media_fee_amount');
});
}
if (Schema::hasColumn('client_invoices', 'management_fee_tax')) {
Schema::table('client_invoices', function (Blueprint $table) {
$table->dropColumn('management_fee_tax');
});
}
if (Schema::hasColumn('client_invoices', 'management_fee_amount')) {
Schema::table('client_invoices', function (Blueprint $table) {
$table->dropColumn('management_fee_amount');
});
}
}
};

View File

@ -15,7 +15,9 @@ public function run(): void
{ {
// User::factory(10)->create(); // User::factory(10)->create();
User::firstOrCreate( $this->call(RoleSeeder::class);
$user = User::firstOrCreate(
['email' => 'test@example.com'], ['email' => 'test@example.com'],
[ [
'name' => 'Test User', 'name' => 'Test User',
@ -23,5 +25,7 @@ public function run(): void
'email_verified_at' => now(), 'email_verified_at' => now(),
] ]
); );
$user->assignRole('Admin');
} }
} }

View File

@ -2,12 +2,10 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\User;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash; use Spatie\Permission\PermissionRegistrar;
class RoleSeeder extends Seeder class RoleSeeder extends Seeder
{ {
@ -16,8 +14,51 @@ class RoleSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
$adminRole = Role::create(['name' => 'Admin','guard_name' => 'web']); app(PermissionRegistrar::class)->forgetCachedPermissions();
$salesRole = Role::create(['name' => 'Sales' ,'guard_name' => 'web']);
$socialMedia = Role::create(['name' => 'Social Media Specialist','guard_name' => 'web']); $permissions = [
['name' => 'dashboard.view', 'group' => 'Dashboard', 'description' => 'View dashboard'],
['name' => 'google-ads.accounts.view', 'group' => 'Google Ads Accounts', 'description' => 'View Google Ads accounts'],
['name' => 'google-ads.accounts.sync', 'group' => 'Google Ads Accounts', 'description' => 'Sync Google account records'],
['name' => 'google-ads.accounts.update', 'group' => 'Google Ads Accounts', 'description' => 'Update Google Ads account assignment details'],
['name' => 'google-ads.activities.create', 'group' => 'Google Ads Activities', 'description' => 'Create account activities'],
['name' => 'google-ads.activities.update', 'group' => 'Google Ads Activities', 'description' => 'Update account activities'],
['name' => 'google-ads.activities.complete', 'group' => 'Google Ads Activities', 'description' => 'Mark account activities as complete'],
['name' => 'google-ads.activities.delete', 'group' => 'Google Ads Activities', 'description' => 'Delete account activities'],
['name' => 'google-ads.import', 'group' => 'Google Ads Accounts', 'description' => 'Import Google Ads data'],
['name' => 'google.reports.view', 'group' => 'Google Reports', 'description' => 'View Google campaign reports'],
['name' => 'client-invoices.create', 'group' => 'Client Invoices', 'description' => 'Create client invoices'],
['name' => 'client-invoices.create-client', 'group' => 'Client Invoices', 'description' => 'Create clients from pending invoices'],
['name' => 'client-invoices.update', 'group' => 'Client Invoices', 'description' => 'Update client invoices'],
['name' => 'client-invoices.approve', 'group' => 'Client Invoices', 'description' => 'Approve client invoices'],
['name' => 'client-invoices.delete', 'group' => 'Client Invoices', 'description' => 'Delete client invoices'],
['name' => 'client-invoices.view-pdf', 'group' => 'Client Invoices', 'description' => 'View invoice PDF'],
['name' => 'clients.adjustments.create', 'group' => 'Client Adjustments', 'description' => 'Create client invoice adjustments'],
['name' => 'clients.adjustments.delete', 'group' => 'Client Adjustments', 'description' => 'Delete client invoice adjustments'],
['name' => 'management.roles.view', 'group' => 'Role Management', 'description' => 'View roles'],
['name' => 'management.roles.update', 'group' => 'Role Management', 'description' => 'Update role permissions'],
['name' => 'management.users.view', 'group' => 'User Management', 'description' => 'View users'],
['name' => 'management.users.create', 'group' => 'User Management', 'description' => 'Create users'],
['name' => 'management.users.update', 'group' => 'User Management', 'description' => 'Update users'],
];
foreach ($permissions as $permission) {
Permission::query()->updateOrCreate(
['name' => $permission['name'], 'guard_name' => 'web'],
[
'group' => $permission['group'],
'group_name' => $permission['group'],
'description' => $permission['description'],
],
);
}
$adminRole = Role::query()->firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);
Role::query()->firstOrCreate(['name' => 'Sales', 'guard_name' => 'web']);
Role::query()->firstOrCreate(['name' => 'Social Media Specialist', 'guard_name' => 'web']);
$adminRole->syncPermissions(Permission::query()->pluck('name')->all());
app(PermissionRegistrar::class)->forgetCachedPermissions();
} }
} }

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { InertiaFormProps } from "@inertiajs/react"; import { InertiaFormProps } from "@inertiajs/react";
import { route } from "ziggy-js"; import { route } from "ziggy-js";
import axios from "axios"; import axios from "axios";
import { Button, Stack, TextInput, NumberInput, Loader, Select, Switch, Text } from "@mantine/core"; import { Button, Group, Stack, TextInput, NumberInput, Loader, Select, Switch, Text } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DateInput } from "@mantine/dates";
import { IconDeviceFloppy } from "@tabler/icons-react"; import { IconDeviceFloppy } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -17,11 +17,16 @@ export interface InvoiceFormValues {
end_date: string; end_date: string;
amount: string; amount: string;
management_fee: string; management_fee: string;
management_fee_amount?: string;
management_fee_tax?: string;
media_fee: string; media_fee: string;
media_fee_amount?: string;
media_fee_tax?: string;
tax_percent: string; tax_percent: string;
total_spending: string; total_spending: string;
client_id?: string; client_id?: string;
customer_id?: string; customer_id?: string;
nett_amount: string;
} }
interface InvoiceOption { interface InvoiceOption {
@ -45,13 +50,30 @@ export default function InvoiceForm({
const formatDate = (value: string) => (value ? dayjs(value).toDate() : null); const formatDate = (value: string) => (value ? dayjs(value).toDate() : null);
const [fetchingSpend, setFetchingSpend] = useState(false); const [fetchingSpend, setFetchingSpend] = useState(false);
const parseAmount = (value?: string) => {
const parsed = Number.parseFloat(value ?? "");
return Number.isFinite(parsed) ? parsed : 0;
};
const formatAmount = (value: number) => value.toFixed(2);
const calculateNettAmount = () => {
const mediaFeeAmount = form.data.media_fee;
const calculated = formatAmount(
parseAmount(mediaFeeAmount) / (1 + parseAmount(form.data.tax_percent) / 100)
);
form.setData("nett_amount", calculated);
};
useEffect(() => { useEffect(() => {
if (!form.data.is_credit_card) return; if (!form.data.is_credit_card) return;
// Credit-card invoices can be fee-free; normalize fields to 0 for convenience. // Credit-card invoices can be fee-free; normalize fields to 0 for convenience.
if (form.data.management_fee !== "0") form.setData("management_fee", "0"); // if (form.data.management_fee !== "0") form.setData("management_fee", "0");
if (form.data.media_fee !== "0") form.setData("media_fee", "0"); if (form.data.media_fee !== "0") form.setData("media_fee", "0");
if (form.data.tax_percent !== "0") form.setData("tax_percent", "0"); // if (form.data.tax_percent !== "0") form.setData("tax_percent", "0");
}, [form.data.is_credit_card]); }, [form.data.is_credit_card]);
useEffect(() => { useEffect(() => {
@ -183,6 +205,21 @@ export default function InvoiceForm({
required required
/> />
<Group align="flex-end">
<NumberInput
precision={2}
label="Media Nett Amount (RM)"
value={form.data.nett_amount ? Number(form.data.nett_amount) : undefined}
onChange={(value) => form.setData("nett_amount", value?.toString() ?? "")}
error={form.errors.nett_amount}
required
style={{ flex: 1 }}
/>
<Button type="button" variant="outline" onClick={calculateNettAmount}>
Calculate
</Button>
</Group>
<NumberInput <NumberInput
precision={2} precision={2}
label="Total Spending (RM)" label="Total Spending (RM)"
@ -192,17 +229,20 @@ export default function InvoiceForm({
/> />
<Text>Additional Info</Text> <Text>Additional Info</Text>
<Group spacing="sm">
<Switch <Switch
label="Credit card" aria-label="Credit card"
checked={form.data.is_credit_card} checked={form.data.is_credit_card}
onChange={(event) => form.setData("is_credit_card", event.currentTarget.checked)} onChange={(event) => form.setData("is_credit_card", event.currentTarget.checked)}
/> />
<Text>Credit card</Text>
</Group>
<Switch {/* <Switch
label="Paid" label="Paid"
checked={form.data.is_paid} checked={form.data.is_paid}
onChange={(event) => form.setData("is_paid", event.currentTarget.checked)} onChange={(event) => form.setData("is_paid", event.currentTarget.checked)}
/> /> */}
<Button <Button
type="submit" type="submit"

View File

@ -62,7 +62,7 @@ export default function UserForm({ role, status, permissions }: Props) {
return ( return (
<Stack key={group} spacing="xs"> <Stack key={group} spacing="xs">
<Checkbox <Checkbox
label={<Text fw={600} sx={{ textTransform: 'capitalize' }}>{group}</Text>} label={<Text fw={600}>{group}</Text>}
checked={allChecked} checked={allChecked}
indeterminate={someChecked && !allChecked} indeterminate={someChecked && !allChecked}
onChange={(e) => handleGroupCheckboxChange(group, e.target.checked)} onChange={(e) => handleGroupCheckboxChange(group, e.target.checked)}

View File

@ -154,29 +154,47 @@ function AppNotifications() {
accessorFn: (invoice) => invoice.pending_sql_acc_code ?? '-', accessorFn: (invoice) => invoice.pending_sql_acc_code ?? '-',
Cell: ({ cell }) => <Text size="sm">{cell.getValue<string>()}</Text>, Cell: ({ cell }) => <Text size="sm">{cell.getValue<string>()}</Text>,
}, },
{ // {
id: 'invoice_management_fee', // id: 'invoice_management_fee',
header: 'Invoice Management Fee', // header: 'Invoice Management Fee',
accessorFn: (invoice) => invoice.invoice_billing_totals?.management_fee ?? 0, // accessorFn: (invoice) => invoice.invoice_billing_totals?.management_fee ?? 0,
Cell: ({ row }) => ( // Cell: ({ row }) => (
<Text size="sm" weight={600} align="right"> // <Text size="sm" weight={600} align="right">
{formatAmount(row.original.invoice_billing_totals?.management_fee ?? 0)} // {formatAmount(row.original.invoice_billing_totals?.management_fee ?? 0)}
</Text> // </Text>
), // ),
}, // },
{ // {
id: 'invoice_media_fee', // id: 'invoice_media_fee',
header: 'Invoice Media Fee', // header: 'Invoice Media Fee',
accessorFn: (invoice) => invoice.invoice_billing_totals?.media_fee ?? 0, // accessorFn: (invoice) => invoice.invoice_billing_totals?.media_fee ?? 0,
Cell: ({ row }) => ( // Cell: ({ row }) => (
<Text size="sm" weight={600} align="right"> // <Text size="sm" weight={600} align="right">
{formatAmount(row.original.invoice_billing_totals?.media_fee ?? 0)} // {formatAmount(row.original.invoice_billing_totals?.media_fee ?? 0)}
</Text> // </Text>
), // ),
}, // },
{ {
accessorKey: 'management_fee', accessorKey: 'management_fee',
header: 'Payment Management Fee', header: 'Payment Management Fee (incl. tax)',
Cell: ({ row }) => (
<Text size="sm" weight={600} align="right">
{formatAmount(row.original.management_fee_amount)}
</Text>
),
},
{
accessorKey: 'management_fee_tax',
header: 'Payment Management Tax',
Cell: ({ row }) => (
<Text size="sm" weight={600} align="right">
{formatAmount(row.original.management_fee_tax)}
</Text>
),
},
{
accessorKey: 'management_fee_nett',
header: 'Payment Management Nett',
Cell: ({ row }) => ( Cell: ({ row }) => (
<Text size="sm" weight={600} align="right"> <Text size="sm" weight={600} align="right">
{formatAmount(row.original.management_fee)} {formatAmount(row.original.management_fee)}
@ -185,7 +203,25 @@ function AppNotifications() {
}, },
{ {
accessorKey: 'media_fee', accessorKey: 'media_fee',
header: 'Payment Media Fee', header: 'Payment Media Fee (incl. tax)',
Cell: ({ row }) => (
<Text size="sm" weight={600} align="right">
{formatAmount(row.original.media_fee_amount)}
</Text>
),
},
{
accessorKey: 'media_fee_tax',
header: 'Payment Media Tax',
Cell: ({ row }) => (
<Text size="sm" weight={600} align="right">
{formatAmount(row.original.media_fee_tax)}
</Text>
),
},
{
accessorKey: 'media_fee_nett',
header: 'Payment Media Nett',
Cell: ({ row }) => ( Cell: ({ row }) => (
<Text size="sm" weight={600} align="right"> <Text size="sm" weight={600} align="right">
{formatAmount(row.original.media_fee)} {formatAmount(row.original.media_fee)}
@ -617,7 +653,7 @@ export default function AppLayout({ children }: Props) {
</Group> </Group>
<Group spacing="sm"> <Group spacing="sm">
<AppNotifications /> {/* <AppNotifications /> */}
<ThemeToggle /> <ThemeToggle />
<Menu shadow="md" width={220}> <Menu shadow="md" width={220}>
<Menu.Target> <Menu.Target>

View File

@ -9,6 +9,7 @@ import {
ActionIcon, ActionIcon,
Stack, Stack,
Text, Text,
Tabs,
} from '@mantine/core'; } from '@mantine/core';
import { IconEye, IconRefresh } from '@tabler/icons-react'; import { IconEye, IconRefresh } from '@tabler/icons-react';
import { MantineReactTable } from 'mantine-react-table'; import { MantineReactTable } from 'mantine-react-table';
@ -35,13 +36,60 @@ const parseAmount = (value?: number | string | null): number => {
return Number.isNaN(normalized) ? 0 : normalized; return Number.isNaN(normalized) ? 0 : normalized;
}; };
const statusOrder = ['ENABLED', 'PAUSED', 'REMOVED', 'ENDED', 'CANCELED'];
const getStatusColor = (status: string) => {
if (status === 'ENABLED') {
return 'green';
}
if (status === 'PAUSED') {
return 'yellow';
}
if (status === 'ENDED' || status === 'CANCELED' || status === 'REMOVED') {
return 'red';
}
return 'gray';
};
export default function TicketDetails({ export default function TicketDetails({
clients, clients,
googleCompanySyncRunning, googleCompanySyncRunning,
}: Props) { }: Props) {
const [syncing, setSyncing] = React.useState(false); const [syncing, setSyncing] = React.useState(false);
// console.log(clients); const [activeStatus, setActiveStatus] = React.useState<string | null>('all');
const campaignsData = clients ?? []; const campaignsData = clients ?? [];
const statusTabs = useMemo(() => {
const counts = campaignsData.reduce<Record<string, number>>((acc, client) => {
const status = client.status || 'UNKNOWN';
acc[status] = (acc[status] ?? 0) + 1;
return acc;
}, {});
return Object.entries(counts).sort(([a], [b]) => {
const aIndex = statusOrder.indexOf(a);
const bIndex = statusOrder.indexOf(b);
if (aIndex !== -1 || bIndex !== -1) {
return (aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex)
- (bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex);
}
return a.localeCompare(b);
});
}, [campaignsData]);
const filteredClients = useMemo(() => {
if (!activeStatus || activeStatus === 'all') {
return campaignsData;
}
return campaignsData.filter((client) => (client.status || 'UNKNOWN') === activeStatus);
}, [activeStatus, campaignsData]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
@ -61,15 +109,7 @@ export default function TicketDetails({
header: 'Status', header: 'Status',
Cell: ({ cell }: any) => { Cell: ({ cell }: any) => {
const value = cell.getValue() as string; const value = cell.getValue() as string;
const color = return <Badge color={getStatusColor(value)}>{value}</Badge>;
value === 'ENABLED'
? 'green'
: value === 'PAUSED'
? 'yellow'
: value === 'ENDED'
? 'red'
: 'gray';
return <Badge color={color}>{value}</Badge>;
}, },
}, },
{ {
@ -142,10 +182,29 @@ export default function TicketDetails({
</Group> </Group>
<MantineReactTable <MantineReactTable
columns={columns} columns={columns}
data={clients} data={filteredClients}
enableRowActions // ✅ REQUIRED enableRowActions // ✅ REQUIRED
positionActionsColumn="last" // optional but recommended positionActionsColumn="last" // optional but recommended
renderRowActions={renderRowActions} renderRowActions={renderRowActions}
renderTopToolbarCustomActions={() => (
<Tabs value={activeStatus} onTabChange={setActiveStatus}>
<Tabs.List>
<Tabs.Tab value="all">All ({campaignsData.length})</Tabs.Tab>
{statusTabs.map(([status, count]) => (
<Tabs.Tab key={status} value={status}>
<Group spacing={6}>
<Badge color={getStatusColor(status)} variant="dot">
{status}
</Badge>
<Text size="sm" color="dimmed">
{count}
</Text>
</Group>
</Tabs.Tab>
))}
</Tabs.List>
</Tabs>
)}
/> />
</Container> </Container>
</AppLayout> </AppLayout>

View File

@ -30,13 +30,24 @@ export default function Page({ clientId, customerId, availableInvoices }: Props)
end_date: "", end_date: "",
amount: "", amount: "",
management_fee: "", management_fee: "",
management_fee_amount: "",
management_fee_tax: "",
media_fee: "", media_fee: "",
media_fee_amount: "",
media_fee_tax: "",
tax_percent: "", tax_percent: "",
total_spending: "", total_spending: "",
nett_amount: "",
}); });
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (!form.data.nett_amount) {
form.setError("nett_amount", "Media Nett Amount is required.");
return;
}
const totalSpending = form.data.total_spending const totalSpending = form.data.total_spending
? parseFloat(form.data.total_spending) ? parseFloat(form.data.total_spending)
: null; : null;
@ -51,9 +62,14 @@ export default function Page({ clientId, customerId, availableInvoices }: Props)
end_date: data.end_date || null, end_date: data.end_date || null,
amount: parseFloat(data.amount) || 0, amount: parseFloat(data.amount) || 0,
management_fee: parseFloat(data.management_fee) || 0, management_fee: parseFloat(data.management_fee) || 0,
management_fee_amount: data.management_fee_amount ? parseFloat(data.management_fee_amount) : null,
management_fee_tax: data.management_fee_tax ? parseFloat(data.management_fee_tax) : null,
media_fee: parseFloat(data.media_fee) || 0, media_fee: parseFloat(data.media_fee) || 0,
media_fee_amount: data.media_fee_amount ? parseFloat(data.media_fee_amount) : null,
media_fee_tax: data.media_fee_tax ? parseFloat(data.media_fee_tax) : null,
tax_percent: parseFloat(data.tax_percent) || 0, tax_percent: parseFloat(data.tax_percent) || 0,
total_spending: totalSpending, total_spending: totalSpending,
nett_amount: parseFloat(form.data.nett_amount) || 0,
})); }));
form.post(route("client-invoices.store")); form.post(route("client-invoices.store"));

View File

@ -30,10 +30,26 @@ export default function Page({ invoice, availableInvoices }: Props) {
invoice.management_fee !== null && invoice.management_fee !== undefined invoice.management_fee !== null && invoice.management_fee !== undefined
? String(invoice.management_fee) ? String(invoice.management_fee)
: "", : "",
management_fee_amount:
invoice.management_fee_amount !== null && invoice.management_fee_amount !== undefined
? String(invoice.management_fee_amount)
: "",
management_fee_tax:
invoice.management_fee_tax !== null && invoice.management_fee_tax !== undefined
? String(invoice.management_fee_tax)
: "",
media_fee: media_fee:
invoice.media_fee !== null && invoice.media_fee !== undefined invoice.media_fee !== null && invoice.media_fee !== undefined
? String(invoice.media_fee) ? String(invoice.media_fee)
: "", : "",
media_fee_amount:
invoice.media_fee_amount !== null && invoice.media_fee_amount !== undefined
? String(invoice.media_fee_amount)
: "",
media_fee_tax:
invoice.media_fee_tax !== null && invoice.media_fee_tax !== undefined
? String(invoice.media_fee_tax)
: "",
tax_percent: tax_percent:
invoice.tax_percent !== null && invoice.tax_percent !== undefined invoice.tax_percent !== null && invoice.tax_percent !== undefined
? String(invoice.tax_percent) ? String(invoice.tax_percent)
@ -44,11 +60,18 @@ export default function Page({ invoice, availableInvoices }: Props) {
: "", : "",
client_id: invoice.client_id ? String(invoice.client_id) : undefined, client_id: invoice.client_id ? String(invoice.client_id) : undefined,
customer_id: invoice.client?.customer_id ?? "", customer_id: invoice.client?.customer_id ?? "",
nett_amount: invoice.nett_amount !== null && invoice.nett_amount !== undefined ? String(invoice.nett_amount) : "",
}); });
const isApproved = !!invoice.approved_at; const isApproved = !!invoice.approved_at;
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (!form.data.nett_amount) {
form.setError("nett_amount", "Media Nett Amount is required.");
return;
}
const totalSpending = form.data.total_spending const totalSpending = form.data.total_spending
? parseFloat(form.data.total_spending) ? parseFloat(form.data.total_spending)
: null; : null;
@ -66,6 +89,7 @@ export default function Page({ invoice, availableInvoices }: Props) {
media_fee: parseFloat(data.media_fee) || 0, media_fee: parseFloat(data.media_fee) || 0,
tax_percent: parseFloat(data.tax_percent) || 0, tax_percent: parseFloat(data.tax_percent) || 0,
total_spending: totalSpending, total_spending: totalSpending,
nett_amount: parseFloat(form.data.nett_amount) || 0,
})); }));
form.put(route("client-invoices.update", { invoice: invoice.id })); form.put(route("client-invoices.update", { invoice: invoice.id }));
@ -91,11 +115,11 @@ export default function Page({ invoice, availableInvoices }: Props) {
</Badge> </Badge>
</Group> </Group>
<Group gap="sm"> <Group gap="sm">
{!isApproved && ( {/* {!isApproved && (
<Button color="green" onClick={handleApprove}> <Button color="green" onClick={handleApprove}>
Approve invoice Approve invoice
</Button> </Button>
)} )} */}
<Button <Button
component={Link} component={Link}
href={route("google-ads.accounts.show", { id: invoice.client?.customer_id ?? "" })} href={route("google-ads.accounts.show", { id: invoice.client?.customer_id ?? "" })}

View File

@ -151,13 +151,13 @@ export default function Dashboard({
icon: IconChecklist, icon: IconChecklist,
color: 'violet', color: 'violet',
}, },
{ // {
label: 'Missing SQL Code', // label: 'Missing SQL Code',
value: stats.clientsMissingSqlCode.toLocaleString('en-MY'), // value: stats.clientsMissingSqlCode.toLocaleString('en-MY'),
detail: 'Synced clients not linked to SQL', // detail: 'Synced clients not linked to SQL',
icon: IconLinkOff, // icon: IconLinkOff,
color: 'red', // color: 'red',
}, // },
]; ];
return ( return (
@ -191,7 +191,7 @@ export default function Dashboard({
</Group> </Group>
</Paper> </Paper>
<SimpleGrid cols={4} breakpoints={[ <SimpleGrid cols={3} breakpoints={[
{ maxWidth: 'lg', cols: 2 }, { maxWidth: 'lg', cols: 2 },
{ maxWidth: 'md', cols: 2 }, { maxWidth: 'md', cols: 2 },
{ maxWidth: 'xs', cols: 1 }, { maxWidth: 'xs', cols: 1 },

View File

@ -3,6 +3,7 @@ import { LucideIcon } from 'lucide-react';
export interface Auth { export interface Auth {
user: User; user: User;
permissions?: Record<string, boolean> | null;
} }
export interface BreadcrumbItem { export interface BreadcrumbItem {
@ -83,7 +84,11 @@ export interface ClientInvoice {
amount: number; amount: number;
total_spend?: number | string; total_spend?: number | string;
management_fee?: number; management_fee?: number;
management_fee_amount?: number;
management_fee_tax?: number;
media_fee?: number; media_fee?: number;
media_fee_amount?: number;
media_fee_tax?: number;
tax_percent?: number; tax_percent?: number;
nett_amount?: number; nett_amount?: number;
total_spending?: number | null; total_spending?: number | null;

View File

@ -16,7 +16,9 @@
})->name('home'); })->name('home');
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
Route::get('dashboard', DashboardController::class)->name('dashboard'); Route::get('dashboard', DashboardController::class)
->middleware('permission:dashboard.view')
->name('dashboard');
Route::prefix('google-ads') Route::prefix('google-ads')
->name('google-ads.') ->name('google-ads.')
@ -25,50 +27,65 @@
->name('accounts.') ->name('accounts.')
->controller(GoogleAdsController::class) ->controller(GoogleAdsController::class)
->group(function () { ->group(function () {
Route::get('/', 'accounts')->name('index'); Route::get('/', 'accounts')
->middleware('permission:google-ads.accounts.view')
->name('index');
Route::post('/sync-google-company-details', 'syncGoogleCompanyDetails') Route::post('/sync-google-company-details', 'syncGoogleCompanyDetails')
->middleware('permission:google-ads.accounts.sync')
->name('sync-google-company-details'); ->name('sync-google-company-details');
Route::get('/{id}/edit', 'edit')->name('edit'); Route::get('/{id}/edit', 'edit')
Route::get('/{id}', 'show')->name('show'); ->middleware('permission:google-ads.accounts.update')
Route::post('/{id}/account', 'updateAccount')->name('account.update'); ->name('edit');
Route::get('/{id}', 'show')
->middleware('permission:google-ads.accounts.view')
->name('show');
Route::post('/{id}/account', 'updateAccount')
->middleware('permission:google-ads.accounts.update')
->name('account.update');
Route::prefix('activity') Route::prefix('activity')
->name('activity.') ->name('activity.')
->controller(ActivityController::class) ->controller(ActivityController::class)
->group(function () { ->group(function () {
Route::post('/{id}/store', 'storeActivity') Route::post('/{id}/store', 'storeActivity')
->middleware('permission:google-ads.activities.create')
->name('storeActivity'); ->name('storeActivity');
Route::patch('/{id}/update', 'updateActivity') Route::patch('/{id}/update', 'updateActivity')
->middleware('permission:google-ads.activities.update')
->name('updateActivity'); ->name('updateActivity');
Route::patch('/{id}/complete', 'completeActivity') Route::patch('/{id}/complete', 'completeActivity')
->middleware('permission:google-ads.activities.complete')
->name('completeActivity'); ->name('completeActivity');
Route::delete('/{id}/delete', 'deleteActivity') Route::delete('/{id}/delete', 'deleteActivity')
->middleware('permission:google-ads.activities.delete')
->name('deleteActivity'); ->name('deleteActivity');
}); });
}); });
Route::get('import', [GoogleAdsController::class, 'insertCSVDataToDB'])->name('import'); Route::get('import', [GoogleAdsController::class, 'insertCSVDataToDB'])
->middleware('permission:google-ads.import')
->name('import');
}); });
Route::prefix('client-invoices') Route::prefix('client-invoices')
->name('client-invoices.') ->name('client-invoices.')
->controller(ClientInvoiceController::class) ->controller(ClientInvoiceController::class)
->group(function () { ->group(function () {
Route::get('/create', 'create')->name('create'); Route::get('/create', 'create')->middleware('permission:client-invoices.create')->name('create');
Route::post('/', 'store')->name('store'); Route::post('/', 'store')->middleware('permission:client-invoices.create')->name('store');
Route::get('{invoice}/client/create', 'createClient')->name('client.create'); Route::get('{invoice}/client/create', 'createClient')->middleware('permission:client-invoices.create-client')->name('client.create');
Route::post('{invoice}/client', 'storeClient')->name('client.store'); Route::post('{invoice}/client', 'storeClient')->middleware('permission:client-invoices.create-client')->name('client.store');
Route::get('{invoice}/edit', 'edit')->name('edit'); Route::get('{invoice}/edit', 'edit')->middleware('permission:client-invoices.update')->name('edit');
Route::put('{invoice}', 'update')->name('update'); Route::put('{invoice}', 'update')->middleware('permission:client-invoices.update')->name('update');
Route::patch('{invoice}/approve', 'approve')->name('approve'); Route::patch('{invoice}/approve', 'approve')->middleware('permission:client-invoices.approve')->name('approve');
Route::delete('{invoice}', 'destroy')->name('destroy'); Route::delete('{invoice}', 'destroy')->middleware('permission:client-invoices.delete')->name('destroy');
Route::get('/pdf/invoice/{id}', 'getPdfInvoice')->name('getPdfInvoice'); Route::get('/pdf/invoice/{id}', 'getPdfInvoice')->middleware('permission:client-invoices.view-pdf')->name('getPdfInvoice');
}); });
Route::prefix('clients') Route::prefix('clients')
->name('clients.') ->name('clients.')
->controller(ClientInvoiceAdjustmentController::class) ->controller(ClientInvoiceAdjustmentController::class)
->group(function () { ->group(function () {
Route::post('{client}/adjustments', 'store')->name('adjustments.store'); Route::post('{client}/adjustments', 'store')->middleware('permission:clients.adjustments.create')->name('adjustments.store');
Route::delete('adjustments/{adjustment}', 'destroy')->name('adjustments.destroy'); Route::delete('adjustments/{adjustment}', 'destroy')->middleware('permission:clients.adjustments.delete')->name('adjustments.destroy');
}); });
Route::prefix('google') Route::prefix('google')
@ -76,6 +93,7 @@
->controller(GoogleController::class) ->controller(GoogleController::class)
->group(function () { ->group(function () {
Route::post('/getCampaignsDetails', 'listCampaignsMetrics') Route::post('/getCampaignsDetails', 'listCampaignsMetrics')
->middleware('permission:google.reports.view')
->name('getCampaignsDetails'); ->name('getCampaignsDetails');
}); });
@ -88,20 +106,20 @@
->group(function () { ->group(function () {
// Route::post('/getCampaignsDetails', 'listCampaignsMetrics') // Route::post('/getCampaignsDetails', 'listCampaignsMetrics')
// ->name('getCampaignsDetails'); // ->name('getCampaignsDetails');
Route::get('/', 'index')->name('index'); Route::get('/', 'index')->middleware('permission:management.roles.view')->name('index');
Route::get('{id}/edit/', 'edit')->name('edit'); Route::get('{id}/edit/', 'edit')->middleware('permission:management.roles.update')->name('edit');
Route::post('{id}/update/', 'update')->name('update'); Route::post('{id}/update/', 'update')->middleware('permission:management.roles.update')->name('update');
}); });
Route::prefix('users') Route::prefix('users')
->name('users.') ->name('users.')
->controller(UserController::class) ->controller(UserController::class)
->group(function () { ->group(function () {
Route::get('/', 'index')->name('index'); Route::get('/', 'index')->middleware('permission:management.users.view')->name('index');
Route::get('/create', 'create')->name('create'); Route::get('/create', 'create')->middleware('permission:management.users.create')->name('create');
Route::post('/store', 'store')->name('store'); Route::post('/store', 'store')->middleware('permission:management.users.create')->name('store');
Route::get('{id}/edit', 'edit')->name('edit'); Route::get('{id}/edit', 'edit')->middleware('permission:management.users.update')->name('edit');
Route::post('{id}/update', 'update')->name('update'); Route::post('{id}/update', 'update')->middleware('permission:management.users.update')->name('update');
}); });
}); });
}); });