Pārlūkot izejas kodu

wip adjustment statuses

3.x
Andrew Wallo 6 mēnešus atpakaļ
vecāks
revīzija
e531917593

+ 18
- 1
app/Enums/Accounting/AdjustmentStatus.php Parādīt failu

10
     case Active = 'active';
10
     case Active = 'active';
11
     case Upcoming = 'upcoming';
11
     case Upcoming = 'upcoming';
12
     case Expired = 'expired';
12
     case Expired = 'expired';
13
+    case Paused = 'paused';
13
     case Archived = 'archived';
14
     case Archived = 'archived';
14
 
15
 
15
     public function getLabel(): ?string
16
     public function getLabel(): ?string
21
     {
22
     {
22
         return match ($this) {
23
         return match ($this) {
23
             self::Active => 'primary',
24
             self::Active => 'primary',
24
-            self::Upcoming => 'warning',
25
+            self::Upcoming, self::Paused => 'warning',
25
             self::Expired => 'danger',
26
             self::Expired => 'danger',
26
             self::Archived => 'gray',
27
             self::Archived => 'gray',
27
         };
28
         };
28
     }
29
     }
30
+
31
+    /**
32
+     * Check if the status is set manually (not calculated from dates)
33
+     */
34
+    public function isManualStatus(): bool
35
+    {
36
+        return in_array($this, [self::Paused, self::Archived]);
37
+    }
38
+
39
+    /**
40
+     * Check if the status is system-calculated based on dates
41
+     */
42
+    public function isSystemStatus(): bool
43
+    {
44
+        return in_array($this, [self::Active, self::Upcoming, self::Expired]);
45
+    }
29
 }
46
 }

+ 141
- 6
app/Filament/Company/Clusters/Settings/Resources/AdjustmentResource.php Parādīt failu

5
 use App\Enums\Accounting\AdjustmentCategory;
5
 use App\Enums\Accounting\AdjustmentCategory;
6
 use App\Enums\Accounting\AdjustmentComputation;
6
 use App\Enums\Accounting\AdjustmentComputation;
7
 use App\Enums\Accounting\AdjustmentScope;
7
 use App\Enums\Accounting\AdjustmentScope;
8
-use App\Enums\Accounting\AdjustmentStatus;
9
 use App\Enums\Accounting\AdjustmentType;
8
 use App\Enums\Accounting\AdjustmentType;
10
 use App\Filament\Company\Clusters\Settings;
9
 use App\Filament\Company\Clusters\Settings;
11
 use App\Filament\Company\Clusters\Settings\Resources\AdjustmentResource\Pages;
10
 use App\Filament\Company\Clusters\Settings\Resources\AdjustmentResource\Pages;
12
 use App\Models\Accounting\Adjustment;
11
 use App\Models\Accounting\Adjustment;
13
 use Filament\Forms;
12
 use Filament\Forms;
14
 use Filament\Forms\Form;
13
 use Filament\Forms\Form;
14
+use Filament\Notifications\Notification;
15
 use Filament\Resources\Resource;
15
 use Filament\Resources\Resource;
16
 use Filament\Tables;
16
 use Filament\Tables;
17
 use Filament\Tables\Table;
17
 use Filament\Tables\Table;
18
+use Illuminate\Database\Eloquent\Collection;
18
 
19
 
19
 class AdjustmentResource extends Resource
20
 class AdjustmentResource extends Resource
20
 {
21
 {
110
             ->actions([
111
             ->actions([
111
                 Tables\Actions\ActionGroup::make([
112
                 Tables\Actions\ActionGroup::make([
112
                     Tables\Actions\EditAction::make(),
113
                     Tables\Actions\EditAction::make(),
114
+                    Tables\Actions\Action::make('pause')
115
+                        ->label('Pause')
116
+                        ->icon('heroicon-m-pause')
117
+                        ->color('warning')
118
+                        ->form([
119
+                            Forms\Components\DateTimePicker::make('paused_until')
120
+                                ->label('Auto-resume Date')
121
+                                ->helperText('When should this adjustment automatically resume? Leave empty to keep paused indefinitely.')
122
+                                ->after('now'),
123
+                            Forms\Components\Textarea::make('status_reason')
124
+                                ->label('Reason for Pausing')
125
+                                ->maxLength(255),
126
+                        ])
127
+                        ->visible(fn (Adjustment $record) => $record->canBePaused())
128
+                        ->action(function (Adjustment $record, array $data) {
129
+                            $pausedUntil = $data['paused_until'] ?? null;
130
+                            $reason = $data['status_reason'] ?? null;
131
+                            $record->pause($reason, $pausedUntil);
132
+                        }),
133
+                    Tables\Actions\Action::make('resume')
134
+                        ->label('Resume')
135
+                        ->icon('heroicon-m-play')
136
+                        ->color('success')
137
+                        ->requiresConfirmation()
138
+                        ->visible(fn (Adjustment $record) => $record->canBeResumed())
139
+                        ->action(fn (Adjustment $record) => $record->resume()),
113
                     Tables\Actions\Action::make('archive')
140
                     Tables\Actions\Action::make('archive')
114
                         ->label('Archive')
141
                         ->label('Archive')
115
-                        ->icon('heroicon-o-archive-box-x-mark')
142
+                        ->icon('heroicon-m-archive-box')
116
                         ->color('danger')
143
                         ->color('danger')
117
-                        ->requiresConfirmation()
118
-                        ->visible(static fn (Adjustment $record) => $record->status !== AdjustmentStatus::Archived)
119
-                        ->action(fn (Adjustment $record) => $record->update(['status' => AdjustmentStatus::Archived])),
144
+                        ->form([
145
+                            Forms\Components\Textarea::make('status_reason')
146
+                                ->label('Reason for Archiving')
147
+                                ->maxLength(255),
148
+                        ])
149
+                        ->visible(fn (Adjustment $record) => $record->canBeArchived())
150
+                        ->action(function (Adjustment $record, array $data) {
151
+                            $reason = $data['status_reason'] ?? null;
152
+                            $record->archive($reason);
153
+                        }),
120
                 ]),
154
                 ]),
121
             ])
155
             ])
122
             ->bulkActions([
156
             ->bulkActions([
123
-                //
157
+                Tables\Actions\BulkActionGroup::make([
158
+                    Tables\Actions\BulkAction::make('pause')
159
+                        ->label('Pause')
160
+                        ->icon('heroicon-m-pause')
161
+                        ->color('warning')
162
+                        ->form([
163
+                            Forms\Components\DateTimePicker::make('paused_until')
164
+                                ->label('Auto-resume Date')
165
+                                ->helperText('When should these adjustments automatically resume? Leave empty to keep paused indefinitely.')
166
+                                ->after('now'),
167
+                            Forms\Components\Textarea::make('status_reason')
168
+                                ->label('Reason for Pausing')
169
+                                ->maxLength(255),
170
+                        ])
171
+                        ->databaseTransaction()
172
+                        ->successNotificationTitle('Adjustments paused')
173
+                        ->failureNotificationTitle('Failed to pause adjustments')
174
+                        ->before(function (Collection $records, Tables\Actions\BulkAction $action) {
175
+                            $isInvalid = $records->contains(fn (Adjustment $record) => ! $record->canBePaused());
176
+
177
+                            if ($isInvalid) {
178
+                                Notification::make()
179
+                                    ->title('Pause failed')
180
+                                    ->body('Only adjustments that are currently active can be paused. Please adjust your selection and try again.')
181
+                                    ->persistent()
182
+                                    ->danger()
183
+                                    ->send();
184
+
185
+                                $action->cancel(true);
186
+                            }
187
+                        })
188
+                        ->deselectRecordsAfterCompletion()
189
+                        ->action(function (Collection $records, array $data) {
190
+                            $pausedUntil = $data['paused_until'] ?? null;
191
+                            $reason = $data['status_reason'] ?? null;
192
+
193
+                            $records->each(function (Adjustment $record) use ($reason, $pausedUntil) {
194
+                                $record->pause($reason, $pausedUntil);
195
+                            });
196
+                        }),
197
+                    Tables\Actions\BulkAction::make('resume')
198
+                        ->label('Resume')
199
+                        ->icon('heroicon-m-play')
200
+                        ->color('success')
201
+                        ->databaseTransaction()
202
+                        ->successNotificationTitle('Adjustments resumed')
203
+                        ->failureNotificationTitle('Failed to resume adjustments')
204
+                        ->before(function (Collection $records, Tables\Actions\BulkAction $action) {
205
+                            $isInvalid = $records->contains(fn (Adjustment $record) => ! $record->canBeResumed());
206
+
207
+                            if ($isInvalid) {
208
+                                Notification::make()
209
+                                    ->title('Resume failed')
210
+                                    ->body('Only adjustments that are currently paused can be resumed. Please adjust your selection and try again.')
211
+                                    ->persistent()
212
+                                    ->danger()
213
+                                    ->send();
214
+
215
+                                $action->cancel(true);
216
+                            }
217
+                        })
218
+                        ->deselectRecordsAfterCompletion()
219
+                        ->action(function (Collection $records) {
220
+                            $records->each(function (Adjustment $record) {
221
+                                $record->resume();
222
+                            });
223
+                        }),
224
+                    Tables\Actions\BulkAction::make('archive')
225
+                        ->label('Archive')
226
+                        ->icon('heroicon-m-archive-box')
227
+                        ->color('danger')
228
+                        ->form([
229
+                            Forms\Components\Textarea::make('status_reason')
230
+                                ->label('Reason for Archiving')
231
+                                ->maxLength(255),
232
+                        ])
233
+                        ->databaseTransaction()
234
+                        ->successNotificationTitle('Adjustments archived')
235
+                        ->failureNotificationTitle('Failed to archive adjustments')
236
+                        ->before(function (Collection $records, Tables\Actions\BulkAction $action) {
237
+                            $isInvalid = $records->contains(fn (Adjustment $record) => ! $record->canBeArchived());
238
+
239
+                            if ($isInvalid) {
240
+                                Notification::make()
241
+                                    ->title('Archive failed')
242
+                                    ->body('Only adjustments that are currently active or paused can be archived. Please adjust your selection and try again.')
243
+                                    ->persistent()
244
+                                    ->danger()
245
+                                    ->send();
246
+
247
+                                $action->cancel(true);
248
+                            }
249
+                        })
250
+                        ->deselectRecordsAfterCompletion()
251
+                        ->action(function (Collection $records, array $data) {
252
+                            $reason = $data['status_reason'] ?? null;
253
+
254
+                            $records->each(function (Adjustment $record) use ($reason) {
255
+                                $record->archive($reason);
256
+                            });
257
+                        }),
258
+                ]),
124
             ]);
259
             ]);
125
     }
260
     }
126
 
261
 

+ 124
- 4
app/Models/Accounting/Adjustment.php Parādīt failu

34
         'account_id',
34
         'account_id',
35
         'name',
35
         'name',
36
         'status',
36
         'status',
37
+        'status_reason',
37
         'description',
38
         'description',
38
         'category',
39
         'category',
39
         'type',
40
         'type',
43
         'scope',
44
         'scope',
44
         'start_date',
45
         'start_date',
45
         'end_date',
46
         'end_date',
47
+        'paused_at',
48
+        'paused_until',
49
+        'archived_at',
46
         'created_by',
50
         'created_by',
47
         'updated_by',
51
         'updated_by',
48
     ];
52
     ];
57
         'scope' => AdjustmentScope::class,
61
         'scope' => AdjustmentScope::class,
58
         'start_date' => 'datetime',
62
         'start_date' => 'datetime',
59
         'end_date' => 'datetime',
63
         'end_date' => 'datetime',
64
+        'paused_at' => 'datetime',
65
+        'paused_until' => 'datetime',
66
+        'archived_at' => 'datetime',
60
     ];
67
     ];
61
 
68
 
62
     public function account(): BelongsTo
69
     public function account(): BelongsTo
94
         return $this->category->isDiscount() && $this->type->isPurchase();
101
         return $this->category->isDiscount() && $this->type->isPurchase();
95
     }
102
     }
96
 
103
 
97
-    public function calculateStatus(): AdjustmentStatus
104
+    // Add these methods to your Adjustment model
105
+
106
+    /**
107
+     * Check if adjustment can be paused
108
+     */
109
+    public function canBePaused(): bool
98
     {
110
     {
99
-        if ($this->status === AdjustmentStatus::Archived) {
100
-            return AdjustmentStatus::Archived;
101
-        }
111
+        return $this->status === AdjustmentStatus::Active;
112
+    }
113
+
114
+    /**
115
+     * Check if adjustment can be resumed
116
+     */
117
+    public function canBeResumed(): bool
118
+    {
119
+        return $this->status === AdjustmentStatus::Paused;
120
+    }
102
 
121
 
122
+    /**
123
+     * Check if adjustment can be archived
124
+     */
125
+    public function canBeArchived(): bool
126
+    {
127
+        return $this->status !== AdjustmentStatus::Archived;
128
+    }
129
+
130
+    /**
131
+     * Calculate the natural status of the adjustment based on dates
132
+     */
133
+    public function calculateNaturalStatus(): AdjustmentStatus
134
+    {
103
         if ($this->start_date?->isFuture()) {
135
         if ($this->start_date?->isFuture()) {
104
             return AdjustmentStatus::Upcoming;
136
             return AdjustmentStatus::Upcoming;
105
         }
137
         }
111
         return AdjustmentStatus::Active;
143
         return AdjustmentStatus::Active;
112
     }
144
     }
113
 
145
 
146
+    /**
147
+     * Pause the adjustment
148
+     */
149
+    public function pause(?string $reason = null, ?\DateTime $untilDate = null): bool
150
+    {
151
+        if (! $this->canBePaused()) {
152
+            return false;
153
+        }
154
+
155
+        $this->paused_at = now();
156
+        $this->paused_until = $untilDate;
157
+        $this->status = AdjustmentStatus::Paused;
158
+        $this->status_reason = $reason;
159
+
160
+        return $this->save();
161
+    }
162
+
163
+    /**
164
+     * Resume the adjustment
165
+     */
166
+    public function resume(): bool
167
+    {
168
+        if (! $this->canBeResumed()) {
169
+            return false;
170
+        }
171
+
172
+        $this->paused_at = null;
173
+        $this->paused_until = null;
174
+        $this->status_reason = null;
175
+        $this->status = $this->calculateNaturalStatus();
176
+
177
+        return $this->save();
178
+    }
179
+
180
+    /**
181
+     * Archive the adjustment
182
+     */
183
+    public function archive(?string $reason = null): bool
184
+    {
185
+        if (! $this->canBeArchived()) {
186
+            return false;
187
+        }
188
+
189
+        $this->status = AdjustmentStatus::Archived;
190
+        $this->status_reason = $reason;
191
+
192
+        return $this->save();
193
+    }
194
+
195
+    /**
196
+     * Check if the adjustment should be automatically resumed
197
+     */
198
+    public function shouldAutoResume(): bool
199
+    {
200
+        return $this->status === AdjustmentStatus::Paused &&
201
+            $this->paused_until !== null &&
202
+            $this->paused_until->isPast();
203
+    }
204
+
205
+    /**
206
+     * Refresh the status based on current dates and conditions
207
+     */
208
+    public function refreshStatus(): bool
209
+    {
210
+        // Don't automatically change archived or paused status
211
+        if ($this->status === AdjustmentStatus::Archived ||
212
+            ($this->status === AdjustmentStatus::Paused && ! $this->shouldAutoResume())) {
213
+            return false;
214
+        }
215
+
216
+        // Check if a paused adjustment should be auto-resumed
217
+        if ($this->shouldAutoResume()) {
218
+            return $this->resume();
219
+        }
220
+
221
+        // Calculate natural status based on dates
222
+        $naturalStatus = $this->calculateNaturalStatus();
223
+
224
+        // Only update if the status would change
225
+        if ($this->status !== $naturalStatus) {
226
+            $this->status = $naturalStatus;
227
+
228
+            return $this->save();
229
+        }
230
+
231
+        return false;
232
+    }
233
+
114
     protected static function newFactory(): Factory
234
     protected static function newFactory(): Factory
115
     {
235
     {
116
         return AdjustmentFactory::new();
236
         return AdjustmentFactory::new();

+ 26
- 6
app/Observers/AdjustmentObserver.php Parādīt failu

27
                 $adjustment->account()->associate($account);
27
                 $adjustment->account()->associate($account);
28
             }
28
             }
29
         }
29
         }
30
-
31
-        if ($adjustment->status !== AdjustmentStatus::Archived) {
32
-            $adjustment->status = $adjustment->calculateStatus();
33
-        }
34
     }
30
     }
35
 
31
 
36
     public function updating(Adjustment $adjustment): void
32
     public function updating(Adjustment $adjustment): void
41
                 'description' => $adjustment->description,
37
                 'description' => $adjustment->description,
42
             ]);
38
             ]);
43
         }
39
         }
40
+    }
41
+
42
+    /**
43
+     * Handle the Adjustment "saving" event.
44
+     */
45
+    public function saving(Adjustment $adjustment): void
46
+    {
47
+        // Handle dates changes affecting status
48
+        // Only if the status isn't being explicitly changed and not in a manual state
49
+        if ($adjustment->isDirty(['start_date', 'end_date']) &&
50
+            ! $adjustment->isDirty('status') &&
51
+            ! in_array($adjustment->status, [AdjustmentStatus::Archived, AdjustmentStatus::Paused])) {
52
+
53
+            $adjustment->status = $adjustment->calculateNaturalStatus();
54
+        }
55
+
56
+        // Handle auto-resume for paused adjustments with a paused_until date
57
+        if ($adjustment->shouldAutoResume() && ! $adjustment->isDirty('status')) {
58
+            $adjustment->status = $adjustment->calculateNaturalStatus();
59
+            $adjustment->paused_at = null;
60
+            $adjustment->paused_until = null;
61
+            $adjustment->status_reason = null;
62
+        }
44
 
63
 
45
-        if ($adjustment->status !== AdjustmentStatus::Archived) {
46
-            $adjustment->status = $adjustment->calculateStatus();
64
+        // Ensure consistency between paused status and paused_at field
65
+        if ($adjustment->status === AdjustmentStatus::Paused && ! $adjustment->paused_at) {
66
+            $adjustment->paused_at = now();
47
         }
67
         }
48
     }
68
     }
49
 }
69
 }

+ 4
- 0
database/migrations/2024_11_14_230753_create_adjustments_table.php Parādīt failu

17
             $table->foreignId('account_id')->nullable()->constrained('accounts')->nullOnDelete();
17
             $table->foreignId('account_id')->nullable()->constrained('accounts')->nullOnDelete();
18
             $table->string('name')->nullable();
18
             $table->string('name')->nullable();
19
             $table->string('status')->default('active');
19
             $table->string('status')->default('active');
20
+            $table->text('status_reason')->nullable();
20
             $table->text('description')->nullable();
21
             $table->text('description')->nullable();
21
             $table->string('category')->default('tax');
22
             $table->string('category')->default('tax');
22
             $table->string('type')->default('sales');
23
             $table->string('type')->default('sales');
26
             $table->string('scope')->nullable();
27
             $table->string('scope')->nullable();
27
             $table->dateTime('start_date')->nullable();
28
             $table->dateTime('start_date')->nullable();
28
             $table->dateTime('end_date')->nullable();
29
             $table->dateTime('end_date')->nullable();
30
+            $table->timestamp('paused_at')->nullable();
31
+            $table->timestamp('paused_until')->nullable();
32
+            $table->timestamp('archived_at')->nullable();
29
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
33
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
30
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
34
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
31
             $table->timestamps();
35
             $table->timestamps();

Notiek ielāde…
Atcelt
Saglabāt