Andrew Wallo 4 ay önce
ebeveyn
işleme
5ecdc7b585

+ 1
- 0
app/Filament/Company/Resources/Purchases/BillResource.php Dosyayı Görüntüle

@@ -668,6 +668,7 @@ class BillResource extends Resource
668 668
     {
669 669
         return [
670 670
             'index' => Pages\ListBills::route('/'),
671
+            'pay-bills' => Pages\PayBills::route('/pay-bills'),
671 672
             'create' => Pages\CreateBill::route('/create'),
672 673
             'view' => Pages\ViewBill::route('/{record}'),
673 674
             'edit' => Pages\EditBill::route('/{record}/edit'),

+ 5
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/ListBills.php Dosyayı Görüntüle

@@ -20,6 +20,11 @@ class ListBills extends ListRecords
20 20
     protected function getHeaderActions(): array
21 21
     {
22 22
         return [
23
+            Actions\Action::make('payBills')
24
+                ->label('Pay Bills')
25
+                ->icon('heroicon-o-credit-card')
26
+                ->color('primary')
27
+                ->url(fn () => BillResource::getUrl('pay-bills')),
23 28
             Actions\CreateAction::make(),
24 29
         ];
25 30
     }

+ 294
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/PayBills.php Dosyayı Görüntüle

@@ -0,0 +1,294 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
4
+
5
+use App\Enums\Accounting\BillStatus;
6
+use App\Enums\Accounting\PaymentMethod;
7
+use App\Filament\Company\Resources\Purchases\BillResource;
8
+use App\Models\Accounting\Bill;
9
+use App\Models\Accounting\Transaction;
10
+use App\Models\Banking\BankAccount;
11
+use App\Utilities\Currency\CurrencyConverter;
12
+use Filament\Actions;
13
+use Filament\Forms;
14
+use Filament\Forms\Form;
15
+use Filament\Notifications\Notification;
16
+use Filament\Resources\Pages\ListRecords;
17
+use Filament\Support\RawJs;
18
+use Filament\Tables;
19
+use Filament\Tables\Columns\TextColumn;
20
+use Filament\Tables\Columns\TextInputColumn;
21
+use Filament\Tables\Table;
22
+use Illuminate\Database\Eloquent\Collection;
23
+use Livewire\Attributes\Computed;
24
+
25
+class PayBills extends ListRecords
26
+{
27
+    protected static string $resource = BillResource::class;
28
+
29
+    protected static ?string $title = 'Pay Bills';
30
+
31
+    protected static ?string $navigationLabel = 'Pay Bills';
32
+
33
+    protected static string $view = 'filament.company.resources.purchases.bill-resource.pages.pay-bills';
34
+
35
+    public array $paymentAmounts = [];
36
+
37
+    public ?array $data = [];
38
+
39
+    public function getTitle(): string
40
+    {
41
+        return 'Pay Bills';
42
+    }
43
+
44
+    public function getBreadcrumb(): string
45
+    {
46
+        return 'Pay Bills';
47
+    }
48
+
49
+    public function mount(): void
50
+    {
51
+        parent::mount();
52
+
53
+        $this->form->fill([
54
+            'bank_account_id' => BankAccount::where('enabled', true)->first()?->id,
55
+            'payment_date' => now(),
56
+            'payment_method' => PaymentMethod::Check->value,
57
+        ]);
58
+    }
59
+
60
+    protected function getHeaderActions(): array
61
+    {
62
+        return [
63
+            Actions\Action::make('paySelected')
64
+                ->label('Pay Selected Bills')
65
+                ->icon('heroicon-o-credit-card')
66
+                ->color('primary')
67
+                ->action(function () {
68
+                    $data = $this->data;
69
+                    $selectedRecords = $this->getTableRecords();
70
+                    $paidCount = 0;
71
+                    $totalPaid = 0;
72
+
73
+                    foreach ($selectedRecords as $bill) {
74
+                        if (! $bill->canRecordPayment()) {
75
+                            continue;
76
+                        }
77
+
78
+                        // Get the payment amount from our component state
79
+                        $paymentAmount = $this->getPaymentAmount($bill);
80
+
81
+                        if ($paymentAmount <= 0) {
82
+                            continue;
83
+                        }
84
+
85
+                        $paymentData = [
86
+                            'posted_at' => $data['payment_date'],
87
+                            'payment_method' => $data['payment_method'],
88
+                            'bank_account_id' => $data['bank_account_id'],
89
+                            'amount' => $paymentAmount,
90
+                        ];
91
+
92
+                        $bill->recordPayment($paymentData);
93
+                        $paidCount++;
94
+                        $totalPaid += $paymentAmount;
95
+                    }
96
+
97
+                    $totalFormatted = CurrencyConverter::formatCentsToMoney($totalPaid);
98
+
99
+                    Notification::make()
100
+                        ->title('Bills paid successfully')
101
+                        ->body("Paid {$paidCount} bill(s) for a total of {$totalFormatted}")
102
+                        ->success()
103
+                        ->send();
104
+
105
+                    // Clear payment amounts after successful payment
106
+                    foreach ($selectedRecords as $bill) {
107
+                        $this->paymentAmounts[$bill->id] = 0;
108
+                    }
109
+
110
+                    $this->resetTable();
111
+                }),
112
+        ];
113
+    }
114
+
115
+    /**
116
+     * @return array<int | string, string | Form>
117
+     */
118
+    protected function getForms(): array
119
+    {
120
+        return [
121
+            'form',
122
+        ];
123
+    }
124
+
125
+    public function form(Form $form): Form
126
+    {
127
+        return $form
128
+            ->schema([
129
+                Forms\Components\Grid::make(3)
130
+                    ->schema([
131
+                        Forms\Components\Select::make('bank_account_id')
132
+                            ->label('Bank Account')
133
+                            ->options(function () {
134
+                                return Transaction::getBankAccountOptionsFlat();
135
+                            })
136
+                            ->selectablePlaceholder(false)
137
+                            ->searchable()
138
+                            ->softRequired(),
139
+                        Forms\Components\DatePicker::make('payment_date')
140
+                            ->label('Payment Date')
141
+                            ->default(now())
142
+                            ->softRequired(),
143
+                        Forms\Components\Select::make('payment_method')
144
+                            ->label('Payment Method')
145
+                            ->selectablePlaceholder(false)
146
+                            ->options(PaymentMethod::class)
147
+                            ->default(PaymentMethod::Check)
148
+                            ->softRequired()
149
+                            ->live(),
150
+                    ]),
151
+            ])->statePath('data');
152
+    }
153
+
154
+    public function table(Table $table): Table
155
+    {
156
+        return $table
157
+            ->query(
158
+                Bill::query()
159
+                    ->with(['vendor'])
160
+                    ->whereIn('status', [
161
+                        BillStatus::Open,
162
+                        BillStatus::Partial,
163
+                        BillStatus::Overdue,
164
+                    ])
165
+            )
166
+            ->selectable()
167
+            ->columns([
168
+                TextColumn::make('vendor.name')
169
+                    ->label('Vendor')
170
+                    ->searchable()
171
+                    ->sortable(),
172
+                TextColumn::make('bill_number')
173
+                    ->label('Bill #')
174
+                    ->searchable()
175
+                    ->sortable(),
176
+                TextColumn::make('due_date')
177
+                    ->label('Due Date')
178
+                    ->date('M j, Y')
179
+                    ->sortable(),
180
+                TextColumn::make('amount_due')
181
+                    ->label('Amount Due')
182
+                    ->currencyWithConversion(fn (Bill $record) => $record->currency_code)
183
+                    ->alignEnd()
184
+                    ->sortable(),
185
+                TextInputColumn::make('payment_amount')
186
+                    ->label('Payment Amount')
187
+                    ->alignEnd()
188
+                    ->mask(RawJs::make('$money($input)'))
189
+                    ->updateStateUsing(function (Bill $record, $state) {
190
+                        if (empty($state) || $state === '0.00') {
191
+                            $this->paymentAmounts[$record->id] = 0;
192
+
193
+                            return '0.00';
194
+                        }
195
+
196
+                        $paymentCents = CurrencyConverter::convertToCents($state, $record->currency_code);
197
+
198
+                        // Validate payment doesn't exceed amount due
199
+                        if ($paymentCents > $record->amount_due) {
200
+                            Notification::make()
201
+                                ->title('Invalid payment amount')
202
+                                ->body('Payment cannot exceed amount due')
203
+                                ->warning()
204
+                                ->send();
205
+
206
+                            $maxAmount = CurrencyConverter::convertCentsToFormatSimple($record->amount_due, $record->currency_code);
207
+                            $this->paymentAmounts[$record->id] = $record->amount_due;
208
+
209
+                            return $maxAmount;
210
+                        }
211
+
212
+                        $this->paymentAmounts[$record->id] = $paymentCents;
213
+
214
+                        return $state;
215
+                    })
216
+                    ->getStateUsing(function (Bill $record) {
217
+                        $paymentAmount = $this->paymentAmounts[$record->id] ?? 0;
218
+
219
+                        return CurrencyConverter::convertCentsToFormatSimple($paymentAmount, $record->currency_code);
220
+                    }),
221
+            ])
222
+            ->actions([
223
+                Tables\Actions\Action::make('setFullAmount')
224
+                    ->label('Pay Full')
225
+                    ->icon('heroicon-o-banknotes')
226
+                    ->color('primary')
227
+                    ->action(function (Bill $record) {
228
+                        $this->paymentAmounts[$record->id] = $record->amount_due;
229
+                    }),
230
+                Tables\Actions\Action::make('clearAmount')
231
+                    ->label('Clear')
232
+                    ->icon('heroicon-o-x-mark')
233
+                    ->color('gray')
234
+                    ->action(function (Bill $record) {
235
+                        $this->paymentAmounts[$record->id] = 0;
236
+                    }),
237
+            ])
238
+            ->bulkActions([
239
+                Tables\Actions\BulkAction::make('setFullAmounts')
240
+                    ->label('Set Full Amounts')
241
+                    ->icon('heroicon-o-banknotes')
242
+                    ->color('primary')
243
+                    ->deselectRecordsAfterCompletion()
244
+                    ->action(function (Collection $records) {
245
+                        $records->each(function (Bill $bill) {
246
+                            $this->paymentAmounts[$bill->id] = $bill->amount_due;
247
+                        });
248
+                    }),
249
+                Tables\Actions\BulkAction::make('clearAmounts')
250
+                    ->label('Clear Amounts')
251
+                    ->icon('heroicon-o-x-mark')
252
+                    ->color('gray')
253
+                    ->deselectRecordsAfterCompletion()
254
+                    ->action(function (Collection $records) {
255
+                        $records->each(function (Bill $bill) {
256
+                            $this->paymentAmounts[$bill->id] = 0;
257
+                        });
258
+                    }),
259
+            ])
260
+            ->filters([
261
+                Tables\Filters\SelectFilter::make('vendor')
262
+                    ->relationship('vendor', 'name')
263
+                    ->searchable()
264
+                    ->preload(),
265
+                Tables\Filters\SelectFilter::make('status')
266
+                    ->multiple()
267
+                    ->options([
268
+                        BillStatus::Open->value => 'Open',
269
+                        BillStatus::Partial->value => 'Partial',
270
+                        BillStatus::Overdue->value => 'Overdue',
271
+                    ])
272
+                    ->default([BillStatus::Open->value, BillStatus::Overdue->value]),
273
+            ])
274
+            ->defaultSort('due_date')
275
+            ->striped()
276
+            ->paginated(false);
277
+    }
278
+
279
+    protected function getPaymentAmount(Bill $record): int
280
+    {
281
+        return $this->paymentAmounts[$record->id] ?? 0;
282
+    }
283
+
284
+    #[Computed]
285
+    public function totalSelectedPaymentAmount(): string
286
+    {
287
+        $selectedIds = array_keys($this->getSelectedTableRecords()->toArray());
288
+        $total = collect($selectedIds)
289
+            ->map(fn ($id) => $this->paymentAmounts[$id] ?? 0)
290
+            ->sum();
291
+
292
+        return CurrencyConverter::formatCentsToMoney($total);
293
+    }
294
+}

+ 20
- 0
app/Models/Accounting/Transaction.php Dosyayı Görüntüle

@@ -113,6 +113,26 @@ class Transaction extends Model
113 113
         }
114 114
     }
115 115
 
116
+    public static function getBankAccountOptionsFlat(?int $excludedAccountId = null, ?int $currentBankAccountId = null, bool $excludeArchived = true): array
117
+    {
118
+        return BankAccount::query()
119
+            ->whereHas('account', function (Builder $query) use ($excludeArchived) {
120
+                if ($excludeArchived) {
121
+                    $query->where('archived', false);
122
+                }
123
+            })
124
+            ->with(['account' => function ($query) use ($excludeArchived) {
125
+                if ($excludeArchived) {
126
+                    $query->where('archived', false);
127
+                }
128
+            }])
129
+            ->when($excludedAccountId, fn (Builder $query) => $query->where('account_id', '!=', $excludedAccountId))
130
+            ->when($currentBankAccountId, fn (Builder $query) => $query->orWhere('id', $currentBankAccountId))
131
+            ->get()
132
+            ->pluck('account.name', 'id')
133
+            ->toArray();
134
+    }
135
+
116 136
     public static function getBankAccountOptions(?int $excludedAccountId = null, ?int $currentBankAccountId = null, bool $excludeArchived = true): array
117 137
     {
118 138
         return BankAccount::query()

+ 29
- 0
resources/views/filament/company/resources/purchases/bill-resource/pages/pay-bills.blade.php Dosyayı Görüntüle

@@ -0,0 +1,29 @@
1
+<x-filament-panels::page>
2
+    <div class="space-y-6">
3
+        <div class="bg-white rounded-lg border border-gray-200 p-6">
4
+            <div class="flex items-center justify-between mb-4">
5
+                <div>
6
+                    {{ $this->form }}
7
+                </div>
8
+                <div class="text-right">
9
+                    <div class="text-sm text-gray-500">Total Selected:</div>
10
+                    <div class="text-xl font-semibold text-gray-900" id="total-amount">{{ $this->totalSelectedPaymentAmount }}</div>
11
+                </div>
12
+            </div>
13
+        </div>
14
+
15
+        {{ $this->table }}
16
+    </div>
17
+
18
+    <script>
19
+        // Update total when table state changes
20
+        document.addEventListener('livewire:navigated', function() {
21
+            updateTotal();
22
+        });
23
+
24
+        function updateTotal() {
25
+            // This would need to be implemented to calculate total from selected bills
26
+            // For now, it's a placeholder
27
+        }
28
+    </script>
29
+</x-filament-panels::page>

Loading…
İptal
Kaydet