|
@@ -21,7 +21,9 @@ use App\Filament\Forms\Components\CustomSection;
|
21
|
21
|
use App\Models\Common\Client;
|
22
|
22
|
use App\Models\Setting\CompanyProfile;
|
23
|
23
|
use App\Observers\RecurringInvoiceObserver;
|
|
24
|
+use App\Support\ScheduleHandler;
|
24
|
25
|
use App\Utilities\Localization\Timezone;
|
|
26
|
+use CodeWithDennis\SimpleAlert\Components\Forms\SimpleAlert;
|
25
|
27
|
use Filament\Actions\Action;
|
26
|
28
|
use Filament\Actions\MountableAction;
|
27
|
29
|
use Filament\Forms;
|
|
@@ -293,34 +295,17 @@ class RecurringInvoice extends Document
|
293
|
295
|
|
294
|
296
|
public function calculateNextWeeklyDate(Carbon $lastDate): ?Carbon
|
295
|
297
|
{
|
296
|
|
- return $lastDate->copy()->next($this->day_of_week->value);
|
|
298
|
+ return $lastDate->copy()->next($this->day_of_week->name);
|
297
|
299
|
}
|
298
|
300
|
|
299
|
301
|
public function calculateNextMonthlyDate(Carbon $lastDate): ?Carbon
|
300
|
302
|
{
|
301
|
|
- return match (true) {
|
302
|
|
- $lastDate->equalTo($this->start_date) => $lastDate->copy()->day(
|
303
|
|
- min($this->day_of_month->value, $lastDate->daysInMonth)
|
304
|
|
- ),
|
305
|
|
-
|
306
|
|
- default => $lastDate->copy()->addMonth()->day(
|
307
|
|
- min($this->day_of_month->value, $lastDate->copy()->addMonth()->daysInMonth)
|
308
|
|
- ),
|
309
|
|
- };
|
|
303
|
+ return $this->day_of_month->resolveDate($lastDate->copy()->addMonth());
|
310
|
304
|
}
|
311
|
305
|
|
312
|
306
|
public function calculateNextYearlyDate(Carbon $lastDate): ?Carbon
|
313
|
307
|
{
|
314
|
|
- return match (true) {
|
315
|
|
- $lastDate->equalTo($this->start_date) => $lastDate->copy()
|
316
|
|
- ->month($this->month->value)
|
317
|
|
- ->day(min($this->day_of_month->value, $lastDate->daysInMonth)),
|
318
|
|
-
|
319
|
|
- default => $lastDate->copy()
|
320
|
|
- ->addYear()
|
321
|
|
- ->month($this->month->value)
|
322
|
|
- ->day(min($this->day_of_month->value, $lastDate->copy()->addYear()->month($this->month->value)->daysInMonth))
|
323
|
|
- };
|
|
308
|
+ return $this->day_of_month->resolveDate($lastDate->copy()->addYear()->month($this->month->value));
|
324
|
309
|
}
|
325
|
310
|
|
326
|
311
|
protected function calculateCustomNextDate(Carbon $lastDate): ?Carbon
|
|
@@ -330,34 +315,11 @@ class RecurringInvoice extends Document
|
330
|
315
|
return match ($this->interval_type) {
|
331
|
316
|
IntervalType::Day => $lastDate->copy()->addDays($interval),
|
332
|
317
|
|
333
|
|
- IntervalType::Week => match (true) {
|
334
|
|
- $lastDate->equalTo($this->start_date) => $lastDate->copy()->next($this->day_of_week->value),
|
335
|
|
-
|
336
|
|
- $lastDate->dayOfWeek === $this->day_of_week->value => $lastDate->copy()->addWeeks($interval),
|
337
|
|
-
|
338
|
|
- default => $lastDate->copy()->next($this->day_of_week->value),
|
339
|
|
- },
|
340
|
|
-
|
341
|
|
- IntervalType::Month => match (true) {
|
342
|
|
- $lastDate->equalTo($this->start_date) => $lastDate->copy()->day(
|
343
|
|
- min($this->day_of_month->value, $lastDate->daysInMonth)
|
344
|
|
- ),
|
|
318
|
+ IntervalType::Week => $lastDate->copy()->addWeeks($interval),
|
345
|
319
|
|
346
|
|
- default => $lastDate->copy()->addMonths($interval)->day(
|
347
|
|
- min($this->day_of_month->value, $lastDate->copy()->addMonths($interval)->daysInMonth)
|
348
|
|
- ),
|
349
|
|
- },
|
|
320
|
+ IntervalType::Month => $this->day_of_month->resolveDate($lastDate->copy()->addMonths($interval)),
|
350
|
321
|
|
351
|
|
- IntervalType::Year => match (true) {
|
352
|
|
- $lastDate->equalTo($this->start_date) => $lastDate->copy()
|
353
|
|
- ->month($this->month->value)
|
354
|
|
- ->day(min($this->day_of_month->value, $lastDate->daysInMonth)),
|
355
|
|
-
|
356
|
|
- default => $lastDate->copy()
|
357
|
|
- ->addYears($interval)
|
358
|
|
- ->month($this->month->value)
|
359
|
|
- ->day(min($this->day_of_month->value, $lastDate->copy()->addYears($interval)->month($this->month->value)->daysInMonth))
|
360
|
|
- },
|
|
322
|
+ IntervalType::Year => $this->day_of_month->resolveDate($lastDate->copy()->addYears($interval)->month($this->month->value)),
|
361
|
323
|
|
362
|
324
|
default => null
|
363
|
325
|
};
|
|
@@ -411,196 +373,97 @@ class RecurringInvoice extends Document
|
411
|
373
|
->form([
|
412
|
374
|
CustomSection::make('Frequency')
|
413
|
375
|
->contained(false)
|
414
|
|
- ->schema([
|
415
|
|
- Forms\Components\Select::make('frequency')
|
416
|
|
- ->label('Repeats')
|
417
|
|
- ->options(Frequency::class)
|
418
|
|
- ->softRequired()
|
419
|
|
- ->live()
|
420
|
|
- ->afterStateUpdated(function (Forms\Set $set, $state) {
|
421
|
|
- $frequency = Frequency::parse($state);
|
422
|
|
-
|
423
|
|
- if ($frequency->isDaily()) {
|
424
|
|
- $set('interval_value', null);
|
425
|
|
- $set('interval_type', null);
|
426
|
|
- }
|
427
|
|
-
|
428
|
|
- if ($frequency->isWeekly()) {
|
429
|
|
- $currentDayOfWeek = now()->dayOfWeek;
|
430
|
|
- $currentDayOfWeek = DayOfWeek::parse($currentDayOfWeek);
|
431
|
|
- $set('day_of_week', $currentDayOfWeek);
|
432
|
|
- $set('interval_value', null);
|
433
|
|
- $set('interval_type', null);
|
434
|
|
- }
|
435
|
|
-
|
436
|
|
- if ($frequency->isMonthly()) {
|
437
|
|
- $set('day_of_month', DayOfMonth::First);
|
438
|
|
- $set('interval_value', null);
|
439
|
|
- $set('interval_type', null);
|
440
|
|
- }
|
441
|
|
-
|
442
|
|
- if ($frequency->isYearly()) {
|
443
|
|
- $currentMonth = now()->month;
|
444
|
|
- $currentMonth = Month::parse($currentMonth);
|
445
|
|
- $set('month', $currentMonth);
|
446
|
|
-
|
447
|
|
- $currentDay = now()->dayOfMonth;
|
448
|
|
- $currentDay = DayOfMonth::parse($currentDay);
|
449
|
|
- $set('day_of_month', $currentDay);
|
450
|
|
-
|
451
|
|
- $set('interval_value', null);
|
452
|
|
- $set('interval_type', null);
|
453
|
|
- }
|
454
|
|
-
|
455
|
|
- if ($frequency->isCustom()) {
|
456
|
|
- $set('interval_value', 1);
|
457
|
|
- $set('interval_type', IntervalType::Month);
|
458
|
|
-
|
459
|
|
- $currentDay = now()->dayOfMonth;
|
460
|
|
- $currentDay = DayOfMonth::parse($currentDay);
|
461
|
|
- $set('day_of_month', $currentDay);
|
462
|
|
- }
|
463
|
|
- }),
|
464
|
|
-
|
465
|
|
- // Custom frequency fields in a nested grid
|
466
|
|
- Cluster::make([
|
467
|
|
- Forms\Components\TextInput::make('interval_value')
|
|
376
|
+ ->schema(function (Forms\Get $get) {
|
|
377
|
+ $frequency = Frequency::parse($get('frequency'));
|
|
378
|
+ $intervalType = IntervalType::parse($get('interval_type'));
|
|
379
|
+ $month = Month::parse($get('month'));
|
|
380
|
+ $dayOfMonth = DayOfMonth::parse($get('day_of_month'));
|
|
381
|
+
|
|
382
|
+ return [
|
|
383
|
+ Forms\Components\Select::make('frequency')
|
|
384
|
+ ->label('Repeats')
|
|
385
|
+ ->options(Frequency::class)
|
468
|
386
|
->softRequired()
|
469
|
|
- ->numeric()
|
470
|
|
- ->default(1),
|
471
|
|
- Forms\Components\Select::make('interval_type')
|
472
|
|
- ->options(IntervalType::class)
|
473
|
|
- ->softRequired()
|
474
|
|
- ->default(IntervalType::Month)
|
475
|
387
|
->live()
|
476
|
388
|
->afterStateUpdated(function (Forms\Set $set, $state) {
|
477
|
|
- $intervalType = IntervalType::parse($state);
|
|
389
|
+ $handler = new ScheduleHandler($set);
|
|
390
|
+ $handler->handleFrequencyChange($state);
|
|
391
|
+ }),
|
478
|
392
|
|
479
|
|
- if ($intervalType->isWeek()) {
|
480
|
|
- $currentDayOfWeek = now()->dayOfWeek;
|
481
|
|
- $currentDayOfWeek = DayOfWeek::parse($currentDayOfWeek);
|
482
|
|
- $set('day_of_week', $currentDayOfWeek);
|
483
|
|
- }
|
|
393
|
+ // Custom frequency fields in a nested grid
|
|
394
|
+ Cluster::make([
|
|
395
|
+ Forms\Components\TextInput::make('interval_value')
|
|
396
|
+ ->softRequired()
|
|
397
|
+ ->numeric()
|
|
398
|
+ ->default(1),
|
|
399
|
+ Forms\Components\Select::make('interval_type')
|
|
400
|
+ ->options(IntervalType::class)
|
|
401
|
+ ->softRequired()
|
|
402
|
+ ->default(IntervalType::Month)
|
|
403
|
+ ->live()
|
|
404
|
+ ->afterStateUpdated(function (Forms\Set $set, $state) {
|
|
405
|
+ $handler = new ScheduleHandler($set);
|
|
406
|
+ $handler->handleIntervalTypeChange($state);
|
|
407
|
+ }),
|
|
408
|
+ ])
|
|
409
|
+ ->live()
|
|
410
|
+ ->label('Every')
|
|
411
|
+ ->required()
|
|
412
|
+ ->markAsRequired(false)
|
|
413
|
+ ->visible($frequency->isCustom()),
|
|
414
|
+
|
|
415
|
+ // Specific schedule details
|
|
416
|
+ Forms\Components\Select::make('month')
|
|
417
|
+ ->label('Month')
|
|
418
|
+ ->options(Month::class)
|
|
419
|
+ ->softRequired()
|
|
420
|
+ ->visible($frequency->isYearly() || $intervalType?->isYear())
|
|
421
|
+ ->live()
|
|
422
|
+ ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
|
423
|
+ $handler = new ScheduleHandler($set, $get);
|
|
424
|
+ $handler->handleDateChange('month', $state);
|
|
425
|
+ }),
|
484
|
426
|
|
485
|
|
- if ($intervalType->isMonth()) {
|
486
|
|
- $currentDay = now()->dayOfMonth;
|
487
|
|
- $currentDay = DayOfMonth::parse($currentDay);
|
488
|
|
- $set('day_of_month', $currentDay);
|
|
427
|
+ Forms\Components\Select::make('day_of_month')
|
|
428
|
+ ->label('Day of Month')
|
|
429
|
+ ->options(function () use ($month) {
|
|
430
|
+ if (! $month) {
|
|
431
|
+ return DayOfMonth::class;
|
489
|
432
|
}
|
490
|
433
|
|
491
|
|
- if ($intervalType->isYear()) {
|
492
|
|
- $currentMonth = now()->month;
|
493
|
|
- $currentMonth = Month::parse($currentMonth);
|
494
|
|
- $set('month', $currentMonth);
|
|
434
|
+ $daysInMonth = Carbon::createFromDate(null, $month->value)->daysInMonth;
|
495
|
435
|
|
496
|
|
- $currentDay = now()->dayOfMonth;
|
497
|
|
- $currentDay = DayOfMonth::parse($currentDay);
|
498
|
|
- $set('day_of_month', $currentDay);
|
499
|
|
- }
|
|
436
|
+ return collect(DayOfMonth::cases())
|
|
437
|
+ ->filter(static fn (DayOfMonth $dayOfMonth) => $dayOfMonth->value <= $daysInMonth || $dayOfMonth->isLast())
|
|
438
|
+ ->mapWithKeys(fn (DayOfMonth $dayOfMonth) => [$dayOfMonth->value => $dayOfMonth->getLabel()]);
|
|
439
|
+ })
|
|
440
|
+ ->softRequired()
|
|
441
|
+ ->visible(in_array($frequency, [Frequency::Monthly, Frequency::Yearly]) || in_array($intervalType, [IntervalType::Month, IntervalType::Year]))
|
|
442
|
+ ->live()
|
|
443
|
+ ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
|
444
|
+ $handler = new ScheduleHandler($set, $get);
|
|
445
|
+ $handler->handleDateChange('day_of_month', $state);
|
500
|
446
|
}),
|
501
|
|
- ])
|
502
|
|
- ->live()
|
503
|
|
- ->label('Every')
|
504
|
|
- ->required()
|
505
|
|
- ->markAsRequired(false)
|
506
|
|
- ->visible(fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isCustom()),
|
507
|
|
-
|
508
|
|
- // Specific schedule details
|
509
|
|
- Forms\Components\Select::make('month')
|
510
|
|
- ->label('Month')
|
511
|
|
- ->options(Month::class)
|
512
|
|
- ->softRequired()
|
513
|
|
- ->visible(
|
514
|
|
- fn (Forms\Get $get) => Frequency::parse($get('frequency'))->isYearly() ||
|
515
|
|
- IntervalType::parse($get('interval_type'))?->isYear()
|
516
|
|
- )
|
517
|
|
- ->live()
|
518
|
|
- ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
519
|
|
- $dayOfMonth = DayOfMonth::parse($get('day_of_month'));
|
520
|
|
- $frequency = Frequency::parse($get('frequency'));
|
521
|
|
- $intervalType = IntervalType::parse($get('interval_type'));
|
522
|
|
- $month = Month::parse($state);
|
523
|
|
-
|
524
|
|
- if (($frequency->isYearly() || $intervalType?->isYear()) && $month && $dayOfMonth) {
|
525
|
|
- $date = $dayOfMonth->resolveDate(today()->month($month->value))->toImmutable();
|
526
|
|
-
|
527
|
|
- $adjustedStartDate = $date->lt(today())
|
528
|
|
- ? $dayOfMonth->resolveDate($date->addYear()->month($month->value))
|
529
|
|
- : $dayOfMonth->resolveDate($date->month($month->value));
|
530
|
|
-
|
531
|
|
- $adjustedDay = min($dayOfMonth->value, $adjustedStartDate->daysInMonth);
|
532
|
447
|
|
533
|
|
- $set('day_of_month', $adjustedDay);
|
|
448
|
+ SimpleAlert::make('dayOfMonthNotice')
|
|
449
|
+ ->title(function () use ($dayOfMonth) {
|
|
450
|
+ return "The invoice will be created on the {$dayOfMonth->getLabel()} day of each month, or on the last day for months ending earlier.";
|
|
451
|
+ })
|
|
452
|
+ ->columnSpanFull()
|
|
453
|
+ ->visible($dayOfMonth?->value > 28),
|
534
|
454
|
|
535
|
|
- $set('start_date', $adjustedStartDate);
|
536
|
|
- }
|
537
|
|
- }),
|
538
|
|
-
|
539
|
|
- Forms\Components\Select::make('day_of_month')
|
540
|
|
- ->label('Day of Month')
|
541
|
|
- ->options(function (Forms\Get $get) {
|
542
|
|
- $month = Month::parse($get('month')) ?? Month::January;
|
543
|
|
-
|
544
|
|
- $daysInMonth = Carbon::createFromDate(null, $month->value)->daysInMonth;
|
545
|
|
-
|
546
|
|
- return collect(DayOfMonth::cases())
|
547
|
|
- ->filter(static fn (DayOfMonth $dayOfMonth) => $dayOfMonth->value <= $daysInMonth || $dayOfMonth->isLast())
|
548
|
|
- ->mapWithKeys(fn (DayOfMonth $dayOfMonth) => [$dayOfMonth->value => $dayOfMonth->getLabel()]);
|
549
|
|
- })
|
550
|
|
- ->softRequired()
|
551
|
|
- ->visible(
|
552
|
|
- fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isMonthly() ||
|
553
|
|
- Frequency::parse($get('frequency'))?->isYearly() ||
|
554
|
|
- IntervalType::parse($get('interval_type'))?->isMonth() ||
|
555
|
|
- IntervalType::parse($get('interval_type'))?->isYear()
|
556
|
|
- )
|
557
|
|
- ->live()
|
558
|
|
- ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
559
|
|
- $dayOfMonth = DayOfMonth::parse($state);
|
560
|
|
- $frequency = Frequency::parse($get('frequency'));
|
561
|
|
- $intervalType = IntervalType::parse($get('interval_type'));
|
562
|
|
- $month = Month::parse($get('month'));
|
563
|
|
-
|
564
|
|
- if (($frequency->isMonthly() || $intervalType?->isMonth()) && $dayOfMonth) {
|
565
|
|
- $date = $dayOfMonth->resolveDate(today())->toImmutable();
|
566
|
|
-
|
567
|
|
- $adjustedStartDate = $date->lt(today())
|
568
|
|
- ? $dayOfMonth->resolveDate($date->addMonth())
|
569
|
|
- : $dayOfMonth->resolveDate($date);
|
570
|
|
-
|
571
|
|
- $set('start_date', $adjustedStartDate);
|
572
|
|
- }
|
573
|
|
-
|
574
|
|
- if (($frequency->isYearly() || $intervalType?->isYear()) && $month && $dayOfMonth) {
|
575
|
|
- $date = $dayOfMonth->resolveDate(today()->month($month->value))->toImmutable();
|
576
|
|
-
|
577
|
|
- $adjustedStartDate = $date->lt(today())
|
578
|
|
- ? $dayOfMonth->resolveDate($date->addYear()->month($month->value))
|
579
|
|
- : $dayOfMonth->resolveDate($date->month($month->value));
|
580
|
|
-
|
581
|
|
- $set('start_date', $adjustedStartDate);
|
582
|
|
- }
|
583
|
|
- }),
|
584
|
|
-
|
585
|
|
- Forms\Components\Select::make('day_of_week')
|
586
|
|
- ->label('Day of Week')
|
587
|
|
- ->options(DayOfWeek::class)
|
588
|
|
- ->softRequired()
|
589
|
|
- ->visible(
|
590
|
|
- fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isWeekly() ||
|
591
|
|
- IntervalType::parse($get('interval_type'))?->isWeek()
|
592
|
|
- )
|
593
|
|
- ->live()
|
594
|
|
- ->afterStateUpdated(function (Forms\Set $set, $state) {
|
595
|
|
- $dayOfWeek = DayOfWeek::parse($state);
|
596
|
|
-
|
597
|
|
- $adjustedStartDate = today()->is($dayOfWeek->name)
|
598
|
|
- ? today()
|
599
|
|
- : today()->next($dayOfWeek->name);
|
600
|
|
-
|
601
|
|
- $set('start_date', $adjustedStartDate);
|
602
|
|
- }),
|
603
|
|
- ])->columns(2),
|
|
455
|
+ Forms\Components\Select::make('day_of_week')
|
|
456
|
+ ->label('Day of Week')
|
|
457
|
+ ->options(DayOfWeek::class)
|
|
458
|
+ ->softRequired()
|
|
459
|
+ ->visible($frequency->isWeekly() || $intervalType?->isWeek())
|
|
460
|
+ ->live()
|
|
461
|
+ ->afterStateUpdated(function (Forms\Set $set, $state) {
|
|
462
|
+ $handler = new ScheduleHandler($set);
|
|
463
|
+ $handler->handleDateChange('day_of_week', $state);
|
|
464
|
+ }),
|
|
465
|
+ ];
|
|
466
|
+ })->columns(2),
|
604
|
467
|
|
605
|
468
|
CustomSection::make('Dates & Time')
|
606
|
469
|
->contained(false)
|
|
@@ -611,12 +474,9 @@ class RecurringInvoice extends Document
|
611
|
474
|
->live()
|
612
|
475
|
->minDate(today())
|
613
|
476
|
->closeOnDateSelection()
|
614
|
|
- ->afterStateUpdated(function (Forms\Set $set, $state) {
|
615
|
|
- $startDate = Carbon::parse($state);
|
616
|
|
-
|
617
|
|
- $dayOfWeek = DayOfWeek::parse($startDate->dayOfWeek);
|
618
|
|
-
|
619
|
|
- $set('day_of_week', $dayOfWeek);
|
|
477
|
+ ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
|
478
|
+ $handler = new ScheduleHandler($set, $get);
|
|
479
|
+ $handler->handleDateChange('start_date', $state);
|
620
|
480
|
}),
|
621
|
481
|
|
622
|
482
|
Forms\Components\Group::make(function (Forms\Get $get) {
|
|
@@ -630,20 +490,8 @@ class RecurringInvoice extends Document
|
630
|
490
|
->afterStateUpdated(function (Forms\Set $set, $state) {
|
631
|
491
|
$endType = EndType::parse($state);
|
632
|
492
|
|
633
|
|
- if ($endType?->isNever()) {
|
634
|
|
- $set('max_occurrences', null);
|
635
|
|
- $set('end_date', null);
|
636
|
|
- }
|
637
|
|
-
|
638
|
|
- if ($endType?->isAfter()) {
|
639
|
|
- $set('max_occurrences', 1);
|
640
|
|
- $set('end_date', null);
|
641
|
|
- }
|
642
|
|
-
|
643
|
|
- if ($endType?->isOn()) {
|
644
|
|
- $set('max_occurrences', null);
|
645
|
|
- $set('end_date', now()->addMonth()->startOfMonth());
|
646
|
|
- }
|
|
493
|
+ $set('max_occurrences', $endType?->isAfter() ? 1 : null);
|
|
494
|
+ $set('end_date', $endType?->isOn() ? now()->addMonth()->startOfMonth() : null);
|
647
|
495
|
});
|
648
|
496
|
|
649
|
497
|
$endType = EndType::parse($get('end_type'));
|