feat: Invoice Changes
This commit is contained in:
parent
3f319aae4f
commit
6edadb1b04
@ -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,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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']],
|
||||||
|
|||||||
@ -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,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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: [
|
||||||
|
|||||||
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"));
|
||||||
|
|||||||
@ -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 ?? "" })}
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
5
resources/js/types/index.d.ts
vendored
5
resources/js/types/index.d.ts
vendored
@ -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;
|
||||||
|
|||||||
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user