Andrew Wallo 9 mesi fa
parent
commit
e71409ec87

+ 0
- 37
app/Concerns/GeneratesDocumentNumbers.php Vedi File

@@ -1,37 +0,0 @@
1
-<?php
2
-
3
-namespace App\Concerns;
4
-
5
-use RuntimeException;
6
-
7
-trait GeneratesDocumentNumbers
8
-{
9
-    public static function getNextDocumentNumber(): string
10
-    {
11
-        $company = auth()->user()->currentCompany;
12
-
13
-        if (! $company) {
14
-            throw new RuntimeException('No current company is set for the user.');
15
-        }
16
-
17
-        $settings = $company->{static::getSettingsKey()};
18
-        $numberField = static::getDocumentNumberField();
19
-
20
-        $latestDocument = static::query()
21
-            ->whereNotNull($numberField)
22
-            ->latest($numberField)
23
-            ->first();
24
-
25
-        $lastNumberPart = $latestDocument
26
-            ? (int) substr($latestDocument->{$numberField}, strlen($settings->number_prefix))
27
-            : 0;
28
-
29
-        return $settings->getNumberNext(
30
-            padded: true,
31
-            format: true,
32
-            prefix: $settings->number_prefix,
33
-            digits: $settings->number_digits,
34
-            next: $lastNumberPart + 1
35
-        );
36
-    }
37
-}

+ 0
- 32
app/Concerns/HandlesCurrencyConversion.php Vedi File

@@ -1,32 +0,0 @@
1
-<?php
2
-
3
-namespace App\Concerns;
4
-
5
-use App\Utilities\Currency\CurrencyAccessor;
6
-use App\Utilities\Currency\CurrencyConverter;
7
-
8
-trait HandlesCurrencyConversion
9
-{
10
-    public function convertAmountToDefaultCurrency(int $amountCents): int
11
-    {
12
-        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
13
-        $needsConversion = $this->currency_code !== $defaultCurrency;
14
-
15
-        if ($needsConversion) {
16
-            return CurrencyConverter::convertBalance(
17
-                $amountCents,
18
-                $this->currency_code,
19
-                $defaultCurrency
20
-            );
21
-        }
22
-
23
-        return $amountCents;
24
-    }
25
-
26
-    public function formatAmountToDefaultCurrency(int $amountCents): string
27
-    {
28
-        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
29
-
30
-        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
31
-    }
32
-}

+ 0
- 37
app/Enums/Accounting/EstimateStatus.php Vedi File

@@ -1,37 +0,0 @@
1
-<?php
2
-
3
-namespace App\Enums\Accounting;
4
-
5
-use App\Enums\Concerns\ParsesEnum;
6
-use Filament\Support\Contracts\HasColor;
7
-use Filament\Support\Contracts\HasLabel;
8
-
9
-enum EstimateStatus: string implements HasColor, HasLabel
10
-{
11
-    use ParsesEnum;
12
-
13
-    case Draft = 'draft';
14
-    case Sent = 'sent';
15
-    case Approved = 'approved';
16
-    case Accepted = 'accepted';
17
-    case Rejected = 'rejected';
18
-    case Expired = 'expired';
19
-    case Converted = 'converted';
20
-
21
-    public function getLabel(): ?string
22
-    {
23
-        return $this->name;
24
-    }
25
-
26
-    public function getColor(): string | array | null
27
-    {
28
-        return match ($this) {
29
-            self::Draft => 'gray',
30
-            self::Sent => 'primary',
31
-            self::Approved, self::Accepted => 'success',
32
-            self::Rejected => 'danger',
33
-            self::Expired => 'warning',
34
-            self::Converted => 'info',
35
-        };
36
-    }
37
-}

+ 173
- 27
app/Models/Accounting/Bill.php Vedi File

@@ -2,66 +2,118 @@
2 2
 
3 3
 namespace App\Models\Accounting;
4 4
 
5
+use App\Casts\MoneyCast;
6
+use App\Casts\RateCast;
7
+use App\Collections\Accounting\DocumentCollection;
8
+use App\Concerns\Blamable;
9
+use App\Concerns\CompanyOwned;
10
+use App\Enums\Accounting\AdjustmentComputation;
5 11
 use App\Enums\Accounting\BillStatus;
6
-use App\Enums\Accounting\DocumentType;
12
+use App\Enums\Accounting\DocumentDiscountMethod;
7 13
 use App\Enums\Accounting\JournalEntryType;
8 14
 use App\Enums\Accounting\TransactionType;
9 15
 use App\Filament\Company\Resources\Purchases\BillResource;
16
+use App\Models\Banking\BankAccount;
10 17
 use App\Models\Common\Vendor;
11
-use App\Models\Setting\DocumentDefault;
18
+use App\Models\Setting\Currency;
12 19
 use App\Observers\BillObserver;
13 20
 use App\Utilities\Currency\CurrencyAccessor;
14 21
 use App\Utilities\Currency\CurrencyConverter;
15 22
 use Filament\Actions\MountableAction;
16 23
 use Filament\Actions\ReplicateAction;
24
+use Illuminate\Database\Eloquent\Attributes\CollectedBy;
17 25
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
18 26
 use Illuminate\Database\Eloquent\Builder;
27
+use Illuminate\Database\Eloquent\Casts\Attribute;
28
+use Illuminate\Database\Eloquent\Factories\HasFactory;
29
+use Illuminate\Database\Eloquent\Model;
19 30
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
31
+use Illuminate\Database\Eloquent\Relations\MorphMany;
20 32
 use Illuminate\Database\Eloquent\Relations\MorphOne;
21 33
 use Illuminate\Support\Carbon;
22 34
 
23 35
 #[ObservedBy(BillObserver::class)]
24
-class Bill extends Document
36
+#[CollectedBy(DocumentCollection::class)]
37
+class Bill extends Model
25 38
 {
39
+    use Blamable;
40
+    use CompanyOwned;
41
+    use HasFactory;
42
+
26 43
     protected $table = 'bills';
27 44
 
28 45
     protected $fillable = [
29
-        ...self::COMMON_FILLABLE,
30
-        ...self::BILL_FILLABLE,
31
-    ];
32
-
33
-    protected const BILL_FILLABLE = [
46
+        'company_id',
34 47
         'vendor_id',
35 48
         'bill_number',
49
+        'order_number',
50
+        'date',
51
+        'due_date',
52
+        'paid_at',
53
+        'status',
54
+        'currency_code',
55
+        'discount_method',
56
+        'discount_computation',
57
+        'discount_rate',
58
+        'subtotal',
59
+        'tax_total',
60
+        'discount_total',
61
+        'total',
62
+        'amount_paid',
36 63
         'notes',
64
+        'created_by',
65
+        'updated_by',
37 66
     ];
38 67
 
39
-    protected function casts(): array
68
+    protected $casts = [
69
+        'date' => 'date',
70
+        'due_date' => 'date',
71
+        'paid_at' => 'datetime',
72
+        'status' => BillStatus::class,
73
+        'discount_method' => DocumentDiscountMethod::class,
74
+        'discount_computation' => AdjustmentComputation::class,
75
+        'discount_rate' => RateCast::class,
76
+        'subtotal' => MoneyCast::class,
77
+        'tax_total' => MoneyCast::class,
78
+        'discount_total' => MoneyCast::class,
79
+        'total' => MoneyCast::class,
80
+        'amount_paid' => MoneyCast::class,
81
+        'amount_due' => MoneyCast::class,
82
+    ];
83
+
84
+    public function currency(): BelongsTo
40 85
     {
41
-        return [
42
-            ...parent::casts(),
43
-            'status' => BillStatus::class,
44
-        ];
86
+        return $this->belongsTo(Currency::class, 'currency_code', 'code');
45 87
     }
46 88
 
47
-    public static function documentNumberColumn(): string
89
+    public function vendor(): BelongsTo
48 90
     {
49
-        return 'bill_number';
91
+        return $this->belongsTo(Vendor::class);
50 92
     }
51 93
 
52
-    public static function documentType(): DocumentType
94
+    public function lineItems(): MorphMany
53 95
     {
54
-        return DocumentType::Bill;
96
+        return $this->morphMany(DocumentLineItem::class, 'documentable');
55 97
     }
56 98
 
57
-    public static function getDocumentSettings(): DocumentDefault
99
+    public function transactions(): MorphMany
58 100
     {
59
-        return auth()->user()->currentCompany->defaultBill;
101
+        return $this->morphMany(Transaction::class, 'transactionable');
60 102
     }
61 103
 
62
-    public function vendor(): BelongsTo
104
+    public function payments(): MorphMany
63 105
     {
64
-        return $this->belongsTo(Vendor::class);
106
+        return $this->transactions()->where('is_payment', true);
107
+    }
108
+
109
+    public function deposits(): MorphMany
110
+    {
111
+        return $this->transactions()->where('type', TransactionType::Deposit)->where('is_payment', true);
112
+    }
113
+
114
+    public function withdrawals(): MorphMany
115
+    {
116
+        return $this->transactions()->where('type', TransactionType::Withdrawal)->where('is_payment', true);
65 117
     }
66 118
 
67 119
     public function initialTransaction(): MorphOne
@@ -70,6 +122,13 @@ class Bill extends Document
70 122
             ->where('type', TransactionType::Journal);
71 123
     }
72 124
 
125
+    protected function isCurrentlyOverdue(): Attribute
126
+    {
127
+        return Attribute::get(function () {
128
+            return $this->due_date->isBefore(today()) && $this->canBeOverdue();
129
+        });
130
+    }
131
+
73 132
     public function canBeOverdue(): bool
74 133
     {
75 134
         return in_array($this->status, BillStatus::canBeOverdue());
@@ -83,6 +142,44 @@ class Bill extends Document
83 142
         ]) && $this->currency_code === CurrencyAccessor::getDefaultCurrency();
84 143
     }
85 144
 
145
+    public function hasPayments(): bool
146
+    {
147
+        return $this->payments->isNotEmpty();
148
+    }
149
+
150
+    public static function getNextDocumentNumber(): string
151
+    {
152
+        $company = auth()->user()->currentCompany;
153
+
154
+        if (! $company) {
155
+            throw new \RuntimeException('No current company is set for the user.');
156
+        }
157
+
158
+        $defaultBillSettings = $company->defaultBill;
159
+
160
+        $numberPrefix = $defaultBillSettings->number_prefix;
161
+        $numberDigits = $defaultBillSettings->number_digits;
162
+
163
+        $latestDocument = static::query()
164
+            ->whereNotNull('bill_number')
165
+            ->latest('bill_number')
166
+            ->first();
167
+
168
+        $lastNumberNumericPart = $latestDocument
169
+            ? (int) substr($latestDocument->bill_number, strlen($numberPrefix))
170
+            : 0;
171
+
172
+        $numberNext = $lastNumberNumericPart + 1;
173
+
174
+        return $defaultBillSettings->getNumberNext(
175
+            padded: true,
176
+            format: true,
177
+            prefix: $numberPrefix,
178
+            digits: $numberDigits,
179
+            next: $numberNext
180
+        );
181
+    }
182
+
86 183
     public function hasInitialTransaction(): bool
87 184
     {
88 185
         return $this->initialTransaction()->exists();
@@ -99,14 +196,44 @@ class Bill extends Document
99 196
 
100 197
     public function recordPayment(array $data): void
101 198
     {
199
+        $transactionType = TransactionType::Withdrawal;
102 200
         $transactionDescription = "Bill #{$this->bill_number}: Payment to {$this->vendor->name}";
103 201
 
104
-        $this->recordTransaction(
105
-            $data,
106
-            TransactionType::Withdrawal, // Always withdrawal for bills
107
-            $transactionDescription,
108
-            Account::getAccountsPayableAccount()->id // Account ID specific to bills
109
-        );
202
+        // Add multi-currency handling
203
+        $bankAccount = BankAccount::findOrFail($data['bank_account_id']);
204
+        $bankAccountCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
205
+
206
+        $billCurrency = $this->currency_code;
207
+        $requiresConversion = $billCurrency !== $bankAccountCurrency;
208
+
209
+        if ($requiresConversion) {
210
+            $amountInBillCurrencyCents = CurrencyConverter::convertToCents($data['amount'], $billCurrency);
211
+            $amountInBankCurrencyCents = CurrencyConverter::convertBalance(
212
+                $amountInBillCurrencyCents,
213
+                $billCurrency,
214
+                $bankAccountCurrency
215
+            );
216
+            $formattedAmountForBankCurrency = CurrencyConverter::convertCentsToFormatSimple(
217
+                $amountInBankCurrencyCents,
218
+                $bankAccountCurrency
219
+            );
220
+        } else {
221
+            $formattedAmountForBankCurrency = $data['amount']; // Already in simple format
222
+        }
223
+
224
+        // Create transaction with converted amount
225
+        $this->transactions()->create([
226
+            'company_id' => $this->company_id,
227
+            'type' => $transactionType,
228
+            'is_payment' => true,
229
+            'posted_at' => $data['posted_at'],
230
+            'amount' => $formattedAmountForBankCurrency,
231
+            'payment_method' => $data['payment_method'],
232
+            'bank_account_id' => $data['bank_account_id'],
233
+            'account_id' => Account::getAccountsPayableAccount()->id,
234
+            'description' => $transactionDescription,
235
+            'notes' => $data['notes'] ?? null,
236
+        ]);
110 237
     }
111 238
 
112 239
     public function createInitialTransaction(?Carbon $postedAt = null): void
@@ -208,6 +335,25 @@ class Bill extends Document
208 335
         $this->createInitialTransaction();
209 336
     }
210 337
 
338
+    public function convertAmountToDefaultCurrency(int $amountCents): int
339
+    {
340
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
341
+        $needsConversion = $this->currency_code !== $defaultCurrency;
342
+
343
+        if ($needsConversion) {
344
+            return CurrencyConverter::convertBalance($amountCents, $this->currency_code, $defaultCurrency);
345
+        }
346
+
347
+        return $amountCents;
348
+    }
349
+
350
+    public function formatAmountToDefaultCurrency(int $amountCents): string
351
+    {
352
+        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
353
+
354
+        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
355
+    }
356
+
211 357
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
212 358
     {
213 359
         return $action::make()

+ 0
- 223
app/Models/Accounting/Document.php Vedi File

@@ -1,223 +0,0 @@
1
-<?php
2
-
3
-namespace App\Models\Accounting;
4
-
5
-use App\Casts\MoneyCast;
6
-use App\Casts\RateCast;
7
-use App\Collections\Accounting\DocumentCollection;
8
-use App\Concerns\Blamable;
9
-use App\Concerns\CompanyOwned;
10
-use App\Enums\Accounting\AdjustmentComputation;
11
-use App\Enums\Accounting\DocumentDiscountMethod;
12
-use App\Enums\Accounting\DocumentType;
13
-use App\Enums\Accounting\TransactionType;
14
-use App\Models\Banking\BankAccount;
15
-use App\Models\Setting\Currency;
16
-use App\Models\Setting\DocumentDefault;
17
-use App\Utilities\Currency\CurrencyAccessor;
18
-use App\Utilities\Currency\CurrencyConverter;
19
-use Illuminate\Database\Eloquent\Casts\Attribute;
20
-use Illuminate\Database\Eloquent\Collection;
21
-use Illuminate\Database\Eloquent\Factories\HasFactory;
22
-use Illuminate\Database\Eloquent\Model;
23
-use Illuminate\Database\Eloquent\Relations\BelongsTo;
24
-use Illuminate\Database\Eloquent\Relations\MorphMany;
25
-
26
-abstract class Document extends Model
27
-{
28
-    use Blamable;
29
-    use CompanyOwned;
30
-    use HasFactory;
31
-
32
-    protected const COMMON_FILLABLE = [
33
-        'company_id',
34
-        'order_number',
35
-        'date',
36
-        'due_date',
37
-        'paid_at',
38
-        'status',
39
-        'currency_code',
40
-        'discount_method',
41
-        'discount_computation',
42
-        'discount_rate',
43
-        'subtotal',
44
-        'tax_total',
45
-        'discount_total',
46
-        'total',
47
-        'amount_paid',
48
-        'created_by',
49
-        'updated_by',
50
-    ];
51
-
52
-    protected $fillable = self::COMMON_FILLABLE;
53
-
54
-    protected function casts(): array
55
-    {
56
-        return [
57
-            'date' => 'date',
58
-            'due_date' => 'date',
59
-            'paid_at' => 'datetime',
60
-            'discount_method' => DocumentDiscountMethod::class,
61
-            'discount_computation' => AdjustmentComputation::class,
62
-            'discount_rate' => RateCast::class,
63
-            'subtotal' => MoneyCast::class,
64
-            'tax_total' => MoneyCast::class,
65
-            'discount_total' => MoneyCast::class,
66
-            'total' => MoneyCast::class,
67
-            'amount_paid' => MoneyCast::class,
68
-            'amount_due' => MoneyCast::class,
69
-        ];
70
-    }
71
-
72
-    public function currency(): BelongsTo
73
-    {
74
-        return $this->belongsTo(Currency::class, 'currency_code', 'code');
75
-    }
76
-
77
-    public function lineItems(): MorphMany
78
-    {
79
-        return $this->morphMany(DocumentLineItem::class, 'documentable');
80
-    }
81
-
82
-    public function transactions(): MorphMany
83
-    {
84
-        return $this->morphMany(Transaction::class, 'transactionable');
85
-    }
86
-
87
-    public function payments(): MorphMany
88
-    {
89
-        return $this->transactions()->where('is_payment', true);
90
-    }
91
-
92
-    public function deposits(): MorphMany
93
-    {
94
-        return $this->transactions()->where('type', TransactionType::Deposit)->where('is_payment', true);
95
-    }
96
-
97
-    public function withdrawals(): MorphMany
98
-    {
99
-        return $this->transactions()->where('type', TransactionType::Withdrawal)->where('is_payment', true);
100
-    }
101
-
102
-    public function hasPayments(): bool
103
-    {
104
-        return $this->payments->isNotEmpty();
105
-    }
106
-
107
-    protected function isCurrentlyOverdue(): Attribute
108
-    {
109
-        return Attribute::get(function () {
110
-            return $this->due_date->isBefore(today()) && $this->canBeOverdue();
111
-        });
112
-    }
113
-
114
-    public static function getNextDocumentNumber(): string
115
-    {
116
-        $company = auth()->user()->currentCompany;
117
-
118
-        if (! $company) {
119
-            throw new \RuntimeException('No current company is set for the user.');
120
-        }
121
-
122
-        $settings = static::getDocumentSettings();
123
-
124
-        $prefix = $settings->number_prefix;
125
-        $digits = $settings->number_digits;
126
-
127
-        $latestDocument = static::query()
128
-            ->whereNotNull(static::documentNumberColumn())
129
-            ->latest(static::documentNumberColumn())
130
-            ->first();
131
-
132
-        $lastNumber = $latestDocument
133
-            ? (int) substr($latestDocument->{static::documentNumberColumn()}, strlen($prefix))
134
-            : 0;
135
-
136
-        $numberNext = $lastNumber + 1;
137
-
138
-        return $settings->getNumberNext(
139
-            padded: true,
140
-            format: true,
141
-            prefix: $prefix,
142
-            digits: $digits,
143
-            next: $numberNext
144
-        );
145
-    }
146
-
147
-    protected function recordTransaction(array $data, string $transactionType, string $transactionDescription, int $accountId): void
148
-    {
149
-        $formattedAmount = $this->prepareAmountForTransaction($data);
150
-
151
-        $this->transactions()->create([
152
-            'company_id' => $this->company_id,
153
-            'type' => $transactionType,
154
-            'is_payment' => true,
155
-            'posted_at' => $data['posted_at'],
156
-            'amount' => $formattedAmount,
157
-            'payment_method' => $data['payment_method'],
158
-            'bank_account_id' => $data['bank_account_id'],
159
-            'account_id' => $accountId,
160
-            'description' => $transactionDescription,
161
-            'notes' => $data['notes'] ?? null,
162
-        ]);
163
-    }
164
-
165
-    protected function prepareAmountForTransaction(array $data): string
166
-    {
167
-        $bankAccount = BankAccount::findOrFail($data['bank_account_id']);
168
-        $bankAccountCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
169
-
170
-        $requiresConversion = $this->currency_code !== $bankAccountCurrency;
171
-
172
-        if ($requiresConversion) {
173
-            $amountInCents = CurrencyConverter::convertToCents($data['amount'], $this->currency_code);
174
-            $convertedAmount = CurrencyConverter::convertBalance(
175
-                $amountInCents,
176
-                $this->currency_code,
177
-                $bankAccountCurrency
178
-            );
179
-
180
-            return CurrencyConverter::convertCentsToFormatSimple($convertedAmount, $bankAccountCurrency);
181
-        }
182
-
183
-        return $data['amount'];
184
-    }
185
-
186
-    public function convertAmountToDefaultCurrency(int $amountCents): int
187
-    {
188
-        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
189
-        $needsConversion = $this->currency_code !== $defaultCurrency;
190
-
191
-        if ($needsConversion) {
192
-            return CurrencyConverter::convertBalance($amountCents, $this->currency_code, $defaultCurrency);
193
-        }
194
-
195
-        return $amountCents;
196
-    }
197
-
198
-    public function formatAmountToDefaultCurrency(int $amountCents): string
199
-    {
200
-        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
201
-
202
-        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
203
-    }
204
-
205
-    /**
206
-     * Create a new Eloquent Collection instance.
207
-     *
208
-     * @param  array<int, Model>  $models
209
-     * @return Collection<int, Model>
210
-     */
211
-    public function newCollection(array $models = []): Collection
212
-    {
213
-        return new DocumentCollection($models);
214
-    }
215
-
216
-    abstract public static function documentType(): DocumentType;
217
-
218
-    abstract public static function documentNumberColumn(): string;
219
-
220
-    abstract public static function getDocumentSettings(): DocumentDefault;
221
-
222
-    abstract public function canBeOverdue(): bool;
223
-}

+ 0
- 61
app/Models/Accounting/Estimate.php Vedi File

@@ -1,61 +0,0 @@
1
-<?php
2
-
3
-namespace App\Models\Accounting;
4
-
5
-use App\Casts\MoneyCast;
6
-use App\Casts\RateCast;
7
-use App\Concerns\Blamable;
8
-use App\Concerns\CompanyOwned;
9
-use App\Enums\Accounting\AdjustmentComputation;
10
-use App\Enums\Accounting\DocumentDiscountMethod;
11
-use App\Enums\Accounting\EstimateStatus;
12
-use Illuminate\Database\Eloquent\Factories\HasFactory;
13
-use Illuminate\Database\Eloquent\Model;
14
-
15
-class Estimate extends Model
16
-{
17
-    use Blamable;
18
-    use CompanyOwned;
19
-    use HasFactory;
20
-
21
-    protected $fillable = [
22
-        'client_id',
23
-        'logo',
24
-        'header',
25
-        'subheader',
26
-        'number',
27
-        'reference_number',
28
-        'date',
29
-        'expiry_date',
30
-        'approved_at',
31
-        'accepted_at',
32
-        'last_sent',
33
-        'status',
34
-        'currency_code',
35
-        'discount_method',
36
-        'discount_computation',
37
-        'discount_rate',
38
-        'subtotal',
39
-        'tax_total',
40
-        'discount_total',
41
-        'total',
42
-        'terms',
43
-        'footer',
44
-    ];
45
-
46
-    protected $casts = [
47
-        'date' => 'date',
48
-        'expiry_date' => 'date',
49
-        'approved_at' => 'datetime',
50
-        'accepted_at' => 'datetime',
51
-        'last_sent' => 'datetime',
52
-        'status' => EstimateStatus::class,
53
-        'discount_method' => DocumentDiscountMethod::class,
54
-        'discount_computation' => AdjustmentComputation::class,
55
-        'discount_rate' => RateCast::class,
56
-        'subtotal' => MoneyCast::class,
57
-        'tax_total' => MoneyCast::class,
58
-        'discount_total' => MoneyCast::class,
59
-        'total' => MoneyCast::class,
60
-    ];
61
-}

+ 179
- 35
app/Models/Accounting/Invoice.php Vedi File

@@ -2,75 +2,127 @@
2 2
 
3 3
 namespace App\Models\Accounting;
4 4
 
5
-use App\Enums\Accounting\DocumentType;
5
+use App\Casts\MoneyCast;
6
+use App\Casts\RateCast;
7
+use App\Collections\Accounting\DocumentCollection;
8
+use App\Concerns\Blamable;
9
+use App\Concerns\CompanyOwned;
10
+use App\Enums\Accounting\AdjustmentComputation;
11
+use App\Enums\Accounting\DocumentDiscountMethod;
6 12
 use App\Enums\Accounting\InvoiceStatus;
7 13
 use App\Enums\Accounting\JournalEntryType;
8 14
 use App\Enums\Accounting\TransactionType;
9 15
 use App\Filament\Company\Resources\Sales\InvoiceResource;
16
+use App\Models\Banking\BankAccount;
10 17
 use App\Models\Common\Client;
11
-use App\Models\Setting\DocumentDefault;
18
+use App\Models\Setting\Currency;
12 19
 use App\Observers\InvoiceObserver;
13 20
 use App\Utilities\Currency\CurrencyAccessor;
14 21
 use App\Utilities\Currency\CurrencyConverter;
15 22
 use Filament\Actions\Action;
16 23
 use Filament\Actions\MountableAction;
17 24
 use Filament\Actions\ReplicateAction;
25
+use Illuminate\Database\Eloquent\Attributes\CollectedBy;
18 26
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
19 27
 use Illuminate\Database\Eloquent\Builder;
28
+use Illuminate\Database\Eloquent\Casts\Attribute;
29
+use Illuminate\Database\Eloquent\Factories\HasFactory;
30
+use Illuminate\Database\Eloquent\Model;
20 31
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
32
+use Illuminate\Database\Eloquent\Relations\MorphMany;
21 33
 use Illuminate\Database\Eloquent\Relations\MorphOne;
22 34
 use Illuminate\Support\Carbon;
23 35
 
24 36
 #[ObservedBy(InvoiceObserver::class)]
25
-class Invoice extends Document
37
+#[CollectedBy(DocumentCollection::class)]
38
+class Invoice extends Model
26 39
 {
40
+    use Blamable;
41
+    use CompanyOwned;
42
+    use HasFactory;
43
+
27 44
     protected $table = 'invoices';
28 45
 
29 46
     protected $fillable = [
30
-        ...self::COMMON_FILLABLE,
31
-        ...self::INVOICE_FILLABLE,
32
-    ];
33
-
34
-    protected const INVOICE_FILLABLE = [
47
+        'company_id',
35 48
         'client_id',
36 49
         'logo',
37 50
         'header',
38 51
         'subheader',
39 52
         'invoice_number',
53
+        'order_number',
54
+        'date',
55
+        'due_date',
40 56
         'approved_at',
57
+        'paid_at',
41 58
         'last_sent',
59
+        'status',
60
+        'currency_code',
61
+        'discount_method',
62
+        'discount_computation',
63
+        'discount_rate',
64
+        'subtotal',
65
+        'tax_total',
66
+        'discount_total',
67
+        'total',
68
+        'amount_paid',
42 69
         'terms',
43 70
         'footer',
71
+        'created_by',
72
+        'updated_by',
44 73
     ];
45 74
 
46
-    protected function casts(): array
75
+    protected $casts = [
76
+        'date' => 'date',
77
+        'due_date' => 'date',
78
+        'approved_at' => 'datetime',
79
+        'paid_at' => 'datetime',
80
+        'last_sent' => 'datetime',
81
+        'status' => InvoiceStatus::class,
82
+        'discount_method' => DocumentDiscountMethod::class,
83
+        'discount_computation' => AdjustmentComputation::class,
84
+        'discount_rate' => RateCast::class,
85
+        'subtotal' => MoneyCast::class,
86
+        'tax_total' => MoneyCast::class,
87
+        'discount_total' => MoneyCast::class,
88
+        'total' => MoneyCast::class,
89
+        'amount_paid' => MoneyCast::class,
90
+        'amount_due' => MoneyCast::class,
91
+    ];
92
+
93
+    public function client(): BelongsTo
47 94
     {
48
-        return [
49
-            ...parent::casts(),
50
-            'approved_at' => 'datetime',
51
-            'last_sent' => 'datetime',
52
-            'status' => InvoiceStatus::class,
53
-        ];
95
+        return $this->belongsTo(Client::class);
54 96
     }
55 97
 
56
-    public static function documentNumberColumn(): string
98
+    public function currency(): BelongsTo
57 99
     {
58
-        return 'invoice_number';
100
+        return $this->belongsTo(Currency::class, 'currency_code', 'code');
59 101
     }
60 102
 
61
-    public static function documentType(): DocumentType
103
+    public function lineItems(): MorphMany
62 104
     {
63
-        return DocumentType::Invoice;
105
+        return $this->morphMany(DocumentLineItem::class, 'documentable');
64 106
     }
65 107
 
66
-    public static function getDocumentSettings(): DocumentDefault
108
+    public function transactions(): MorphMany
67 109
     {
68
-        return auth()->user()->currentCompany->defaultInvoice;
110
+        return $this->morphMany(Transaction::class, 'transactionable');
69 111
     }
70 112
 
71
-    public function client(): BelongsTo
113
+    public function payments(): MorphMany
72 114
     {
73
-        return $this->belongsTo(Client::class);
115
+        return $this->transactions()->where('is_payment', true);
116
+    }
117
+
118
+    public function deposits(): MorphMany
119
+    {
120
+        return $this->transactions()->where('type', TransactionType::Deposit)->where('is_payment', true);
121
+    }
122
+
123
+    public function withdrawals(): MorphMany
124
+    {
125
+        return $this->transactions()->where('type', TransactionType::Withdrawal)->where('is_payment', true);
74 126
     }
75 127
 
76 128
     public function approvalTransaction(): MorphOne
@@ -89,6 +141,13 @@ class Invoice extends Document
89 141
         ]);
90 142
     }
91 143
 
144
+    protected function isCurrentlyOverdue(): Attribute
145
+    {
146
+        return Attribute::get(function () {
147
+            return $this->due_date->isBefore(today()) && $this->canBeOverdue();
148
+        });
149
+    }
150
+
92 151
     public function isDraft(): bool
93 152
     {
94 153
         return $this->status === InvoiceStatus::Draft;
@@ -118,24 +177,90 @@ class Invoice extends Document
118 177
         return in_array($this->status, InvoiceStatus::canBeOverdue());
119 178
     }
120 179
 
180
+    public function hasPayments(): bool
181
+    {
182
+        return $this->payments->isNotEmpty();
183
+    }
184
+
185
+    public static function getNextDocumentNumber(): string
186
+    {
187
+        $company = auth()->user()->currentCompany;
188
+
189
+        if (! $company) {
190
+            throw new \RuntimeException('No current company is set for the user.');
191
+        }
192
+
193
+        $defaultInvoiceSettings = $company->defaultInvoice;
194
+
195
+        $numberPrefix = $defaultInvoiceSettings->number_prefix;
196
+        $numberDigits = $defaultInvoiceSettings->number_digits;
197
+
198
+        $latestDocument = static::query()
199
+            ->whereNotNull('invoice_number')
200
+            ->latest('invoice_number')
201
+            ->first();
202
+
203
+        $lastNumberNumericPart = $latestDocument
204
+            ? (int) substr($latestDocument->invoice_number, strlen($numberPrefix))
205
+            : 0;
206
+
207
+        $numberNext = $lastNumberNumericPart + 1;
208
+
209
+        return $defaultInvoiceSettings->getNumberNext(
210
+            padded: true,
211
+            format: true,
212
+            prefix: $numberPrefix,
213
+            digits: $numberDigits,
214
+            next: $numberNext
215
+        );
216
+    }
217
+
121 218
     public function recordPayment(array $data): void
122 219
     {
123 220
         $isRefund = $this->status === InvoiceStatus::Overpaid;
124 221
 
125
-        $transactionType = $isRefund
126
-            ? TransactionType::Withdrawal // Refunds are withdrawals
127
-            : TransactionType::Deposit;  // Payments are deposits
222
+        if ($isRefund) {
223
+            $transactionType = TransactionType::Withdrawal;
224
+            $transactionDescription = "Invoice #{$this->invoice_number}: Refund to {$this->client->name}";
225
+        } else {
226
+            $transactionType = TransactionType::Deposit;
227
+            $transactionDescription = "Invoice #{$this->invoice_number}: Payment from {$this->client->name}";
228
+        }
128 229
 
129
-        $transactionDescription = $isRefund
130
-            ? "Invoice #{$this->invoice_number}: Refund to {$this->client->name}"
131
-            : "Invoice #{$this->invoice_number}: Payment from {$this->client->name}";
230
+        $bankAccount = BankAccount::findOrFail($data['bank_account_id']);
231
+        $bankAccountCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
232
+
233
+        $invoiceCurrency = $this->currency_code;
234
+        $requiresConversion = $invoiceCurrency !== $bankAccountCurrency;
235
+
236
+        if ($requiresConversion) {
237
+            $amountInInvoiceCurrencyCents = CurrencyConverter::convertToCents($data['amount'], $invoiceCurrency);
238
+            $amountInBankCurrencyCents = CurrencyConverter::convertBalance(
239
+                $amountInInvoiceCurrencyCents,
240
+                $invoiceCurrency,
241
+                $bankAccountCurrency
242
+            );
243
+            $formattedAmountForBankCurrency = CurrencyConverter::convertCentsToFormatSimple(
244
+                $amountInBankCurrencyCents,
245
+                $bankAccountCurrency
246
+            );
247
+        } else {
248
+            $formattedAmountForBankCurrency = $data['amount']; // Already in simple format
249
+        }
132 250
 
133
-        $this->recordTransaction(
134
-            $data,
135
-            $transactionType,
136
-            $transactionDescription,
137
-            Account::getAccountsReceivableAccount()->id // Account ID specific to invoices
138
-        );
251
+        // Create transaction
252
+        $this->transactions()->create([
253
+            'company_id' => $this->company_id,
254
+            'type' => $transactionType,
255
+            'is_payment' => true,
256
+            'posted_at' => $data['posted_at'],
257
+            'amount' => $formattedAmountForBankCurrency,
258
+            'payment_method' => $data['payment_method'],
259
+            'bank_account_id' => $data['bank_account_id'],
260
+            'account_id' => Account::getAccountsReceivableAccount()->id,
261
+            'description' => $transactionDescription,
262
+            'notes' => $data['notes'] ?? null,
263
+        ]);
139 264
     }
140 265
 
141 266
     public function approveDraft(?Carbon $approvedAt = null): void
@@ -241,6 +366,25 @@ class Invoice extends Document
241 366
         $this->createApprovalTransaction();
242 367
     }
243 368
 
369
+    public function convertAmountToDefaultCurrency(int $amountCents): int
370
+    {
371
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
372
+        $needsConversion = $this->currency_code !== $defaultCurrency;
373
+
374
+        if ($needsConversion) {
375
+            return CurrencyConverter::convertBalance($amountCents, $this->currency_code, $defaultCurrency);
376
+        }
377
+
378
+        return $amountCents;
379
+    }
380
+
381
+    public function formatAmountToDefaultCurrency(int $amountCents): string
382
+    {
383
+        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
384
+
385
+        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
386
+    }
387
+
244 388
     public static function getApproveDraftAction(string $action = Action::class): MountableAction
245 389
     {
246 390
         return $action::make('approveDraft')

+ 10
- 10
composer.lock Vedi File

@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.336.2",
500
+            "version": "3.336.0",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "954bfdfc048840ca34afe2a2e1cbcff6681989c4"
504
+                "reference": "5a62003bf183e32da038056a4b9077c27224e034"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/954bfdfc048840ca34afe2a2e1cbcff6681989c4",
509
-                "reference": "954bfdfc048840ca34afe2a2e1cbcff6681989c4",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5a62003bf183e32da038056a4b9077c27224e034",
509
+                "reference": "5a62003bf183e32da038056a4b9077c27224e034",
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.2"
592
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.336.0"
593 593
             },
594
-            "time": "2024-12-20T19:05:10+00:00"
594
+            "time": "2024-12-18T19:04:32+00:00"
595 595
         },
596 596
         {
597 597
             "name": "aws/aws-sdk-php-laravel",
@@ -4707,6 +4707,10 @@
4707 4707
             ],
4708 4708
             "type": "library",
4709 4709
             "extra": {
4710
+                "branch-alias": {
4711
+                    "dev-master": "3.x-dev",
4712
+                    "dev-2.x": "2.x-dev"
4713
+                },
4710 4714
                 "laravel": {
4711 4715
                     "providers": [
4712 4716
                         "Carbon\\Laravel\\ServiceProvider"
@@ -4716,10 +4720,6 @@
4716 4720
                     "includes": [
4717 4721
                         "extension.neon"
4718 4722
                     ]
4719
-                },
4720
-                "branch-alias": {
4721
-                    "dev-2.x": "2.x-dev",
4722
-                    "dev-master": "3.x-dev"
4723 4723
                 }
4724 4724
             },
4725 4725
             "autoload": {

+ 0
- 23
database/factories/Accounting/EstimateFactory.php Vedi File

@@ -1,23 +0,0 @@
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\Estimate>
9
- */
10
-class EstimateFactory 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
-}

+ 1
- 1
database/migrations/2024_11_27_221657_create_bills_table.php Vedi File

@@ -15,7 +15,7 @@ return new class extends Migration
15 15
             $table->id();
16 16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17 17
             $table->foreignId('vendor_id')->nullable()->constrained('vendors')->nullOnDelete();
18
-            $table->string('number')->nullable();
18
+            $table->string('bill_number')->nullable();
19 19
             $table->string('order_number')->nullable(); // PO, SO, etc.
20 20
             $table->date('date')->nullable();
21 21
             $table->date('due_date')->nullable();

+ 1
- 1
database/migrations/2024_11_27_223015_create_invoices_table.php Vedi File

@@ -18,7 +18,7 @@ return new class extends Migration
18 18
             $table->string('logo')->nullable();
19 19
             $table->string('header')->nullable();
20 20
             $table->string('subheader')->nullable();
21
-            $table->string('number')->nullable();
21
+            $table->string('invoice_number')->nullable();
22 22
             $table->string('order_number')->nullable(); // PO, SO, etc.
23 23
             $table->date('date')->nullable();
24 24
             $table->date('due_date')->nullable();

+ 0
- 52
database/migrations/2024_12_21_001553_create_estimates_table.php Vedi File

@@ -1,52 +0,0 @@
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('estimates', function (Blueprint $table) {
15
-            $table->id();
16
-            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
-            $table->foreignId('client_id')->nullable()->constrained('clients')->nullOnDelete();
18
-            $table->string('logo')->nullable();
19
-            $table->string('header')->nullable();
20
-            $table->string('subheader')->nullable();
21
-            $table->string('number')->nullable();
22
-            $table->string('reference_number')->nullable();
23
-            $table->date('date')->nullable();
24
-            $table->date('expiry_date')->nullable();
25
-            $table->timestamp('approved_at')->nullable();
26
-            $table->timestamp('accepted_at')->nullable();
27
-            $table->timestamp('last_sent')->nullable();
28
-            $table->string('status')->default('draft');
29
-            $table->string('currency_code')->nullable();
30
-            $table->string('discount_method')->default('per_line_item');
31
-            $table->string('discount_computation')->default('percentage');
32
-            $table->integer('discount_rate')->default(0);
33
-            $table->integer('subtotal')->default(0);
34
-            $table->integer('tax_total')->default(0);
35
-            $table->integer('discount_total')->default(0);
36
-            $table->integer('total')->default(0);
37
-            $table->text('terms')->nullable(); // terms, notes
38
-            $table->text('footer')->nullable();
39
-            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
40
-            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
41
-            $table->timestamps();
42
-        });
43
-    }
44
-
45
-    /**
46
-     * Reverse the migrations.
47
-     */
48
-    public function down(): void
49
-    {
50
-        Schema::dropIfExists('estimates');
51
-    }
52
-};

+ 92
- 95
package-lock.json Vedi File

@@ -558,9 +558,9 @@
558 558
             }
559 559
         },
560 560
         "node_modules/@rollup/rollup-android-arm-eabi": {
561
-            "version": "4.29.0",
562
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.0.tgz",
563
-            "integrity": "sha512-TnF0md3qWSRDlU96y9+0dd5RNrlXiQUp1K2pK1UpNmjeND+o9ts9Jxv3G6ntagkt8jVh0KAT1VYgU0nCz5gt2w==",
561
+            "version": "4.28.1",
562
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
563
+            "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
564 564
             "cpu": [
565 565
                 "arm"
566 566
             ],
@@ -572,9 +572,9 @@
572 572
             ]
573 573
         },
574 574
         "node_modules/@rollup/rollup-android-arm64": {
575
-            "version": "4.29.0",
576
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.0.tgz",
577
-            "integrity": "sha512-L/7oX07eY6ACt2NXDrku1JIPdf9VGV/DI92EjAd8FRDzMMub5hXFpT1OegBqimJh9xy9Vv+nToaVtZp4Ku9SEA==",
575
+            "version": "4.28.1",
576
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
577
+            "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
578 578
             "cpu": [
579 579
                 "arm64"
580 580
             ],
@@ -586,9 +586,9 @@
586 586
             ]
587 587
         },
588 588
         "node_modules/@rollup/rollup-darwin-arm64": {
589
-            "version": "4.29.0",
590
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.0.tgz",
591
-            "integrity": "sha512-I1ZucWPVS96hjAsMSJiGosHTqMulMynrmTN+Xde5OsLcU5SjE0xylBmQ/DbB2psJ+HasINrJYz8HQpojtAw2eA==",
589
+            "version": "4.28.1",
590
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
591
+            "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
592 592
             "cpu": [
593 593
                 "arm64"
594 594
             ],
@@ -600,9 +600,9 @@
600 600
             ]
601 601
         },
602 602
         "node_modules/@rollup/rollup-darwin-x64": {
603
-            "version": "4.29.0",
604
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.0.tgz",
605
-            "integrity": "sha512-CTZ+lHMsTbH1q/XLKzmnJWxl2r/1xdv7cnjwbi5v+95nVA1syikxWLvqur4nDoGDHjC8oNMBGurnQptpuFJHXA==",
603
+            "version": "4.28.1",
604
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
605
+            "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
606 606
             "cpu": [
607 607
                 "x64"
608 608
             ],
@@ -614,9 +614,9 @@
614 614
             ]
615 615
         },
616 616
         "node_modules/@rollup/rollup-freebsd-arm64": {
617
-            "version": "4.29.0",
618
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.0.tgz",
619
-            "integrity": "sha512-BB8+4OMzk2JiKL5+aK8A0pi9DPB5pkIBZWXr19+grdez9b0VKihvV432uSwuZLO0sI6zCyxak8NO3mZ1yjM1jA==",
617
+            "version": "4.28.1",
618
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
619
+            "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
620 620
             "cpu": [
621 621
                 "arm64"
622 622
             ],
@@ -628,9 +628,9 @@
628 628
             ]
629 629
         },
630 630
         "node_modules/@rollup/rollup-freebsd-x64": {
631
-            "version": "4.29.0",
632
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.0.tgz",
633
-            "integrity": "sha512-Udz9Uh26uEE6phGMG2++TfpsLK/z4cYJqrIOyVhig/PMoWiZLghpjZUQvsAylsoztbpg0/QmplkDAyyVq0x6Jg==",
631
+            "version": "4.28.1",
632
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
633
+            "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
634 634
             "cpu": [
635 635
                 "x64"
636 636
             ],
@@ -642,9 +642,9 @@
642 642
             ]
643 643
         },
644 644
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
645
-            "version": "4.29.0",
646
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.0.tgz",
647
-            "integrity": "sha512-IPSCTzP8GRYzY+siSnggIKrckC2U+kVXoen6eSHRDgU9a4EZCHHWWOiKio1EkieOOk2j6EvZaaHfQUCmt8UJBg==",
645
+            "version": "4.28.1",
646
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
647
+            "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
648 648
             "cpu": [
649 649
                 "arm"
650 650
             ],
@@ -656,9 +656,9 @@
656 656
             ]
657 657
         },
658 658
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
659
-            "version": "4.29.0",
660
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.0.tgz",
661
-            "integrity": "sha512-GvHPu0UIDx+ohyS8vTYnwoSVMM5BH3NO+JwQs6GWNCuQVlC5rKxnH2WClTGu3NxiIfhKLai08IKUwn3QbzX1UQ==",
659
+            "version": "4.28.1",
660
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
661
+            "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
662 662
             "cpu": [
663 663
                 "arm"
664 664
             ],
@@ -670,9 +670,9 @@
670 670
             ]
671 671
         },
672 672
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
673
-            "version": "4.29.0",
674
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.0.tgz",
675
-            "integrity": "sha512-Pnnn/2CAZWcH9GQoj1nnr85Ejh7aNDe5MsEV0xhuFNUPF0SdnutJ7b2muOI5Kx12T0/i2ol5B/tlhMviZQDL3g==",
673
+            "version": "4.28.1",
674
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
675
+            "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
676 676
             "cpu": [
677 677
                 "arm64"
678 678
             ],
@@ -684,9 +684,9 @@
684 684
             ]
685 685
         },
686 686
         "node_modules/@rollup/rollup-linux-arm64-musl": {
687
-            "version": "4.29.0",
688
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.0.tgz",
689
-            "integrity": "sha512-AP+DLj4q9FT22ZL43ssA3gizEn7/MfJcZ1BOuyEPqoriuH3a8VRuDddN0MtpUwEtiZL6jc1GY5/eL99hkloQ1Q==",
687
+            "version": "4.28.1",
688
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
689
+            "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
690 690
             "cpu": [
691 691
                 "arm64"
692 692
             ],
@@ -698,9 +698,9 @@
698 698
             ]
699 699
         },
700 700
         "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
701
-            "version": "4.29.0",
702
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.0.tgz",
703
-            "integrity": "sha512-1+jPFClHmDATqbk0Cwi74KEOymVcs09Vbqe/CTKqLwCP0TeP2CACfnMnjYBs5CJgO20e/4bxFtmbR/9fKE1gug==",
701
+            "version": "4.28.1",
702
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
703
+            "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
704 704
             "cpu": [
705 705
                 "loong64"
706 706
             ],
@@ -712,9 +712,9 @@
712 712
             ]
713 713
         },
714 714
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
715
-            "version": "4.29.0",
716
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.0.tgz",
717
-            "integrity": "sha512-Nmt5Us5w2dL8eh7QVyAIDVVwBv4wk8ljrBQe7lWkLaOcwABDaFQ3K4sAAC6IsOdJwaXXW+d85zVaMN+Xl8Co2w==",
715
+            "version": "4.28.1",
716
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
717
+            "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
718 718
             "cpu": [
719 719
                 "ppc64"
720 720
             ],
@@ -726,9 +726,9 @@
726 726
             ]
727 727
         },
728 728
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
729
-            "version": "4.29.0",
730
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.0.tgz",
731
-            "integrity": "sha512-KGuQ8WGhnq09LR7eOru7P9jfBSYXTMhq6TyavWfmEo+TxvkvuRwOCee5lPIa6HYjblOuFr4GeOxSE0c8iyw2Fg==",
729
+            "version": "4.28.1",
730
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
731
+            "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
732 732
             "cpu": [
733 733
                 "riscv64"
734 734
             ],
@@ -740,9 +740,9 @@
740 740
             ]
741 741
         },
742 742
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
743
-            "version": "4.29.0",
744
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.0.tgz",
745
-            "integrity": "sha512-lSQtvrYIONme7a4gbf4O9d3zbZat3/5covIeoqk27ZIkTgBeL/67x+wq2bZfpLjkqQQp5SjBPQ/n0sg8iArzTg==",
743
+            "version": "4.28.1",
744
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
745
+            "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
746 746
             "cpu": [
747 747
                 "s390x"
748 748
             ],
@@ -754,9 +754,9 @@
754 754
             ]
755 755
         },
756 756
         "node_modules/@rollup/rollup-linux-x64-gnu": {
757
-            "version": "4.29.0",
758
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.0.tgz",
759
-            "integrity": "sha512-qh0ussrXBwnF4L07M9t1+jpHRhiGSae+wpNQDbmlXHXciT7pqpZ5zpk4dyGZPtDGB2l2clDiufE16BufXPGRWQ==",
757
+            "version": "4.28.1",
758
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
759
+            "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
760 760
             "cpu": [
761 761
                 "x64"
762 762
             ],
@@ -768,9 +768,9 @@
768 768
             ]
769 769
         },
770 770
         "node_modules/@rollup/rollup-linux-x64-musl": {
771
-            "version": "4.29.0",
772
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.0.tgz",
773
-            "integrity": "sha512-YEABzSaRS7+v14yw6MVBZoMqLoUyTX1/sJoGeC0euvgMrzvw0i+jHo4keDZgYeOblfwdseVAf6ylxWSvcBAKTA==",
771
+            "version": "4.28.1",
772
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
773
+            "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
774 774
             "cpu": [
775 775
                 "x64"
776 776
             ],
@@ -782,9 +782,9 @@
782 782
             ]
783 783
         },
784 784
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
785
-            "version": "4.29.0",
786
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.0.tgz",
787
-            "integrity": "sha512-jA4+oxG7QTTtSQxwSHzFVwShcppHO2DpkbAM59pfD5WMG/da79yQaeBtXAfGTI+ciUx8hqK3RF3H2KWByITXtQ==",
785
+            "version": "4.28.1",
786
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
787
+            "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
788 788
             "cpu": [
789 789
                 "arm64"
790 790
             ],
@@ -796,9 +796,9 @@
796 796
             ]
797 797
         },
798 798
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
799
-            "version": "4.29.0",
800
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.0.tgz",
801
-            "integrity": "sha512-4TQbLoAQVu9uE+cvh47JnjRZylXVdRCoOkRSVF2Rr2T0U1YwphGRjR0sHyRPEt95y3ETT4YFTTzQPq1O4bcjmw==",
799
+            "version": "4.28.1",
800
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
801
+            "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
802 802
             "cpu": [
803 803
                 "ia32"
804 804
             ],
@@ -810,9 +810,9 @@
810 810
             ]
811 811
         },
812 812
         "node_modules/@rollup/rollup-win32-x64-msvc": {
813
-            "version": "4.29.0",
814
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.0.tgz",
815
-            "integrity": "sha512-GsFvcTZ7Yj9k94Qm0qgav7pxmQ7lQDR9NjoelRaxeV1UF6JSDfanR/2tHZ8hS7Ps4KPIVf5AElYPRPmN/Q0ZkQ==",
813
+            "version": "4.28.1",
814
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
815
+            "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
816 816
             "cpu": [
817 817
                 "x64"
818 818
             ],
@@ -1057,9 +1057,9 @@
1057 1057
             }
1058 1058
         },
1059 1059
         "node_modules/caniuse-lite": {
1060
-            "version": "1.0.30001690",
1061
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
1062
-            "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
1060
+            "version": "1.0.30001689",
1061
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
1062
+            "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
1063 1063
             "dev": true,
1064 1064
             "funding": [
1065 1065
                 {
@@ -1218,9 +1218,9 @@
1218 1218
             "license": "MIT"
1219 1219
         },
1220 1220
         "node_modules/electron-to-chromium": {
1221
-            "version": "1.5.75",
1222
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
1223
-            "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
1221
+            "version": "1.5.74",
1222
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
1223
+            "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
1224 1224
             "dev": true,
1225 1225
             "license": "ISC"
1226 1226
         },
@@ -2192,9 +2192,9 @@
2192 2192
             }
2193 2193
         },
2194 2194
         "node_modules/resolve": {
2195
-            "version": "1.22.10",
2196
-            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
2197
-            "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
2195
+            "version": "1.22.9",
2196
+            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz",
2197
+            "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==",
2198 2198
             "dev": true,
2199 2199
             "license": "MIT",
2200 2200
             "dependencies": {
@@ -2205,9 +2205,6 @@
2205 2205
             "bin": {
2206 2206
                 "resolve": "bin/resolve"
2207 2207
             },
2208
-            "engines": {
2209
-                "node": ">= 0.4"
2210
-            },
2211 2208
             "funding": {
2212 2209
                 "url": "https://github.com/sponsors/ljharb"
2213 2210
             }
@@ -2224,9 +2221,9 @@
2224 2221
             }
2225 2222
         },
2226 2223
         "node_modules/rollup": {
2227
-            "version": "4.29.0",
2228
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.0.tgz",
2229
-            "integrity": "sha512-pdftUn12oB9Qlka+Vpyc39R28D4NsP9Sz6neepSrekofJmWzPD1sxcSO9hEOxFF8+7Kz3sHvwSkkRREI28M1/w==",
2224
+            "version": "4.28.1",
2225
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
2226
+            "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
2230 2227
             "dev": true,
2231 2228
             "license": "MIT",
2232 2229
             "dependencies": {
@@ -2240,25 +2237,25 @@
2240 2237
                 "npm": ">=8.0.0"
2241 2238
             },
2242 2239
             "optionalDependencies": {
2243
-                "@rollup/rollup-android-arm-eabi": "4.29.0",
2244
-                "@rollup/rollup-android-arm64": "4.29.0",
2245
-                "@rollup/rollup-darwin-arm64": "4.29.0",
2246
-                "@rollup/rollup-darwin-x64": "4.29.0",
2247
-                "@rollup/rollup-freebsd-arm64": "4.29.0",
2248
-                "@rollup/rollup-freebsd-x64": "4.29.0",
2249
-                "@rollup/rollup-linux-arm-gnueabihf": "4.29.0",
2250
-                "@rollup/rollup-linux-arm-musleabihf": "4.29.0",
2251
-                "@rollup/rollup-linux-arm64-gnu": "4.29.0",
2252
-                "@rollup/rollup-linux-arm64-musl": "4.29.0",
2253
-                "@rollup/rollup-linux-loongarch64-gnu": "4.29.0",
2254
-                "@rollup/rollup-linux-powerpc64le-gnu": "4.29.0",
2255
-                "@rollup/rollup-linux-riscv64-gnu": "4.29.0",
2256
-                "@rollup/rollup-linux-s390x-gnu": "4.29.0",
2257
-                "@rollup/rollup-linux-x64-gnu": "4.29.0",
2258
-                "@rollup/rollup-linux-x64-musl": "4.29.0",
2259
-                "@rollup/rollup-win32-arm64-msvc": "4.29.0",
2260
-                "@rollup/rollup-win32-ia32-msvc": "4.29.0",
2261
-                "@rollup/rollup-win32-x64-msvc": "4.29.0",
2240
+                "@rollup/rollup-android-arm-eabi": "4.28.1",
2241
+                "@rollup/rollup-android-arm64": "4.28.1",
2242
+                "@rollup/rollup-darwin-arm64": "4.28.1",
2243
+                "@rollup/rollup-darwin-x64": "4.28.1",
2244
+                "@rollup/rollup-freebsd-arm64": "4.28.1",
2245
+                "@rollup/rollup-freebsd-x64": "4.28.1",
2246
+                "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
2247
+                "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
2248
+                "@rollup/rollup-linux-arm64-gnu": "4.28.1",
2249
+                "@rollup/rollup-linux-arm64-musl": "4.28.1",
2250
+                "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
2251
+                "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
2252
+                "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
2253
+                "@rollup/rollup-linux-s390x-gnu": "4.28.1",
2254
+                "@rollup/rollup-linux-x64-gnu": "4.28.1",
2255
+                "@rollup/rollup-linux-x64-musl": "4.28.1",
2256
+                "@rollup/rollup-win32-arm64-msvc": "4.28.1",
2257
+                "@rollup/rollup-win32-ia32-msvc": "4.28.1",
2258
+                "@rollup/rollup-win32-x64-msvc": "4.28.1",
2262 2259
                 "fsevents": "~2.3.2"
2263 2260
             }
2264 2261
         },
@@ -2606,13 +2603,13 @@
2606 2603
             "license": "MIT"
2607 2604
         },
2608 2605
         "node_modules/vite": {
2609
-            "version": "6.0.5",
2610
-            "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.5.tgz",
2611
-            "integrity": "sha512-akD5IAH/ID5imgue2DYhzsEwCi0/4VKY31uhMLEYJwPP4TiUp8pL5PIK+Wo7H8qT8JY9i+pVfPydcFPYD1EL7g==",
2606
+            "version": "6.0.3",
2607
+            "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz",
2608
+            "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==",
2612 2609
             "dev": true,
2613 2610
             "license": "MIT",
2614 2611
             "dependencies": {
2615
-                "esbuild": "0.24.0",
2612
+                "esbuild": "^0.24.0",
2616 2613
                 "postcss": "^8.4.49",
2617 2614
                 "rollup": "^4.23.0"
2618 2615
             },

Loading…
Annulla
Salva