Andrew Wallo 4 个月前
父节点
当前提交
00ca86bdaa

+ 10
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/PayBills.php 查看文件

66
         return [
66
         return [
67
             Actions\Action::make('processPayments')
67
             Actions\Action::make('processPayments')
68
                 ->color('primary')
68
                 ->color('primary')
69
+                ->requiresConfirmation()
70
+                ->modalHeading('Confirm payments')
71
+                ->modalDescription(function () {
72
+                    $billCount = collect($this->paymentAmounts)->filter(fn ($amount) => $amount > 0)->count();
73
+                    $totalAmount = array_sum($this->paymentAmounts);
74
+                    $currencyCode = $this->getTableFilterState('currency_code')['value'];
75
+                    $totalFormatted = CurrencyConverter::formatCentsToMoney($totalAmount, $currencyCode, true);
76
+
77
+                    return "You are about to pay {$billCount} " . Str::plural('bill', $billCount) . " for a total of {$totalFormatted}. This action cannot be undone.";
78
+                })
69
                 ->action(function () {
79
                 ->action(function () {
70
                     $data = $this->data;
80
                     $data = $this->data;
71
                     $tableRecords = $this->getTableRecords();
81
                     $tableRecords = $this->getTableRecords();

+ 2
- 2
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ListInvoices.php 查看文件

7
 use App\Filament\Company\Resources\Sales\InvoiceResource;
7
 use App\Filament\Company\Resources\Sales\InvoiceResource;
8
 use App\Filament\Company\Resources\Sales\InvoiceResource\Widgets;
8
 use App\Filament\Company\Resources\Sales\InvoiceResource\Widgets;
9
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource\Pages\ViewRecurringInvoice;
9
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource\Pages\ViewRecurringInvoice;
10
+use App\Filament\Infolists\Components\BannerEntry;
10
 use App\Models\Accounting\RecurringInvoice;
11
 use App\Models\Accounting\RecurringInvoice;
11
-use CodeWithDennis\SimpleAlert\Components\Infolists\SimpleAlert;
12
 use Filament\Actions;
12
 use Filament\Actions;
13
 use Filament\Infolists\Components\Actions\Action;
13
 use Filament\Infolists\Components\Actions\Action;
14
 use Filament\Infolists\Infolist;
14
 use Filament\Infolists\Infolist;
37
     {
37
     {
38
         return $infolist
38
         return $infolist
39
             ->schema([
39
             ->schema([
40
-                SimpleAlert::make('recurringInvoiceFilter')
40
+                BannerEntry::make('recurringInvoiceFilter')
41
                     ->info()
41
                     ->info()
42
                     ->title(function () {
42
                     ->title(function () {
43
                         if (empty($this->recurringInvoice)) {
43
                         if (empty($this->recurringInvoice)) {

+ 59
- 51
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/RecordPayments.php 查看文件

45
 
45
 
46
     public ?array $data = [];
46
     public ?array $data = [];
47
 
47
 
48
-    // New property for allocation amount
49
     public ?int $allocationAmount = null;
48
     public ?int $allocationAmount = null;
50
 
49
 
51
     #[Url(as: 'invoice_id')]
50
     #[Url(as: 'invoice_id')]
94
         return [
93
         return [
95
             Actions\Action::make('processPayments')
94
             Actions\Action::make('processPayments')
96
                 ->color('primary')
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
                 ->action(function () {
106
                 ->action(function () {
98
                     $data = $this->data;
107
                     $data = $this->data;
99
                     $tableRecords = $this->getTableRecords();
108
                     $tableRecords = $this->getTableRecords();
201
                             ->options(PaymentMethod::class)
210
                             ->options(PaymentMethod::class)
202
                             ->default(PaymentMethod::BankPayment)
211
                             ->default(PaymentMethod::BankPayment)
203
                             ->softRequired(),
212
                             ->softRequired(),
204
-                        // Allocation amount field with suffix action
205
                         Forms\Components\TextInput::make('allocation_amount')
213
                         Forms\Components\TextInput::make('allocation_amount')
206
                             ->label('Allocate Payment Amount')
214
                             ->label('Allocate Payment Amount')
207
-                            ->live()
208
                             ->default(array_sum($this->paymentAmounts))
215
                             ->default(array_sum($this->paymentAmounts))
209
                             ->money($this->getTableFilterState('currency_code')['value'])
216
                             ->money($this->getTableFilterState('currency_code')['value'])
217
+                            ->extraAlpineAttributes([
218
+                                'x-on:keydown.enter.prevent' => '$refs.allocate.click()',
219
+                            ])
210
                             ->suffixAction(
220
                             ->suffixAction(
211
                                 Forms\Components\Actions\Action::make('allocate')
221
                                 Forms\Components\Actions\Action::make('allocate')
212
                                     ->icon('heroicon-m-calculator')
222
                                     ->icon('heroicon-m-calculator')
223
+                                    ->extraAttributes([
224
+                                        'x-ref' => 'allocate',
225
+                                    ])
213
                                     ->action(function ($state) {
226
                                     ->action(function ($state) {
214
                                         $this->allocationAmount = CurrencyConverter::convertToCents($state, 'USD');
227
                                         $this->allocationAmount = CurrencyConverter::convertToCents($state, 'USD');
215
                                         if ($this->allocationAmount && $this->hasSelectedClient()) {
228
                                         if ($this->allocationAmount && $this->hasSelectedClient()) {
216
                                             $this->allocateOldestFirst($this->getTableRecords(), $this->allocationAmount);
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
             ])->statePath('data');
234
             ])->statePath('data');
240
             ->recordClasses(['is-spreadsheet'])
245
             ->recordClasses(['is-spreadsheet'])
241
             ->defaultSort('due_date')
246
             ->defaultSort('due_date')
242
             ->paginated(false)
247
             ->paginated(false)
248
+            ->emptyStateHeading('No client selected')
249
+            ->emptyStateDescription('Select a client from the filters above to view and process invoice payments.')
243
             ->columns([
250
             ->columns([
244
                 TextColumn::make('client.name')
251
                 TextColumn::make('client.name')
245
                     ->label('Client')
252
                     ->label('Client')
335
                                 return $activeCurrency && $activeCurrency !== $bankAccountCurrency;
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
             ->bulkActions([
346
             ->bulkActions([
362
                 Tables\Actions\BulkAction::make('setFullAmounts')
347
                 Tables\Actions\BulkAction::make('setFullAmounts')
419
 
404
 
420
                         return $query->where('client_id', $data['value']);
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
                 Tables\Filters\SelectFilter::make('status')
447
                 Tables\Filters\SelectFilter::make('status')
423
                     ->multiple()
448
                     ->multiple()
424
                     ->options(InvoiceStatus::getUnpaidOptions()),
449
                     ->options(InvoiceStatus::getUnpaidOptions()),
440
         return CurrencyConverter::formatCentsToMoney($total, $currencyCode, true);
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
     public function getSelectedBankAccount(): BankAccount
468
     public function getSelectedBankAccount(): BankAccount
459
     {
469
     {
460
         $bankAccountId = $this->data['bank_account_id'];
470
         $bankAccountId = $this->data['bank_account_id'];
472
         $visibleInvoiceKeys = array_flip($visibleInvoiceIds);
482
         $visibleInvoiceKeys = array_flip($visibleInvoiceIds);
473
 
483
 
474
         $this->paymentAmounts = array_intersect_key($this->paymentAmounts, $visibleInvoiceKeys);
484
         $this->paymentAmounts = array_intersect_key($this->paymentAmounts, $visibleInvoiceKeys);
475
-
476
-        // Reset allocation when client changes
477
         $this->allocationAmount = null;
485
         $this->allocationAmount = null;
478
     }
486
     }
479
 }
487
 }

+ 11
- 0
app/Models/Accounting/Invoice.php 查看文件

186
         return $query->whereIn('status', InvoiceStatus::unpaidStatuses());
186
         return $query->whereIn('status', InvoiceStatus::unpaidStatuses());
187
     }
187
     }
188
 
188
 
189
+    // TODO: Consider storing the numeric part of the invoice number separately
190
+    public function scopeByNumber(Builder $query, string $number): Builder
191
+    {
192
+        $invoicePrefix = DocumentDefault::invoice()->first()->number_prefix ?? '';
193
+
194
+        return $query->where(function ($q) use ($number, $invoicePrefix) {
195
+            $q->where('invoice_number', $number)
196
+                ->orWhere('invoice_number', $invoicePrefix . $number);
197
+        });
198
+    }
199
+
189
     public function scopeOverdue(Builder $query): Builder
200
     public function scopeOverdue(Builder $query): Builder
190
     {
201
     {
191
         return $query
202
         return $query

+ 1
- 1
app/Models/Setting/DocumentDefault.php 查看文件

75
 
75
 
76
     public function scopeType(Builder $query, string | DocumentType $type): Builder
76
     public function scopeType(Builder $query, string | DocumentType $type): Builder
77
     {
77
     {
78
-        return $query->where($this->qualifyColumn('type'), $type);
78
+        return $query->where('type', $type);
79
     }
79
     }
80
 
80
 
81
     public function scopeInvoice(Builder $query): Builder
81
     public function scopeInvoice(Builder $query): Builder

+ 6
- 1
app/Providers/Filament/CompanyPanelProvider.php 查看文件

284
             $table
284
             $table
285
                 ->paginationPageOptions([5, 10, 25, 50, 100])
285
                 ->paginationPageOptions([5, 10, 25, 50, 100])
286
                 ->filtersFormWidth(MaxWidth::Small)
286
                 ->filtersFormWidth(MaxWidth::Small)
287
-                ->filtersTriggerAction(fn (Tables\Actions\Action $action) => $action->slideOver());
287
+                ->filtersTriggerAction(
288
+                    fn (Tables\Actions\Action $action) => $action
289
+                        ->button()
290
+                        ->label('Filters')
291
+                        ->slideOver()
292
+                );
288
         });
293
         });
289
 
294
 
290
         Tables\Columns\TextColumn::configureUsing(function (Tables\Columns\TextColumn $column): void {
295
         Tables\Columns\TextColumn::configureUsing(function (Tables\Columns\TextColumn $column): void {

正在加载...
取消
保存