Selaa lähdekoodia

wip budgets

3.x
Andrew Wallo 7 kuukautta sitten
vanhempi
commit
abe1226736

+ 154
- 0
app/Filament/Company/Resources/Accounting/BudgetResource.php Näytä tiedosto

@@ -0,0 +1,154 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting;
4
+
5
+use App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
6
+use App\Models\Accounting\Account;
7
+use App\Models\Accounting\Budget;
8
+use Awcodes\TableRepeater\Components\TableRepeater;
9
+use Awcodes\TableRepeater\Header;
10
+use Filament\Forms;
11
+use Filament\Forms\Form;
12
+use Filament\Resources\Resource;
13
+use Filament\Tables;
14
+use Filament\Tables\Table;
15
+
16
+class BudgetResource extends Resource
17
+{
18
+    protected static ?string $model = Budget::class;
19
+
20
+    protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
21
+
22
+    public static function form(Form $form): Form
23
+    {
24
+        return $form
25
+            ->schema([
26
+                Forms\Components\Section::make('Budget Details')
27
+                    ->schema([
28
+                        Forms\Components\TextInput::make('name')
29
+                            ->required()
30
+                            ->maxLength(255),
31
+
32
+                        Forms\Components\Grid::make(2)->schema([
33
+                            Forms\Components\DatePicker::make('start_date')->required(),
34
+                            Forms\Components\DatePicker::make('end_date')->required(),
35
+                        ]),
36
+
37
+                        Forms\Components\Select::make('interval_type')
38
+                            ->label('Budget Interval')
39
+                            ->options([
40
+                                'day' => 'Daily',
41
+                                'week' => 'Weekly',
42
+                                'month' => 'Monthly',
43
+                                'quarter' => 'Quarterly',
44
+                                'year' => 'Yearly',
45
+                            ])
46
+                            ->default('month')
47
+                            ->required()
48
+                            ->live(),
49
+
50
+                        Forms\Components\Textarea::make('notes')->columnSpanFull(),
51
+                    ]),
52
+
53
+                Forms\Components\Section::make('Budget Items')
54
+                    ->schema([
55
+                        TableRepeater::make('budgetItems')
56
+                            ->relationship()
57
+                            ->saveRelationshipsUsing(null)
58
+                            ->dehydrated(true)
59
+                            ->headers(fn (Forms\Get $get) => self::getHeaders($get('interval_type')))
60
+                            ->schema([
61
+                                Forms\Components\Select::make('account_id')
62
+                                    ->label('Account')
63
+                                    ->options(Account::query()->pluck('name', 'id'))
64
+                                    ->searchable()
65
+                                    ->required(),
66
+
67
+                                Forms\Components\Grid::make(2)->schema([
68
+                                    Forms\Components\DatePicker::make('start_date')->required(),
69
+                                    Forms\Components\DatePicker::make('end_date')->required(),
70
+                                ]),
71
+
72
+                                Forms\Components\TextInput::make('amount')
73
+                                    ->numeric()
74
+                                    ->suffix('USD')
75
+                                    ->required(),
76
+                            ])
77
+                            ->defaultItems(1)
78
+                            ->addActionLabel('Add Budget Item'),
79
+                    ]),
80
+            ]);
81
+    }
82
+
83
+    public static function table(Table $table): Table
84
+    {
85
+        return $table
86
+            ->columns([
87
+                //
88
+            ])
89
+            ->filters([
90
+                //
91
+            ])
92
+            ->actions([
93
+                Tables\Actions\ViewAction::make(),
94
+                Tables\Actions\EditAction::make(),
95
+            ])
96
+            ->bulkActions([
97
+                Tables\Actions\BulkActionGroup::make([
98
+                    Tables\Actions\DeleteBulkAction::make(),
99
+                ]),
100
+            ]);
101
+    }
102
+
103
+    private static function getHeaders(?string $intervalType): array
104
+    {
105
+        $headers = [
106
+            Header::make('Account')->width('20%'),
107
+            Header::make('Start Date')->width('15%'),
108
+            Header::make('End Date')->width('15%'),
109
+        ];
110
+
111
+        // Adjust the number of columns dynamically based on interval type
112
+        switch ($intervalType) {
113
+            case 'day':
114
+                $headers[] = Header::make('Daily Budget')->width('20%')->align('right');
115
+
116
+                break;
117
+            case 'week':
118
+                $headers[] = Header::make('Weekly Budget')->width('20%')->align('right');
119
+
120
+                break;
121
+            case 'month':
122
+                $headers[] = Header::make('Monthly Budget')->width('20%')->align('right');
123
+
124
+                break;
125
+            case 'quarter':
126
+                $headers[] = Header::make('Quarterly Budget')->width('20%')->align('right');
127
+
128
+                break;
129
+            case 'year':
130
+                $headers[] = Header::make('Yearly Budget')->width('20%')->align('right');
131
+
132
+                break;
133
+        }
134
+
135
+        return $headers;
136
+    }
137
+
138
+    public static function getRelations(): array
139
+    {
140
+        return [
141
+            //
142
+        ];
143
+    }
144
+
145
+    public static function getPages(): array
146
+    {
147
+        return [
148
+            'index' => Pages\ListBudgets::route('/'),
149
+            'create' => Pages\CreateBudget::route('/create'),
150
+            'view' => Pages\ViewBudget::route('/{record}'),
151
+            'edit' => Pages\EditBudget::route('/{record}/edit'),
152
+        ];
153
+    }
154
+}

+ 11
- 0
app/Filament/Company/Resources/Accounting/BudgetResource/Pages/CreateBudget.php Näytä tiedosto

@@ -0,0 +1,11 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
4
+
5
+use App\Filament\Company\Resources\Accounting\BudgetResource;
6
+use Filament\Resources\Pages\CreateRecord;
7
+
8
+class CreateBudget extends CreateRecord
9
+{
10
+    protected static string $resource = BudgetResource::class;
11
+}

+ 20
- 0
app/Filament/Company/Resources/Accounting/BudgetResource/Pages/EditBudget.php Näytä tiedosto

@@ -0,0 +1,20 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
4
+
5
+use App\Filament\Company\Resources\Accounting\BudgetResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\EditRecord;
8
+
9
+class EditBudget extends EditRecord
10
+{
11
+    protected static string $resource = BudgetResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\ViewAction::make(),
17
+            Actions\DeleteAction::make(),
18
+        ];
19
+    }
20
+}

+ 19
- 0
app/Filament/Company/Resources/Accounting/BudgetResource/Pages/ListBudgets.php Näytä tiedosto

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
4
+
5
+use App\Filament\Company\Resources\Accounting\BudgetResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListBudgets extends ListRecords
10
+{
11
+    protected static string $resource = BudgetResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make(),
17
+        ];
18
+    }
19
+}

+ 19
- 0
app/Filament/Company/Resources/Accounting/BudgetResource/Pages/ViewBudget.php Näytä tiedosto

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
4
+
5
+use App\Filament\Company\Resources\Accounting\BudgetResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ViewRecord;
8
+
9
+class ViewBudget extends ViewRecord
10
+{
11
+    protected static string $resource = BudgetResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\EditAction::make(),
17
+        ];
18
+    }
19
+}

+ 25
- 4
app/Models/Accounting/Budget.php Näytä tiedosto

@@ -5,6 +5,7 @@ namespace App\Models\Accounting;
5 5
 use App\Concerns\Blamable;
6 6
 use App\Concerns\CompanyOwned;
7 7
 use App\Enums\Accounting\BudgetStatus;
8
+use App\Filament\Company\Resources\Accounting\BudgetResource;
8 9
 use Filament\Actions\Action;
9 10
 use Filament\Actions\MountableAction;
10 11
 use Filament\Actions\ReplicateAction;
@@ -13,6 +14,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
13 14
 use Illuminate\Database\Eloquent\Factories\HasFactory;
14 15
 use Illuminate\Database\Eloquent\Model;
15 16
 use Illuminate\Database\Eloquent\Relations\HasMany;
17
+use Illuminate\Database\Eloquent\Relations\HasManyThrough;
16 18
 use Illuminate\Support\Carbon;
17 19
 
18 20
 class Budget extends Model
@@ -48,6 +50,11 @@ class Budget extends Model
48 50
         return $this->hasMany(BudgetItem::class);
49 51
     }
50 52
 
53
+    public function allocations(): HasManyThrough
54
+    {
55
+        return $this->hasManyThrough(BudgetAllocation::class, BudgetItem::class);
56
+    }
57
+
51 58
     public function isDraft(): bool
52 59
     {
53 60
         return $this->status === BudgetStatus::Draft;
@@ -88,6 +95,11 @@ class Budget extends Model
88 95
         return $this->budgetItems()->exists();
89 96
     }
90 97
 
98
+    public function hasAllocations(): bool
99
+    {
100
+        return $this->allocations()->exists();
101
+    }
102
+
91 103
     public function scopeDraft(Builder $query): Builder
92 104
     {
93 105
         return $query->where('status', BudgetStatus::Draft);
@@ -245,13 +257,10 @@ class Budget extends Model
245 257
             ->beforeReplicaSaved(function (self $original, self $replica) {
246 258
                 $replica->status = BudgetStatus::Draft;
247 259
                 $replica->name = $replica->name . ' (Copy)';
248
-                // Optionally adjust dates for the new budget period
249
-                // $replica->start_date = now()->startOfMonth();
250
-                // $replica->end_date = now()->endOfYear();
251 260
             })
252 261
             ->databaseTransaction()
253 262
             ->after(function (self $original, self $replica) {
254
-                // Clone budget items
263
+                // Clone budget items and their allocations
255 264
                 $original->budgetItems->each(function (BudgetItem $item) use ($replica) {
256 265
                     $newItem = $item->replicate([
257 266
                         'budget_id',
@@ -263,6 +272,18 @@ class Budget extends Model
263 272
 
264 273
                     $newItem->budget_id = $replica->id;
265 274
                     $newItem->save();
275
+
276
+                    // Clone the allocations for this budget item
277
+                    $item->allocations->each(function (BudgetAllocation $allocation) use ($newItem) {
278
+                        $newAllocation = $allocation->replicate([
279
+                            'budget_item_id',
280
+                            'created_at',
281
+                            'updated_at',
282
+                        ]);
283
+
284
+                        $newAllocation->budget_item_id = $newItem->id;
285
+                        $newAllocation->save();
286
+                    });
266 287
                 });
267 288
             })
268 289
             ->successRedirectUrl(static function (self $replica) {

+ 36
- 0
app/Models/Accounting/BudgetAllocation.php Näytä tiedosto

@@ -0,0 +1,36 @@
1
+<?php
2
+
3
+namespace App\Models\Accounting;
4
+
5
+use App\Casts\MoneyCast;
6
+use App\Concerns\CompanyOwned;
7
+use Illuminate\Database\Eloquent\Factories\HasFactory;
8
+use Illuminate\Database\Eloquent\Model;
9
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
10
+
11
+class BudgetAllocation extends Model
12
+{
13
+    use CompanyOwned;
14
+    use HasFactory;
15
+
16
+    protected $fillable = [
17
+        'company_id',
18
+        'budget_item_id',
19
+        'period',
20
+        'interval_type',
21
+        'start_date',
22
+        'end_date',
23
+        'amount',
24
+    ];
25
+
26
+    protected $casts = [
27
+        'start_date' => 'date',
28
+        'end_date' => 'date',
29
+        'amount' => MoneyCast::class,
30
+    ];
31
+
32
+    public function budgetItem(): BelongsTo
33
+    {
34
+        return $this->belongsTo(BudgetItem::class);
35
+    }
36
+}

+ 3
- 52
app/Models/Accounting/BudgetItem.php Näytä tiedosto

@@ -2,13 +2,12 @@
2 2
 
3 3
 namespace App\Models\Accounting;
4 4
 
5
-use App\Casts\MoneyCast;
6 5
 use App\Concerns\Blamable;
7 6
 use App\Concerns\CompanyOwned;
8
-use Illuminate\Database\Eloquent\Casts\Attribute;
9 7
 use Illuminate\Database\Eloquent\Factories\HasFactory;
10 8
 use Illuminate\Database\Eloquent\Model;
11 9
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
10
+use Illuminate\Database\Eloquent\Relations\HasMany;
12 11
 
13 12
 class BudgetItem extends Model
14 13
 {
@@ -20,19 +19,10 @@ class BudgetItem extends Model
20 19
         'company_id',
21 20
         'budget_id',
22 21
         'account_id',
23
-        'start_date',
24
-        'end_date',
25
-        'amount', // in cents
26 22
         'created_by',
27 23
         'updated_by',
28 24
     ];
29 25
 
30
-    protected $casts = [
31
-        'start_date' => 'date',
32
-        'end_date' => 'date',
33
-        'amount' => MoneyCast::class,
34
-    ];
35
-
36 26
     public function budget(): BelongsTo
37 27
     {
38 28
         return $this->belongsTo(Budget::class);
@@ -43,47 +33,8 @@ class BudgetItem extends Model
43 33
         return $this->belongsTo(Account::class);
44 34
     }
45 35
 
46
-    /**
47
-     * Get actual transaction amount for this account and date range.
48
-     */
49
-    protected function actualAmount(): Attribute
50
-    {
51
-        return Attribute::make(
52
-            get: function () {
53
-                return Transaction::whereHas('journalEntries', function ($query) {
54
-                    $query->where('account_id', $this->account_id);
55
-                })
56
-                    ->whereBetween('posted_at', [$this->start_date, $this->end_date])
57
-                    ->sum('amount');
58
-            }
59
-        );
60
-    }
61
-
62
-    /**
63
-     * Get variance (budget - actual) for this budget item.
64
-     */
65
-    protected function variance(): Attribute
36
+    public function allocations(): HasMany
66 37
     {
67
-        return Attribute::make(
68
-            get: function () {
69
-                return $this->amount - $this->actualAmount;
70
-            }
71
-        );
72
-    }
73
-
74
-    /**
75
-     * Get variance percentage for this budget item.
76
-     */
77
-    protected function variancePercentage(): Attribute
78
-    {
79
-        return Attribute::make(
80
-            get: function () {
81
-                if ($this->amount == 0) {
82
-                    return 0;
83
-                }
84
-
85
-                return ($this->variance / $this->amount) * 100;
86
-            }
87
-        );
38
+        return $this->hasMany(BudgetAllocation::class);
88 39
     }
89 40
 }

+ 2
- 0
app/Providers/Filament/CompanyPanelProvider.php Näytä tiedosto

@@ -25,6 +25,7 @@ use App\Filament\Company\Pages\ManageCompany;
25 25
 use App\Filament\Company\Pages\Reports;
26 26
 use App\Filament\Company\Pages\Service\ConnectedAccount;
27 27
 use App\Filament\Company\Pages\Service\LiveCurrency;
28
+use App\Filament\Company\Resources\Accounting\BudgetResource;
28 29
 use App\Filament\Company\Resources\Banking\AccountResource;
29 30
 use App\Filament\Company\Resources\Common\OfferingResource;
30 31
 use App\Filament\Company\Resources\Purchases\BillResource;
@@ -150,6 +151,7 @@ class CompanyPanelProvider extends PanelProvider
150 151
                             ->icon('heroicon-o-clipboard-document-list')
151 152
                             ->extraSidebarAttributes(['class' => 'es-sidebar-group'])
152 153
                             ->items([
154
+                                ...BudgetResource::getNavigationItems(),
153 155
                                 ...AccountChart::getNavigationItems(),
154 156
                                 ...Transactions::getNavigationItems(),
155 157
                             ]),

+ 23
- 0
database/factories/Accounting/BudgetAllocationFactory.php Näytä tiedosto

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace Database\Factories\Accounting;
4
+
5
+use Illuminate\Database\Eloquent\Factories\Factory;
6
+
7
+/**
8
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Accounting\BudgetAllocation>
9
+ */
10
+class BudgetAllocationFactory extends Factory
11
+{
12
+    /**
13
+     * Define the model's default state.
14
+     *
15
+     * @return array<string, mixed>
16
+     */
17
+    public function definition(): array
18
+    {
19
+        return [
20
+            //
21
+        ];
22
+    }
23
+}

+ 0
- 3
database/migrations/2025_03_15_191321_create_budget_items_table.php Näytä tiedosto

@@ -16,9 +16,6 @@ return new class extends Migration
16 16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17 17
             $table->foreignId('budget_id')->constrained()->cascadeOnDelete();
18 18
             $table->foreignId('account_id')->constrained('accounts')->cascadeOnDelete();
19
-            $table->date('start_date');
20
-            $table->date('end_date');
21
-            $table->bigInteger('amount')->default(0); // in cents
22 19
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
23 20
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
24 21
             $table->timestamps();

+ 34
- 0
database/migrations/2025_03_15_203454_create_budget_allocations_table.php Näytä tiedosto

@@ -0,0 +1,34 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('budget_allocations', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
+            $table->foreignId('budget_item_id')->constrained()->cascadeOnDelete();
18
+            $table->string('period'); // e.g., 'Jan 2024', 'Q1 2024', '2024'
19
+            $table->string('interval_type'); // 'month', 'quarter', 'year'
20
+            $table->date('start_date'); // Period start
21
+            $table->date('end_date'); // Period end
22
+            $table->bigInteger('amount')->default(0); // Stored in cents
23
+            $table->timestamps();
24
+        });
25
+    }
26
+
27
+    /**
28
+     * Reverse the migrations.
29
+     */
30
+    public function down(): void
31
+    {
32
+        Schema::dropIfExists('budget_allocations');
33
+    }
34
+};

Loading…
Peruuta
Tallenna