Ver código fonte

wip Recurring Invoices

3.x
Andrew Wallo 9 meses atrás
pai
commit
a38d632eea

+ 19
- 4
app/Enums/Accounting/DocumentType.php Ver arquivo

@@ -10,18 +10,22 @@ enum DocumentType: string implements HasIcon, HasLabel
10 10
     case Invoice = 'invoice';
11 11
     case Bill = 'bill';
12 12
     case Estimate = 'estimate';
13
+    case RecurringInvoice = 'recurring_invoice';
13 14
 
14 15
     public const DEFAULT = self::Invoice->value;
15 16
 
16 17
     public function getLabel(): ?string
17 18
     {
18
-        return $this->name;
19
+        return match ($this) {
20
+            self::Invoice, self::Bill, self::Estimate => $this->name,
21
+            self::RecurringInvoice => 'Recurring Invoice',
22
+        };
19 23
     }
20 24
 
21 25
     public function getIcon(): ?string
22 26
     {
23 27
         return match ($this->value) {
24
-            self::Invoice->value => 'heroicon-o-document-duplicate',
28
+            self::Invoice->value, self::RecurringInvoice->value => 'heroicon-o-document-duplicate',
25 29
             self::Bill->value => 'heroicon-o-clipboard-document-list',
26 30
             self::Estimate->value => 'heroicon-o-document-text',
27 31
         };
@@ -30,7 +34,7 @@ enum DocumentType: string implements HasIcon, HasLabel
30 34
     public function getTaxKey(): string
31 35
     {
32 36
         return match ($this) {
33
-            self::Invoice, self::Estimate => 'salesTaxes',
37
+            self::Invoice, self::RecurringInvoice, self::Estimate => 'salesTaxes',
34 38
             self::Bill => 'purchaseTaxes',
35 39
         };
36 40
     }
@@ -38,7 +42,7 @@ enum DocumentType: string implements HasIcon, HasLabel
38 42
     public function getDiscountKey(): string
39 43
     {
40 44
         return match ($this) {
41
-            self::Invoice, self::Estimate => 'salesDiscounts',
45
+            self::Invoice, self::RecurringInvoice, self::Estimate => 'salesDiscounts',
42 46
             self::Bill => 'purchaseDiscounts',
43 47
         };
44 48
     }
@@ -52,6 +56,15 @@ enum DocumentType: string implements HasIcon, HasLabel
52 56
                 'reference_number' => 'P.O/S.O Number',
53 57
                 'date' => 'Invoice Date',
54 58
                 'due_date' => 'Payment Due',
59
+                'amount_due' => 'Amount Due',
60
+            ],
61
+            self::RecurringInvoice => [
62
+                'title' => 'Recurring Invoice',
63
+                'number' => 'Invoice Number',
64
+                'reference_number' => 'P.O/S.O Number',
65
+                'date' => 'Invoice Date',
66
+                'due_date' => 'Payment Due',
67
+                'amount_due' => 'Amount Due',
55 68
             ],
56 69
             self::Estimate => [
57 70
                 'title' => 'Estimate',
@@ -59,6 +72,7 @@ enum DocumentType: string implements HasIcon, HasLabel
59 72
                 'reference_number' => 'Reference Number',
60 73
                 'date' => 'Estimate Date',
61 74
                 'due_date' => 'Expiration Date',
75
+                'amount_due' => 'Grand Total',
62 76
             ],
63 77
             self::Bill => [
64 78
                 'title' => 'Bill',
@@ -66,6 +80,7 @@ enum DocumentType: string implements HasIcon, HasLabel
66 80
                 'reference_number' => 'P.O/S.O Number',
67 81
                 'date' => 'Bill Date',
68 82
                 'due_date' => 'Payment Due',
83
+                'amount_due' => 'Amount Due',
69 84
             ],
70 85
         };
71 86
     }

+ 1
- 0
app/Filament/Company/Resources/Sales/RecurringInvoiceResource.php Ver arquivo

@@ -320,6 +320,7 @@ class RecurringInvoiceResource extends Resource
320 320
                     Tables\Actions\EditAction::make(),
321 321
                     Tables\Actions\ViewAction::make(),
322 322
                     Tables\Actions\DeleteAction::make(),
323
+                    RecurringInvoice::getUpdateScheduleAction(Tables\Actions\Action::class),
323 324
                 ]),
324 325
             ])
325 326
             ->bulkActions([

+ 93
- 286
app/Filament/Company/Resources/Sales/RecurringInvoiceResource/Pages/ViewRecurringInvoice.php Ver arquivo

@@ -3,20 +3,23 @@
3 3
 namespace App\Filament\Company\Resources\Sales\RecurringInvoiceResource\Pages;
4 4
 
5 5
 use App\Enums\Accounting\DayOfMonth;
6
-use App\Enums\Accounting\DayOfWeek;
7
-use App\Enums\Accounting\EndType;
8
-use App\Enums\Accounting\Frequency;
9
-use App\Enums\Accounting\IntervalType;
10
-use App\Enums\Accounting\Month;
6
+use App\Enums\Accounting\DocumentType;
7
+use App\Filament\Company\Resources\Sales\ClientResource;
11 8
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource;
12
-use App\Filament\Forms\Components\LabeledField;
13
-use App\Models\Setting\CompanyProfile;
14
-use App\Utilities\Localization\Timezone;
15
-use Filament\Forms;
16
-use Filament\Forms\Form;
9
+use App\Filament\Infolists\Components\DocumentPreview;
10
+use App\Models\Accounting\RecurringInvoice;
11
+use CodeWithDennis\SimpleAlert\Components\Infolists\SimpleAlert;
12
+use Filament\Actions;
13
+use Filament\Infolists\Components\Actions\Action;
14
+use Filament\Infolists\Components\Grid;
15
+use Filament\Infolists\Components\Section;
16
+use Filament\Infolists\Components\TextEntry;
17
+use Filament\Infolists\Infolist;
17 18
 use Filament\Resources\Pages\ViewRecord;
19
+use Filament\Support\Enums\FontWeight;
20
+use Filament\Support\Enums\IconPosition;
21
+use Filament\Support\Enums\IconSize;
18 22
 use Filament\Support\Enums\MaxWidth;
19
-use Guava\FilamentClusters\Forms\Cluster;
20 23
 
21 24
 class ViewRecurringInvoice extends ViewRecord
22 25
 {
@@ -35,285 +38,89 @@ class ViewRecurringInvoice extends ViewRecord
35 38
         return MaxWidth::SixExtraLarge;
36 39
     }
37 40
 
38
-    public function form(Form $form): Form
41
+    protected function getHeaderActions(): array
39 42
     {
40
-        return $form
41
-            ->disabled(false)
43
+        return [
44
+            Actions\ActionGroup::make([
45
+                Actions\EditAction::make(),
46
+                Actions\DeleteAction::make(),
47
+                RecurringInvoice::getUpdateScheduleAction(),
48
+                RecurringInvoice::getApproveDraftAction(),
49
+            ])
50
+                ->label('Actions')
51
+                ->button()
52
+                ->outlined()
53
+                ->dropdownPlacement('bottom-end')
54
+                ->icon('heroicon-c-chevron-down')
55
+                ->iconSize(IconSize::Small)
56
+                ->iconPosition(IconPosition::After),
57
+        ];
58
+    }
59
+
60
+    public function infolist(Infolist $infolist): Infolist
61
+    {
62
+        return $infolist
42 63
             ->schema([
43
-                Forms\Components\Section::make('Scheduling')
64
+                SimpleAlert::make('scheduleIsNotSet')
65
+                    ->info()
66
+                    ->title('Schedule Not Set')
67
+                    ->description('The schedule for this recurring invoice has not been set. You must set a schedule before you can approve this draft and start creating invoices.')
68
+                    ->visible(fn (RecurringInvoice $record) => ! $record->hasSchedule())
69
+                    ->columnSpanFull()
70
+                    ->actions([
71
+                        RecurringInvoice::getUpdateScheduleAction(Action::class)
72
+                            ->outlined(),
73
+                    ]),
74
+                Section::make('Invoice Details')
75
+                    ->columns(4)
44 76
                     ->schema([
45
-                        Forms\Components\Group::make([
46
-                            Forms\Components\Select::make('frequency')
47
-                                ->label('Repeat this invoice')
48
-                                ->inlineLabel()
49
-                                ->options(Frequency::class)
50
-                                ->softRequired()
51
-                                ->live()
52
-                                ->afterStateUpdated(function (Forms\Set $set, $state) {
53
-                                    $frequency = Frequency::parse($state);
54
-
55
-                                    if ($frequency->isDaily()) {
56
-                                        $set('interval_value', null);
57
-                                        $set('interval_type', null);
58
-                                    }
59
-
60
-                                    if ($frequency->isWeekly()) {
61
-                                        $currentDayOfWeek = now()->dayOfWeek;
62
-                                        $currentDayOfWeek = DayOfWeek::parse($currentDayOfWeek);
63
-                                        $set('day_of_week', $currentDayOfWeek);
64
-                                        $set('interval_value', null);
65
-                                        $set('interval_type', null);
66
-                                    }
67
-
68
-                                    if ($frequency->isMonthly()) {
69
-                                        $set('day_of_month', DayOfMonth::First);
70
-                                        $set('interval_value', null);
71
-                                        $set('interval_type', null);
72
-                                    }
73
-
74
-                                    if ($frequency->isYearly()) {
75
-                                        $currentMonth = now()->month;
76
-                                        $currentMonth = Month::parse($currentMonth);
77
-                                        $set('month', $currentMonth);
78
-
79
-                                        $currentDay = now()->dayOfMonth;
80
-                                        $currentDay = DayOfMonth::parse($currentDay);
81
-                                        $set('day_of_month', $currentDay);
82
-
83
-                                        $set('interval_value', null);
84
-                                        $set('interval_type', null);
85
-                                    }
86
-
87
-                                    if ($frequency->isCustom()) {
88
-                                        $set('interval_value', 1);
89
-                                        $set('interval_type', IntervalType::Month);
90
-
91
-                                        $currentDay = now()->dayOfMonth;
92
-                                        $currentDay = DayOfMonth::parse($currentDay);
93
-                                        $set('day_of_month', $currentDay);
94
-                                    }
95
-                                }),
96
-
97
-                            // Custom frequency fields
98
-
99
-                            LabeledField::make()
100
-                                ->prefix('every')
101
-                                ->schema([
102
-                                    Cluster::make([
103
-                                        Forms\Components\TextInput::make('interval_value')
104
-                                            ->label('every')
105
-                                            ->numeric()
106
-                                            ->default(1),
107
-                                        Forms\Components\Select::make('interval_type')
108
-                                            ->label('Interval Type')
109
-                                            ->options(IntervalType::class)
110
-                                            ->softRequired()
111
-                                            ->default(IntervalType::Month)
112
-                                            ->live()
113
-                                            ->afterStateUpdated(function (Forms\Set $set, $state) {
114
-                                                $intervalType = IntervalType::parse($state);
115
-
116
-                                                if ($intervalType->isWeek()) {
117
-                                                    $currentDayOfWeek = now()->dayOfWeek;
118
-                                                    $currentDayOfWeek = DayOfWeek::parse($currentDayOfWeek);
119
-                                                    $set('day_of_week', $currentDayOfWeek);
120
-                                                }
121
-
122
-                                                if ($intervalType->isMonth()) {
123
-                                                    $currentDay = now()->dayOfMonth;
124
-                                                    $currentDay = DayOfMonth::parse($currentDay);
125
-                                                    $set('day_of_month', $currentDay);
126
-                                                }
127
-
128
-                                                if ($intervalType->isYear()) {
129
-                                                    $currentMonth = now()->month;
130
-                                                    $currentMonth = Month::parse($currentMonth);
131
-                                                    $set('month', $currentMonth);
132
-
133
-                                                    $currentDay = now()->dayOfMonth;
134
-                                                    $currentDay = DayOfMonth::parse($currentDay);
135
-                                                    $set('day_of_month', $currentDay);
136
-                                                }
137
-                                            }),
138
-                                    ])
139
-                                        ->live()
140
-                                        ->hiddenLabel(),
141
-                                ])
142
-                                ->visible(fn (Forms\Get $get) => Frequency::parse($get('frequency'))->isCustom()),
143
-
144
-                            LabeledField::make()
145
-                                ->prefix(function (Forms\Get $get) {
146
-                                    $frequency = Frequency::parse($get('frequency'));
147
-                                    $intervalType = IntervalType::parse($get('interval_type'));
148
-
149
-                                    if ($frequency->isYearly()) {
150
-                                        return 'every';
151
-                                    }
152
-
153
-                                    if ($frequency->isCustom() && $intervalType?->isYear()) {
154
-                                        return 'in';
155
-                                    }
156
-
157
-                                    return null;
158
-                                })
159
-                                ->schema([
160
-                                    Forms\Components\Select::make('month')
161
-                                        ->hiddenLabel()
162
-                                        ->options(Month::class)
163
-                                        ->live()
164
-                                        ->softRequired(),
165
-                                ])
166
-                                ->visible(fn (Forms\Get $get) => Frequency::parse($get('frequency'))->isYearly() || IntervalType::parse($get('interval_type'))?->isYear()),
167
-
168
-                            LabeledField::make()
169
-                                ->prefix('on the')
170
-                                ->suffix(function (Forms\Get $get) {
171
-                                    $frequency = Frequency::parse($get('frequency'));
172
-                                    $intervalType = IntervalType::parse($get('interval_type'));
173
-
174
-                                    if ($frequency->isMonthly()) {
175
-                                        return 'day of every month';
176
-                                    }
177
-
178
-                                    if ($frequency->isYearly() || ($frequency->isCustom() && $intervalType->isMonth()) || ($frequency->isCustom() && $intervalType->isYear())) {
179
-                                        return 'day of the month';
180
-                                    }
181
-
182
-                                    return null;
183
-                                })
184
-                                ->schema([
185
-                                    Forms\Components\Select::make('day_of_month')
186
-                                        ->hiddenLabel()
187
-                                        ->inlineLabel()
188
-                                        ->options(DayOfMonth::class)
189
-                                        ->live()
190
-                                        ->softRequired(),
191
-                                ])
192
-                                ->visible(fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isMonthly() || Frequency::parse($get('frequency'))?->isYearly() || IntervalType::parse($get('interval_type'))?->isMonth() || IntervalType::parse($get('interval_type'))?->isYear()),
193
-
194
-                            LabeledField::make()
195
-                                ->prefix(function (Forms\Get $get) {
196
-                                    $frequency = Frequency::parse($get('frequency'));
197
-                                    $intervalType = IntervalType::parse($get('interval_type'));
198
-
199
-                                    if ($frequency->isWeekly()) {
200
-                                        return 'every';
201
-                                    }
202
-
203
-                                    if ($frequency->isCustom() && $intervalType->isWeek()) {
204
-                                        return 'on';
205
-                                    }
206
-
207
-                                    return null;
208
-                                })
209
-                                ->schema([
210
-                                    Forms\Components\Select::make('day_of_week')
211
-                                        ->hiddenLabel()
212
-                                        ->options(DayOfWeek::class)
213
-                                        ->live()
214
-                                        ->softRequired(),
215
-                                ])
216
-                                ->visible(fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isWeekly() || IntervalType::parse($get('interval_type'))?->isWeek()),
217
-                        ])->columns(2),
218
-
219
-                        Forms\Components\Group::make([
220
-                            Forms\Components\DatePicker::make('start_date')
221
-                                ->label('Create first invoice on')
222
-                                ->inlineLabel()
223
-                                ->softRequired(),
224
-
225
-                            LabeledField::make()
226
-                                ->prefix('and end')
227
-                                ->suffix(function (Forms\Get $get) {
228
-                                    $endType = EndType::parse($get('end_type'));
229
-
230
-                                    if ($endType->isAfter()) {
231
-                                        return 'invoices';
232
-                                    }
233
-
234
-                                    return null;
235
-                                })
236
-                                ->schema(function (Forms\Get $get) {
237
-                                    $components = [];
238
-
239
-                                    $components[] = Forms\Components\Select::make('end_type')
240
-                                        ->hiddenLabel()
241
-                                        ->options(EndType::class)
242
-                                        ->softRequired()
243
-                                        ->live()
244
-                                        ->afterStateUpdated(function (Forms\Set $set, $state) {
245
-                                            $endType = EndType::parse($state);
246
-
247
-                                            if ($endType->isNever()) {
248
-                                                $set('max_occurrences', null);
249
-                                                $set('end_date', null);
250
-                                            }
251
-
252
-                                            if ($endType->isAfter()) {
253
-                                                $set('max_occurrences', 1);
254
-                                                $set('end_date', null);
255
-                                            }
256
-
257
-                                            if ($endType->isOn()) {
258
-                                                $set('max_occurrences', null);
259
-                                                $set('end_date', now()->addMonth()->startOfMonth());
260
-                                            }
261
-                                        });
262
-
263
-                                    $endType = EndType::parse($get('end_type'));
264
-
265
-                                    if ($endType->isAfter()) {
266
-                                        $components[] = Forms\Components\TextInput::make('max_occurrences')
267
-                                            ->numeric()
268
-                                            ->live();
269
-                                    }
270
-
271
-                                    if ($endType->isOn()) {
272
-                                        $components[] = Forms\Components\DatePicker::make('end_date')
273
-                                            ->live();
274
-                                    }
275
-
276
-                                    return [
277
-                                        Cluster::make($components)
278
-                                            ->hiddenLabel(),
279
-                                    ];
280
-                                }),
281
-                        ])->columns(2),
282
-
283
-                        Forms\Components\Group::make([
284
-                            LabeledField::make()
285
-                                ->prefix('Create in')
286
-                                ->suffix('time zone')
287
-                                ->schema([
288
-                                    Forms\Components\Select::make('timezone')
289
-                                        ->softRequired()
290
-                                        ->hiddenLabel()
291
-                                        ->options(Timezone::getTimezoneOptions(CompanyProfile::first()->country))
292
-                                        ->searchable(),
293
-                                ])
294
-                                ->columns(1),
295
-                        ])->columns(2),
296
-                    ])
297
-                    ->headerActions([
298
-                        Forms\Components\Actions\Action::make('save')
299
-                            ->label('Submit and Approve')
300
-                            ->button()
301
-                            ->successNotificationTitle('Scheduling saved')
302
-                            ->action(function (Forms\Components\Actions\Action $action) {
303
-                                $this->save();
304
-
305
-                                $action->success();
306
-                            }),
77
+                        Grid::make(1)
78
+                            ->schema([
79
+                                TextEntry::make('status')
80
+                                    ->badge(),
81
+                                TextEntry::make('client.name')
82
+                                    ->label('Client')
83
+                                    ->color('primary')
84
+                                    ->weight(FontWeight::SemiBold)
85
+                                    ->url(static fn (RecurringInvoice $record) => ClientResource::getUrl('edit', ['record' => $record->client_id])),
86
+                                TextEntry::make('last_date')
87
+                                    ->label('Last Invoice')
88
+                                    ->date()
89
+                                    ->placeholder('Not Created'),
90
+                                TextEntry::make('next_date')
91
+                                    ->label('Next Invoice')
92
+                                    ->placeholder('Not Scheduled')
93
+                                    ->date(),
94
+                                TextEntry::make('schedule')
95
+                                    ->label('Schedule')
96
+                                    ->getStateUsing(function (RecurringInvoice $record) {
97
+                                        return $record->getScheduleDescription();
98
+                                    })
99
+                                    ->helperText(function (RecurringInvoice $record) {
100
+                                        return $record->getTimelineDescription();
101
+                                    }),
102
+                                TextEntry::make('occurrences_count')
103
+                                    ->label('Invoices Created')
104
+                                    ->visible(fn (RecurringInvoice $record) => $record->occurrences_count > 0),
105
+                                TextEntry::make('end_date')
106
+                                    ->label('Ends On')
107
+                                    ->date()
108
+                                    ->visible(fn (RecurringInvoice $record) => $record->end_type?->isOn()),
109
+                                TextEntry::make('approved_at')
110
+                                    ->label('Approved At')
111
+                                    ->placeholder('Not Approved')
112
+                                    ->date(),
113
+                                TextEntry::make('ended_at')
114
+                                    ->label('Ended At')
115
+                                    ->date()
116
+                                    ->visible(fn (RecurringInvoice $record) => $record->ended_at),
117
+                                TextEntry::make('total')
118
+                                    ->label('Invoice Amount')
119
+                                    ->currency(static fn (RecurringInvoice $record) => $record->currency_code),
120
+                            ])->columnSpan(1),
121
+                        DocumentPreview::make()
122
+                            ->type(DocumentType::RecurringInvoice),
307 123
                     ]),
308 124
             ]);
309 125
     }
310
-
311
-    public function save(): void
312
-    {
313
-        $state = $this->form->getState();
314
-
315
-        $this->getRecord()->update($state);
316
-
317
-        $this->getRecord()->markAsApproved();
318
-    }
319 126
 }

+ 277
- 5
app/Models/Accounting/RecurringInvoice.php Ver arquivo

@@ -17,9 +17,18 @@ use App\Enums\Accounting\IntervalType;
17 17
 use App\Enums\Accounting\Month;
18 18
 use App\Enums\Accounting\RecurringInvoiceStatus;
19 19
 use App\Enums\Setting\PaymentTerms;
20
+use App\Filament\Forms\Components\CustomSection;
20 21
 use App\Models\Common\Client;
22
+use App\Models\Setting\CompanyProfile;
21 23
 use App\Models\Setting\Currency;
22 24
 use App\Observers\RecurringInvoiceObserver;
25
+use App\Utilities\Localization\Timezone;
26
+use Filament\Actions\Action;
27
+use Filament\Actions\MountableAction;
28
+use Filament\Forms;
29
+use Filament\Forms\Form;
30
+use Filament\Support\Enums\MaxWidth;
31
+use Guava\FilamentClusters\Forms\Cluster;
23 32
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
24 33
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
25 34
 use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -153,7 +162,7 @@ class RecurringInvoice extends Model
153 162
 
154 163
     public function canBeApproved(): bool
155 164
     {
156
-        return $this->isDraft() && ! $this->wasApproved();
165
+        return $this->isDraft() && $this->hasSchedule() && ! $this->wasApproved();
157 166
     }
158 167
 
159 168
     public function canBeEnded(): bool
@@ -166,6 +175,11 @@ class RecurringInvoice extends Model
166 175
         return $this->lineItems()->exists();
167 176
     }
168 177
 
178
+    public function hasSchedule(): bool
179
+    {
180
+        return $this->start_date !== null;
181
+    }
182
+
169 183
     public function getScheduleDescription(): string
170 184
     {
171 185
         $frequency = $this->frequency;
@@ -181,7 +195,7 @@ class RecurringInvoice extends Model
181 195
 
182 196
             $frequency->isCustom() => $this->getCustomScheduleDescription(),
183 197
 
184
-            default => 'Schedule not configured'
198
+            default => 'Not Configured',
185 199
         };
186 200
     }
187 201
 
@@ -245,7 +259,7 @@ class RecurringInvoice extends Model
245 259
     /**
246 260
      * Get next occurrence date based on the schedule.
247 261
      */
248
-    public function calculateNextDate(): ?\Carbon\Carbon
262
+    public function calculateNextDate(): ?Carbon
249 263
     {
250 264
         $lastDate = $this->last_date ?? $this->start_date;
251 265
         if (! $lastDate) {
@@ -274,6 +288,21 @@ class RecurringInvoice extends Model
274 288
         return $nextDate;
275 289
     }
276 290
 
291
+    public function calculateNextDueDate(): ?Carbon
292
+    {
293
+        $nextDate = $this->calculateNextDate();
294
+        if (! $nextDate) {
295
+            return null;
296
+        }
297
+
298
+        $terms = $this->payment_terms;
299
+        if (! $terms) {
300
+            return $nextDate;
301
+        }
302
+
303
+        return $nextDate->addDays($terms->getDays());
304
+    }
305
+
277 306
     /**
278 307
      * Calculate next date for custom intervals
279 308
      */
@@ -310,10 +339,253 @@ class RecurringInvoice extends Model
310 339
         };
311 340
     }
312 341
 
313
-    public function markAsApproved(): void
342
+    public static function getUpdateScheduleAction(string $action = Action::class): MountableAction
343
+    {
344
+        return $action::make('updateSchedule')
345
+            ->label(fn (self $record) => $record->hasSchedule() ? 'Update Schedule' : 'Set Schedule')
346
+            ->icon('heroicon-o-calendar-date-range')
347
+            ->slideOver()
348
+            ->modalWidth(MaxWidth::FiveExtraLarge)
349
+            ->successNotificationTitle('Schedule Updated')
350
+            ->mountUsing(function (self $record, Form $form) {
351
+                $data = $record->attributesToArray();
352
+
353
+                $data['day_of_month'] ??= DayOfMonth::First;
354
+                $data['start_date'] ??= now()->addMonth()->startOfMonth();
355
+
356
+                $form->fill($data);
357
+            })
358
+            ->form([
359
+                CustomSection::make('Frequency')
360
+                    ->contained(false)
361
+                    ->schema([
362
+                        Forms\Components\Select::make('frequency')
363
+                            ->label('Repeats')
364
+                            ->options(Frequency::class)
365
+                            ->softRequired()
366
+                            ->live()
367
+                            ->afterStateUpdated(function (Forms\Set $set, $state) {
368
+                                $frequency = Frequency::parse($state);
369
+
370
+                                if ($frequency->isDaily()) {
371
+                                    $set('interval_value', null);
372
+                                    $set('interval_type', null);
373
+                                }
374
+
375
+                                if ($frequency->isWeekly()) {
376
+                                    $currentDayOfWeek = now()->dayOfWeek;
377
+                                    $currentDayOfWeek = DayOfWeek::parse($currentDayOfWeek);
378
+                                    $set('day_of_week', $currentDayOfWeek);
379
+                                    $set('interval_value', null);
380
+                                    $set('interval_type', null);
381
+                                }
382
+
383
+                                if ($frequency->isMonthly()) {
384
+                                    $set('day_of_month', DayOfMonth::First);
385
+                                    $set('interval_value', null);
386
+                                    $set('interval_type', null);
387
+                                }
388
+
389
+                                if ($frequency->isYearly()) {
390
+                                    $currentMonth = now()->month;
391
+                                    $currentMonth = Month::parse($currentMonth);
392
+                                    $set('month', $currentMonth);
393
+
394
+                                    $currentDay = now()->dayOfMonth;
395
+                                    $currentDay = DayOfMonth::parse($currentDay);
396
+                                    $set('day_of_month', $currentDay);
397
+
398
+                                    $set('interval_value', null);
399
+                                    $set('interval_type', null);
400
+                                }
401
+
402
+                                if ($frequency->isCustom()) {
403
+                                    $set('interval_value', 1);
404
+                                    $set('interval_type', IntervalType::Month);
405
+
406
+                                    $currentDay = now()->dayOfMonth;
407
+                                    $currentDay = DayOfMonth::parse($currentDay);
408
+                                    $set('day_of_month', $currentDay);
409
+                                }
410
+                            }),
411
+
412
+                        // Custom frequency fields in a nested grid
413
+                        Cluster::make([
414
+                            Forms\Components\TextInput::make('interval_value')
415
+                                ->label('every')
416
+                                ->softRequired()
417
+                                ->numeric()
418
+                                ->default(1),
419
+                            Forms\Components\Select::make('interval_type')
420
+                                ->label('Interval Type')
421
+                                ->options(IntervalType::class)
422
+                                ->softRequired()
423
+                                ->default(IntervalType::Month)
424
+                                ->live()
425
+                                ->afterStateUpdated(function (Forms\Set $set, $state) {
426
+                                    $intervalType = IntervalType::parse($state);
427
+
428
+                                    if ($intervalType->isWeek()) {
429
+                                        $currentDayOfWeek = now()->dayOfWeek;
430
+                                        $currentDayOfWeek = DayOfWeek::parse($currentDayOfWeek);
431
+                                        $set('day_of_week', $currentDayOfWeek);
432
+                                    }
433
+
434
+                                    if ($intervalType->isMonth()) {
435
+                                        $currentDay = now()->dayOfMonth;
436
+                                        $currentDay = DayOfMonth::parse($currentDay);
437
+                                        $set('day_of_month', $currentDay);
438
+                                    }
439
+
440
+                                    if ($intervalType->isYear()) {
441
+                                        $currentMonth = now()->month;
442
+                                        $currentMonth = Month::parse($currentMonth);
443
+                                        $set('month', $currentMonth);
444
+
445
+                                        $currentDay = now()->dayOfMonth;
446
+                                        $currentDay = DayOfMonth::parse($currentDay);
447
+                                        $set('day_of_month', $currentDay);
448
+                                    }
449
+                                }),
450
+                        ])
451
+                            ->live()
452
+                            ->label('Interval')
453
+                            ->required()
454
+                            ->markAsRequired(false)
455
+                            ->visible(fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isCustom()),
456
+
457
+                        // Specific schedule details
458
+                        Forms\Components\Select::make('month')
459
+                            ->label('Month')
460
+                            ->options(Month::class)
461
+                            ->softRequired()
462
+                            ->visible(
463
+                                fn (Forms\Get $get) => Frequency::parse($get('frequency'))->isYearly() ||
464
+                                IntervalType::parse($get('interval_type'))?->isYear()
465
+                            ),
466
+
467
+                        Forms\Components\Select::make('day_of_month')
468
+                            ->label('Day of Month')
469
+                            ->options(DayOfMonth::class)
470
+                            ->softRequired()
471
+                            ->visible(
472
+                                fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isMonthly() ||
473
+                                Frequency::parse($get('frequency'))?->isYearly() ||
474
+                                IntervalType::parse($get('interval_type'))?->isMonth() ||
475
+                                IntervalType::parse($get('interval_type'))?->isYear()
476
+                            ),
477
+
478
+                        Forms\Components\Select::make('day_of_week')
479
+                            ->label('Day of Week')
480
+                            ->options(DayOfWeek::class)
481
+                            ->softRequired()
482
+                            ->visible(
483
+                                fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isWeekly() ||
484
+                                IntervalType::parse($get('interval_type'))?->isWeek()
485
+                            ),
486
+                    ])->columns(2),
487
+
488
+                CustomSection::make('Dates')
489
+                    ->contained(false)
490
+                    ->schema([
491
+                        Forms\Components\DatePicker::make('start_date')
492
+                            ->label('Create First Invoice')
493
+                            ->softRequired(),
494
+
495
+                        Forms\Components\Group::make(function (Forms\Get $get) {
496
+                            $components = [];
497
+
498
+                            $components[] = Forms\Components\Select::make('end_type')
499
+                                ->label('End Schedule')
500
+                                ->options(EndType::class)
501
+                                ->softRequired()
502
+                                ->live()
503
+                                ->afterStateUpdated(function (Forms\Set $set, $state) {
504
+                                    $endType = EndType::parse($state);
505
+
506
+                                    if ($endType?->isNever()) {
507
+                                        $set('max_occurrences', null);
508
+                                        $set('end_date', null);
509
+                                    }
510
+
511
+                                    if ($endType?->isAfter()) {
512
+                                        $set('max_occurrences', 1);
513
+                                        $set('end_date', null);
514
+                                    }
515
+
516
+                                    if ($endType?->isOn()) {
517
+                                        $set('max_occurrences', null);
518
+                                        $set('end_date', now()->addMonth()->startOfMonth());
519
+                                    }
520
+                                });
521
+
522
+                            $endType = EndType::parse($get('end_type'));
523
+
524
+                            if ($endType?->isAfter()) {
525
+                                $components[] = Forms\Components\TextInput::make('max_occurrences')
526
+                                    ->numeric()
527
+                                    ->live();
528
+                            }
529
+
530
+                            if ($endType?->isOn()) {
531
+                                $components[] = Forms\Components\DatePicker::make('end_date')
532
+                                    ->live();
533
+                            }
534
+
535
+                            return [
536
+                                Cluster::make($components)
537
+                                    ->label('Ends')
538
+                                    ->required()
539
+                                    ->markAsRequired(false),
540
+                            ];
541
+                        }),
542
+                    ])
543
+                    ->columns(2),
544
+
545
+                CustomSection::make('Time Zone')
546
+                    ->contained(false)
547
+                    ->columns(2)
548
+                    ->schema([
549
+                        Forms\Components\Select::make('timezone')
550
+                            ->options(Timezone::getTimezoneOptions(CompanyProfile::first()->country))
551
+                            ->searchable()
552
+                            ->softRequired(),
553
+                    ]),
554
+            ])
555
+            ->action(function (self $record, array $data, MountableAction $action) {
556
+                $record->update($data);
557
+
558
+                $action->success();
559
+            });
560
+    }
561
+
562
+    public static function getApproveDraftAction(string $action = Action::class): MountableAction
563
+    {
564
+        return $action::make('approveDraft')
565
+            ->label('Approve')
566
+            ->icon('heroicon-o-check-circle')
567
+            ->visible(function (self $record) {
568
+                return $record->canBeApproved();
569
+            })
570
+            ->databaseTransaction()
571
+            ->successNotificationTitle('Recurring Invoice Approved')
572
+            ->action(function (self $record, MountableAction $action) {
573
+                $record->approveDraft();
574
+
575
+                $action->success();
576
+            });
577
+    }
578
+
579
+    public function approveDraft(?Carbon $approvedAt = null): void
314 580
     {
581
+        if (! $this->isDraft()) {
582
+            throw new \RuntimeException('Invoice is not in draft status.');
583
+        }
584
+
585
+        $approvedAt ??= now();
586
+
315 587
         $this->update([
316
-            'approved_at' => now(),
588
+            'approved_at' => $approvedAt,
317 589
             'status' => RecurringInvoiceStatus::Active,
318 590
         ]);
319 591
     }

+ 11
- 4
app/View/Models/DocumentPreviewViewModel.php Ver arquivo

@@ -70,11 +70,18 @@ class DocumentPreviewViewModel
70 70
 
71 71
     private function getDocumentMetadata(): array
72 72
     {
73
+        $number = match ($this->documentType) {
74
+            DocumentType::Invoice => $this->document->invoice_number,
75
+            DocumentType::RecurringInvoice => 'Auto-generated',
76
+            DocumentType::Bill => $this->document->bill_number,
77
+            DocumentType::Estimate => $this->document->estimate_number,
78
+        };
79
+
73 80
         return [
74
-            'number' => $this->document->invoice_number ?? $this->document->estimate_number,
81
+            'number' => $number,
75 82
             'reference_number' => $this->document->order_number ?? $this->document->reference_number,
76
-            'date' => $this->document->date?->toDefaultDateFormat(),
77
-            'due_date' => $this->document->due_date?->toDefaultDateFormat() ?? $this->document->expiration_date?->toDefaultDateFormat(),
83
+            'date' => $this->document->date?->toDefaultDateFormat() ?? $this->document->calculateNextDate()?->toDefaultDateFormat(),
84
+            'due_date' => $this->document->due_date?->toDefaultDateFormat() ?? $this->document->expiration_date?->toDefaultDateFormat() ?? $this->document->calculateNextDueDate()?->toDefaultDateFormat(),
78 85
             'currency_code' => $this->document->currency_code ?? CurrencyAccessor::getDefaultCurrency(),
79 86
         ];
80 87
     }
@@ -101,7 +108,7 @@ class DocumentPreviewViewModel
101 108
             'discount' => CurrencyConverter::formatToMoney($this->document->discount_total, $currencyCode),
102 109
             'tax' => CurrencyConverter::formatToMoney($this->document->tax_total, $currencyCode),
103 110
             'total' => CurrencyConverter::formatToMoney($this->document->total, $currencyCode),
104
-            'amount_due' => $this->document->amount_due ? CurrencyConverter::formatToMoney($this->document->amount_due, $currencyCode) : null,
111
+            'amount_due' => $this->document->amount_due ? CurrencyConverter::formatToMoney($this->document->amount_due, $currencyCode) : CurrencyConverter::formatToMoney($this->document->total, $currencyCode),
105 112
         ];
106 113
     }
107 114
 

+ 1
- 0
composer.json Ver arquivo

@@ -17,6 +17,7 @@
17 17
         "andrewdwallo/transmatic": "^1.1",
18 18
         "awcodes/filament-table-repeater": "^3.0",
19 19
         "barryvdh/laravel-snappy": "^1.0",
20
+        "codewithdennis/filament-simple-alert": "^3.0",
20 21
         "filament/filament": "^3.2",
21 22
         "guava/filament-clusters": "^1.1",
22 23
         "guzzlehttp/guzzle": "^7.8",

+ 80
- 7
composer.lock Ver arquivo

@@ -4,7 +4,7 @@
4 4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 5
         "This file is @generated automatically"
6 6
     ],
7
-    "content-hash": "a04479d57fe01a2694ad92a8901363ea",
7
+    "content-hash": "095bb4040f9910ddd128bd53c0670a55",
8 8
     "packages": [
9 9
         {
10 10
             "name": "akaunting/laravel-money",
@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.336.7",
500
+            "version": "3.336.8",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "3ebc383239f93d6f1e74573112c9d179070d2620"
504
+                "reference": "933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3ebc383239f93d6f1e74573112c9d179070d2620",
509
-                "reference": "3ebc383239f93d6f1e74573112c9d179070d2620",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6",
509
+                "reference": "933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -589,9 +589,9 @@
589 589
             "support": {
590 590
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
591 591
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
592
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.336.7"
592
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.336.8"
593 593
             },
594
-            "time": "2025-01-02T19:07:47+00:00"
594
+            "time": "2025-01-03T19:06:11+00:00"
595 595
         },
596 596
         {
597 597
             "name": "aws/aws-sdk-php-laravel",
@@ -1027,6 +1027,79 @@
1027 1027
             ],
1028 1028
             "time": "2024-02-09T16:56:22+00:00"
1029 1029
         },
1030
+        {
1031
+            "name": "codewithdennis/filament-simple-alert",
1032
+            "version": "v3.0.15",
1033
+            "source": {
1034
+                "type": "git",
1035
+                "url": "https://github.com/CodeWithDennis/filament-simple-alert.git",
1036
+                "reference": "8fd7dfa48bb98061bcc3f5fbaacb82dce7a09c7b"
1037
+            },
1038
+            "dist": {
1039
+                "type": "zip",
1040
+                "url": "https://api.github.com/repos/CodeWithDennis/filament-simple-alert/zipball/8fd7dfa48bb98061bcc3f5fbaacb82dce7a09c7b",
1041
+                "reference": "8fd7dfa48bb98061bcc3f5fbaacb82dce7a09c7b",
1042
+                "shasum": ""
1043
+            },
1044
+            "require": {
1045
+                "filament/filament": "^3.0",
1046
+                "php": "^8.1",
1047
+                "spatie/laravel-package-tools": "^1.15.0"
1048
+            },
1049
+            "require-dev": {
1050
+                "laravel/pint": "^1.16",
1051
+                "nunomaduro/collision": "^7.9",
1052
+                "orchestra/testbench": "^8.0",
1053
+                "pestphp/pest": "^2.1",
1054
+                "pestphp/pest-plugin-arch": "^2.0",
1055
+                "pestphp/pest-plugin-laravel": "^2.0"
1056
+            },
1057
+            "type": "library",
1058
+            "extra": {
1059
+                "laravel": {
1060
+                    "aliases": {
1061
+                        "SimpleAlert": "CodeWithDennis\\SimpleAlert\\Facades\\SimpleAlert"
1062
+                    },
1063
+                    "providers": [
1064
+                        "CodeWithDennis\\SimpleAlert\\SimpleAlertServiceProvider"
1065
+                    ]
1066
+                }
1067
+            },
1068
+            "autoload": {
1069
+                "psr-4": {
1070
+                    "CodeWithDennis\\SimpleAlert\\": "src/",
1071
+                    "CodeWithDennis\\SimpleAlert\\Database\\Factories\\": "database/factories/"
1072
+                }
1073
+            },
1074
+            "notification-url": "https://packagist.org/downloads/",
1075
+            "license": [
1076
+                "MIT"
1077
+            ],
1078
+            "authors": [
1079
+                {
1080
+                    "name": "CodeWithDennis",
1081
+                    "role": "Developer"
1082
+                }
1083
+            ],
1084
+            "description": "A plugin for adding straightforward alerts to your filament pages",
1085
+            "homepage": "https://github.com/codewithdennis/filament-simple-alert",
1086
+            "keywords": [
1087
+                "CodeWithDennis",
1088
+                "filament-simple-alert",
1089
+                "laravel"
1090
+            ],
1091
+            "support": {
1092
+                "issues": "https://github.com/codewithdennis/filament-simple-alert/issues",
1093
+                "source": "https://github.com/codewithdennis/filament-simple-alert"
1094
+            },
1095
+            "funding": [
1096
+                {
1097
+                    "url": "https://github.com/CodeWithDennis",
1098
+                    "type": "github"
1099
+                }
1100
+            ],
1101
+            "time": "2024-12-03T16:17:47+00:00"
1102
+        },
1030 1103
         {
1031 1104
             "name": "danharrin/date-format-converter",
1032 1105
             "version": "v0.3.1",

+ 1
- 0
resources/css/filament/company/tailwind.config.js Ver arquivo

@@ -13,6 +13,7 @@ export default {
13 13
         './vendor/andrewdwallo/filament-selectify/resources/views/**/*.blade.php',
14 14
         './vendor/awcodes/filament-table-repeater/resources/**/*.blade.php',
15 15
         './vendor/jaocero/radio-deck/resources/views/**/*.blade.php',
16
+        './vendor/codewithdennis/filament-simple-alert/resources/**/*.blade.php',
16 17
     ],
17 18
     theme: {
18 19
         extend: {

+ 4
- 1
resources/data/lang/en.json Ver arquivo

@@ -219,5 +219,8 @@
219 219
     "Estimate Footer": "Estimate Footer",
220 220
     "Scheduling": "Scheduling",
221 221
     "Scheduling Form": "Scheduling Form",
222
-    "Approve": "Approve"
222
+    "Approve": "Approve",
223
+    "Frequency": "Frequency",
224
+    "Schedule Bounds": "Schedule Bounds",
225
+    "Time Zone": "Time Zone"
223 226
 }

+ 1
- 9
resources/views/filament/infolists/components/document-preview.blade.php Ver arquivo

@@ -145,19 +145,11 @@
145 145
                 @if($totals['amount_due'])
146 146
                     <tr>
147 147
                         <td class="pl-6 py-2" colspan="2"></td>
148
-                        <td class="text-right font-semibold border-t-4 border-double py-2">Amount Due
148
+                        <td class="text-right font-semibold border-t-4 border-double py-2">{{ $labels['amount_due'] }}
149 149
                             ({{ $metadata['currency_code'] }}):
150 150
                         </td>
151 151
                         <td class="text-right border-t-4 border-double pr-6 py-2">{{ $totals['amount_due'] }}</td>
152 152
                     </tr>
153
-                @else
154
-                    <tr>
155
-                        <td class="pl-6 py-2" colspan="2"></td>
156
-                        <td class="text-right font-semibold border-t-4 border-double py-2">Grand Total
157
-                            ({{ $metadata['currency_code'] }}):
158
-                        </td>
159
-                        <td class="text-right border-t-4 border-double pr-6 py-2">{{ $totals['total'] }}</td>
160
-                    </tr>
161 153
                 @endif
162 154
                 </tfoot>
163 155
             </table>

Carregando…
Cancelar
Salvar