Преглед на файлове

wip Recurring Invoices

3.x
Andrew Wallo преди 9 месеца
родител
ревизия
757edf0e6c

+ 20
- 0
app/Enums/Accounting/IntervalType.php Целия файл

@@ -24,6 +24,26 @@ enum IntervalType: string implements HasLabel
24 24
         };
25 25
     }
26 26
 
27
+    public function getSingularLabel(): ?string
28
+    {
29
+        return match ($this) {
30
+            self::Day => 'Day',
31
+            self::Week => 'Week',
32
+            self::Month => 'Month',
33
+            self::Year => 'Year',
34
+        };
35
+    }
36
+
37
+    public function getPluralLabel(): ?string
38
+    {
39
+        return match ($this) {
40
+            self::Day => 'Days',
41
+            self::Week => 'Weeks',
42
+            self::Month => 'Months',
43
+            self::Year => 'Years',
44
+        };
45
+    }
46
+
27 47
     public function isDay(): bool
28 48
     {
29 49
         return $this === self::Day;

+ 10
- 0
app/Filament/Company/Resources/Sales/RecurringInvoiceResource.php Целия файл

@@ -283,12 +283,22 @@ class RecurringInvoiceResource extends Resource
283 283
                 Tables\Columns\TextColumn::make('client.name')
284 284
                     ->sortable()
285 285
                     ->searchable(),
286
+                Tables\Columns\TextColumn::make('schedule')
287
+                    ->label('Schedule')
288
+                    ->getStateUsing(function (RecurringInvoice $record) {
289
+                        return $record->getScheduleDescription();
290
+                    })
291
+                    ->description(function (RecurringInvoice $record) {
292
+                        return $record->getTimelineDescription();
293
+                    }),
286 294
                 Tables\Columns\TextColumn::make('last_date')
287 295
                     ->label('Last Invoice')
296
+                    ->date()
288 297
                     ->sortable()
289 298
                     ->searchable(),
290 299
                 Tables\Columns\TextColumn::make('next_date')
291 300
                     ->label('Next Invoice')
301
+                    ->date()
292 302
                     ->sortable()
293 303
                     ->searchable(),
294 304
                 Tables\Columns\TextColumn::make('total')

+ 3
- 1
app/Filament/Company/Resources/Sales/RecurringInvoiceResource/Pages/ViewRecurringInvoice.php Целия файл

@@ -296,7 +296,7 @@ class ViewRecurringInvoice extends ViewRecord
296 296
                     ])
297 297
                     ->headerActions([
298 298
                         Forms\Components\Actions\Action::make('save')
299
-                            ->label('Save')
299
+                            ->label('Submit and Approve')
300 300
                             ->button()
301 301
                             ->successNotificationTitle('Scheduling saved')
302 302
                             ->action(function (Forms\Components\Actions\Action $action) {
@@ -313,5 +313,7 @@ class ViewRecurringInvoice extends ViewRecord
313 313
         $state = $this->form->getState();
314 314
 
315 315
         $this->getRecord()->update($state);
316
+
317
+        $this->getRecord()->markAsApproved();
316 318
     }
317 319
 }

+ 156
- 0
app/Models/Accounting/RecurringInvoice.php Целия файл

@@ -19,14 +19,18 @@ use App\Enums\Accounting\RecurringInvoiceStatus;
19 19
 use App\Enums\Setting\PaymentTerms;
20 20
 use App\Models\Common\Client;
21 21
 use App\Models\Setting\Currency;
22
+use App\Observers\RecurringInvoiceObserver;
22 23
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
24
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
23 25
 use Illuminate\Database\Eloquent\Factories\HasFactory;
24 26
 use Illuminate\Database\Eloquent\Model;
25 27
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
26 28
 use Illuminate\Database\Eloquent\Relations\HasMany;
27 29
 use Illuminate\Database\Eloquent\Relations\MorphMany;
30
+use Illuminate\Support\Carbon;
28 31
 
29 32
 #[CollectedBy(DocumentCollection::class)]
33
+#[ObservedBy(RecurringInvoiceObserver::class)]
30 34
 class RecurringInvoice extends Model
31 35
 {
32 36
     use Blamable;
@@ -161,4 +165,156 @@ class RecurringInvoice extends Model
161 165
     {
162 166
         return $this->lineItems()->exists();
163 167
     }
168
+
169
+    public function getScheduleDescription(): string
170
+    {
171
+        $frequency = $this->frequency;
172
+
173
+        return match (true) {
174
+            $frequency->isDaily() => 'Repeat daily',
175
+
176
+            $frequency->isWeekly() && $this->day_of_week => "Repeat weekly every {$this->day_of_week->getLabel()}",
177
+
178
+            $frequency->isMonthly() && $this->day_of_month => "Repeat monthly on the {$this->day_of_month->getLabel()} day",
179
+
180
+            $frequency->isYearly() && $this->month && $this->day_of_month => "Repeat yearly on {$this->month->getLabel()} {$this->day_of_month->getLabel()}",
181
+
182
+            $frequency->isCustom() => $this->getCustomScheduleDescription(),
183
+
184
+            default => 'Schedule not configured'
185
+        };
186
+    }
187
+
188
+    private function getCustomScheduleDescription(): string
189
+    {
190
+        $interval = $this->interval_value > 1
191
+            ? "{$this->interval_value} {$this->interval_type->getPluralLabel()}"
192
+            : $this->interval_type->getSingularLabel();
193
+
194
+        $dayDescription = match (true) {
195
+            $this->interval_type->isWeek() && $this->day_of_week => " on {$this->day_of_week->getLabel()}",
196
+
197
+            $this->interval_type->isMonth() && $this->day_of_month => " on the {$this->day_of_month->getLabel()} day",
198
+
199
+            $this->interval_type->isYear() && $this->month && $this->day_of_month => " on {$this->month->getLabel()} {$this->day_of_month->getLabel()}",
200
+
201
+            default => ''
202
+        };
203
+
204
+        return "Repeat every {$interval}{$dayDescription}";
205
+    }
206
+
207
+    /**
208
+     * Get a human-readable description of when the schedule ends.
209
+     */
210
+    public function getEndDescription(): string
211
+    {
212
+        if (! $this->end_type) {
213
+            return 'Not configured';
214
+        }
215
+
216
+        return match (true) {
217
+            $this->end_type->isNever() => 'Never',
218
+
219
+            $this->end_type->isAfter() && $this->max_occurrences => "After {$this->max_occurrences} " . str($this->max_occurrences === 1 ? 'invoice' : 'invoices'),
220
+
221
+            $this->end_type->isOn() && $this->end_date => 'On ' . $this->end_date->toDefaultDateFormat(),
222
+
223
+            default => 'Not configured'
224
+        };
225
+    }
226
+
227
+    /**
228
+     * Get the schedule timeline description.
229
+     */
230
+    public function getTimelineDescription(): string
231
+    {
232
+        $parts = [];
233
+
234
+        if ($this->start_date) {
235
+            $parts[] = 'First Invoice: ' . $this->start_date->toDefaultDateFormat();
236
+        }
237
+
238
+        if ($this->end_type) {
239
+            $parts[] = 'Ends: ' . $this->getEndDescription();
240
+        }
241
+
242
+        return implode(', ', $parts);
243
+    }
244
+
245
+    /**
246
+     * Get next occurrence date based on the schedule.
247
+     */
248
+    public function calculateNextDate(): ?\Carbon\Carbon
249
+    {
250
+        $lastDate = $this->last_date ?? $this->start_date;
251
+        if (! $lastDate) {
252
+            return null;
253
+        }
254
+
255
+        $nextDate = match (true) {
256
+            $this->frequency->isDaily() => $lastDate->addDay(),
257
+
258
+            $this->frequency->isWeekly() => $lastDate->addWeek(),
259
+
260
+            $this->frequency->isMonthly() => $lastDate->addMonth(),
261
+
262
+            $this->frequency->isYearly() => $lastDate->addYear(),
263
+
264
+            $this->frequency->isCustom() => $this->calculateCustomNextDate($lastDate),
265
+
266
+            default => null
267
+        };
268
+
269
+        // Check if we've reached the end
270
+        if ($this->hasReachedEnd($nextDate)) {
271
+            return null;
272
+        }
273
+
274
+        return $nextDate;
275
+    }
276
+
277
+    /**
278
+     * Calculate next date for custom intervals
279
+     */
280
+    protected function calculateCustomNextDate(Carbon $lastDate): ?\Carbon\Carbon
281
+    {
282
+        $value = $this->interval_value ?? 1;
283
+
284
+        return match ($this->interval_type) {
285
+            IntervalType::Day => $lastDate->addDays($value),
286
+            IntervalType::Week => $lastDate->addWeeks($value),
287
+            IntervalType::Month => $lastDate->addMonths($value),
288
+            IntervalType::Year => $lastDate->addYears($value),
289
+            default => null
290
+        };
291
+    }
292
+
293
+    /**
294
+     * Check if the schedule has reached its end
295
+     */
296
+    public function hasReachedEnd(?Carbon $nextDate = null): bool
297
+    {
298
+        if (! $this->end_type) {
299
+            return false;
300
+        }
301
+
302
+        return match (true) {
303
+            $this->end_type->isNever() => false,
304
+
305
+            $this->end_type->isAfter() => ($this->occurrences_count ?? 0) >= ($this->max_occurrences ?? 0),
306
+
307
+            $this->end_type->isOn() && $this->end_date && $nextDate => $nextDate->greaterThan($this->end_date),
308
+
309
+            default => false
310
+        };
311
+    }
312
+
313
+    public function markAsApproved(): void
314
+    {
315
+        $this->update([
316
+            'approved_at' => now(),
317
+            'status' => RecurringInvoiceStatus::Active,
318
+        ]);
319
+    }
164 320
 }

+ 26
- 0
app/Observers/RecurringInvoiceObserver.php Целия файл

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace App\Observers;
4
+
5
+use App\Models\Accounting\RecurringInvoice;
6
+
7
+class RecurringInvoiceObserver
8
+{
9
+    /**
10
+     * Handle the RecurringInvoice "updated" event.
11
+     */
12
+    public function updated(RecurringInvoice $recurringInvoice): void
13
+    {
14
+        $recurringInvoice->updateQuietly([
15
+            'next_date' => $recurringInvoice->calculateNextDate(),
16
+        ]);
17
+    }
18
+
19
+    /**
20
+     * Handle the RecurringInvoice "deleted" event.
21
+     */
22
+    public function deleted(RecurringInvoice $recurringInvoice): void
23
+    {
24
+        //
25
+    }
26
+}

+ 2
- 1
resources/data/lang/en.json Целия файл

@@ -218,5 +218,6 @@
218 218
     "Estimate Details": "Estimate Details",
219 219
     "Estimate Footer": "Estimate Footer",
220 220
     "Scheduling": "Scheduling",
221
-    "Scheduling Form": "Scheduling Form"
221
+    "Scheduling Form": "Scheduling Form",
222
+    "Approve": "Approve"
222 223
 }

Loading…
Отказ
Запис