|
@@ -45,7 +45,6 @@ class RecordPayments extends ListRecords
|
45
|
45
|
|
46
|
46
|
public ?array $data = [];
|
47
|
47
|
|
48
|
|
- // New property for allocation amount
|
49
|
48
|
public ?int $allocationAmount = null;
|
50
|
49
|
|
51
|
50
|
#[Url(as: 'invoice_id')]
|
|
@@ -94,6 +93,16 @@ class RecordPayments extends ListRecords
|
94
|
93
|
return [
|
95
|
94
|
Actions\Action::make('processPayments')
|
96
|
95
|
->color('primary')
|
|
96
|
+ ->requiresConfirmation()
|
|
97
|
+ ->modalHeading('Confirm payments')
|
|
98
|
+ ->modalDescription(function () {
|
|
99
|
+ $invoiceCount = collect($this->paymentAmounts)->filter(fn ($amount) => $amount > 0)->count();
|
|
100
|
+ $totalAmount = array_sum($this->paymentAmounts);
|
|
101
|
+ $currencyCode = $this->getTableFilterState('currency_code')['value'];
|
|
102
|
+ $totalFormatted = CurrencyConverter::formatCentsToMoney($totalAmount, $currencyCode, true);
|
|
103
|
+
|
|
104
|
+ return "You are about to pay {$invoiceCount} " . Str::plural('invoice', $invoiceCount) . " for a total of {$totalFormatted}. This action cannot be undone.";
|
|
105
|
+ })
|
97
|
106
|
->action(function () {
|
98
|
107
|
$data = $this->data;
|
99
|
108
|
$tableRecords = $this->getTableRecords();
|
|
@@ -201,29 +210,25 @@ class RecordPayments extends ListRecords
|
201
|
210
|
->options(PaymentMethod::class)
|
202
|
211
|
->default(PaymentMethod::BankPayment)
|
203
|
212
|
->softRequired(),
|
204
|
|
- // Allocation amount field with suffix action
|
205
|
213
|
Forms\Components\TextInput::make('allocation_amount')
|
206
|
214
|
->label('Allocate Payment Amount')
|
207
|
|
- ->live()
|
208
|
215
|
->default(array_sum($this->paymentAmounts))
|
209
|
216
|
->money($this->getTableFilterState('currency_code')['value'])
|
|
217
|
+ ->extraAlpineAttributes([
|
|
218
|
+ 'x-on:keydown.enter.prevent' => '$refs.allocate.click()',
|
|
219
|
+ ])
|
210
|
220
|
->suffixAction(
|
211
|
221
|
Forms\Components\Actions\Action::make('allocate')
|
212
|
222
|
->icon('heroicon-m-calculator')
|
|
223
|
+ ->extraAttributes([
|
|
224
|
+ 'x-ref' => 'allocate',
|
|
225
|
+ ])
|
213
|
226
|
->action(function ($state) {
|
214
|
227
|
$this->allocationAmount = CurrencyConverter::convertToCents($state, 'USD');
|
215
|
228
|
if ($this->allocationAmount && $this->hasSelectedClient()) {
|
216
|
229
|
$this->allocateOldestFirst($this->getTableRecords(), $this->allocationAmount);
|
217
|
|
-
|
218
|
|
- $amountFormatted = CurrencyConverter::formatCentsToMoney($this->allocationAmount, 'USD', true);
|
219
|
|
-
|
220
|
|
- Notification::make()
|
221
|
|
- ->title('Payment allocated')
|
222
|
|
- ->body("Allocated {$amountFormatted} across invoices")
|
223
|
|
- ->success()
|
224
|
|
- ->send();
|
225
|
230
|
}
|
226
|
|
- })
|
|
231
|
+ }),
|
227
|
232
|
),
|
228
|
233
|
]),
|
229
|
234
|
])->statePath('data');
|
|
@@ -240,6 +245,8 @@ class RecordPayments extends ListRecords
|
240
|
245
|
->recordClasses(['is-spreadsheet'])
|
241
|
246
|
->defaultSort('due_date')
|
242
|
247
|
->paginated(false)
|
|
248
|
+ ->emptyStateHeading('No client selected')
|
|
249
|
+ ->emptyStateDescription('Select a client from the filters above to view and process invoice payments.')
|
243
|
250
|
->columns([
|
244
|
251
|
TextColumn::make('client.name')
|
245
|
252
|
->label('Client')
|
|
@@ -335,28 +342,6 @@ class RecordPayments extends ListRecords
|
335
|
342
|
return $activeCurrency && $activeCurrency !== $bankAccountCurrency;
|
336
|
343
|
}),
|
337
|
344
|
]),
|
338
|
|
- // New allocation status column
|
339
|
|
- TextColumn::make('allocation_status')
|
340
|
|
- ->label('Status')
|
341
|
|
- ->getStateUsing(function (Invoice $record) {
|
342
|
|
- $paymentAmount = $this->paymentAmounts[$record->id] ?? 0;
|
343
|
|
-
|
344
|
|
- if ($paymentAmount <= 0) {
|
345
|
|
- return 'No payment';
|
346
|
|
- }
|
347
|
|
-
|
348
|
|
- if ($paymentAmount >= $record->amount_due) {
|
349
|
|
- return 'Full payment';
|
350
|
|
- }
|
351
|
|
-
|
352
|
|
- return 'Partial payment';
|
353
|
|
- })
|
354
|
|
- ->badge()
|
355
|
|
- ->color(fn (string $state): string => match ($state) {
|
356
|
|
- 'Full payment' => 'success',
|
357
|
|
- 'Partial payment' => 'warning',
|
358
|
|
- default => 'gray',
|
359
|
|
- }),
|
360
|
345
|
])
|
361
|
346
|
->bulkActions([
|
362
|
347
|
Tables\Actions\BulkAction::make('setFullAmounts')
|
|
@@ -419,6 +404,46 @@ class RecordPayments extends ListRecords
|
419
|
404
|
|
420
|
405
|
return $query->where('client_id', $data['value']);
|
421
|
406
|
}),
|
|
407
|
+ Tables\Filters\Filter::make('invoice_lookup')
|
|
408
|
+ ->label('Find Invoice')
|
|
409
|
+ ->form([
|
|
410
|
+ Forms\Components\TextInput::make('invoice_number')
|
|
411
|
+ ->label('Invoice Number')
|
|
412
|
+ ->placeholder('Enter invoice number')
|
|
413
|
+ ->suffixAction(
|
|
414
|
+ Forms\Components\Actions\Action::make('findInvoice')
|
|
415
|
+ ->icon('heroicon-m-magnifying-glass')
|
|
416
|
+ ->keyBindings(['enter'])
|
|
417
|
+ ->action(function ($state, Forms\Set $set) {
|
|
418
|
+ if (blank($state)) {
|
|
419
|
+ return;
|
|
420
|
+ }
|
|
421
|
+
|
|
422
|
+ $invoice = Invoice::byNumber($state)
|
|
423
|
+ ->unpaid()
|
|
424
|
+ ->first();
|
|
425
|
+
|
|
426
|
+ if ($invoice) {
|
|
427
|
+ $set('tableFilters.client_id.value', $invoice->client_id, true);
|
|
428
|
+ $this->paymentAmounts[$invoice->id] = $invoice->amount_due;
|
|
429
|
+
|
|
430
|
+ Notification::make()
|
|
431
|
+ ->title('Invoice found')
|
|
432
|
+ ->body("Found invoice {$invoice->invoice_number} for {$invoice->client->name}")
|
|
433
|
+ ->success()
|
|
434
|
+ ->send();
|
|
435
|
+ } else {
|
|
436
|
+ Notification::make()
|
|
437
|
+ ->title('Invoice not found')
|
|
438
|
+ ->body("No unpaid invoice found with number: {$state}")
|
|
439
|
+ ->warning()
|
|
440
|
+ ->send();
|
|
441
|
+ }
|
|
442
|
+ })
|
|
443
|
+ ),
|
|
444
|
+ ])
|
|
445
|
+ ->query(null)
|
|
446
|
+ ->indicateUsing(null),
|
422
|
447
|
Tables\Filters\SelectFilter::make('status')
|
423
|
448
|
->multiple()
|
424
|
449
|
->options(InvoiceStatus::getUnpaidOptions()),
|
|
@@ -440,21 +465,6 @@ class RecordPayments extends ListRecords
|
440
|
465
|
return CurrencyConverter::formatCentsToMoney($total, $currencyCode, true);
|
441
|
466
|
}
|
442
|
467
|
|
443
|
|
- #[Computed]
|
444
|
|
- public function allocationVariance(): string
|
445
|
|
- {
|
446
|
|
- if (! $this->allocationAmount) {
|
447
|
|
- return '$0.00';
|
448
|
|
- }
|
449
|
|
-
|
450
|
|
- $totalAllocated = array_sum($this->paymentAmounts);
|
451
|
|
- $variance = $this->allocationAmount - $totalAllocated;
|
452
|
|
-
|
453
|
|
- $currencyCode = $this->getTableFilterState('currency_code')['value'];
|
454
|
|
-
|
455
|
|
- return CurrencyConverter::formatCentsToMoney($variance, $currencyCode, true);
|
456
|
|
- }
|
457
|
|
-
|
458
|
468
|
public function getSelectedBankAccount(): BankAccount
|
459
|
469
|
{
|
460
|
470
|
$bankAccountId = $this->data['bank_account_id'];
|
|
@@ -472,8 +482,6 @@ class RecordPayments extends ListRecords
|
472
|
482
|
$visibleInvoiceKeys = array_flip($visibleInvoiceIds);
|
473
|
483
|
|
474
|
484
|
$this->paymentAmounts = array_intersect_key($this->paymentAmounts, $visibleInvoiceKeys);
|
475
|
|
-
|
476
|
|
- // Reset allocation when client changes
|
477
|
485
|
$this->allocationAmount = null;
|
478
|
486
|
}
|
479
|
487
|
}
|