Przeglądaj źródła

Merge pull request #160 from andrewdwallo/development-3.x

Development 3.x
3.x
Andrew Wallo 5 miesięcy temu
rodzic
commit
b359e9dde5
No account linked to committer's email address
30 zmienionych plików z 1142 dodań i 533 usunięć
  1. 2
    1
      app/Concerns/ManagesLineItems.php
  2. 13
    4
      app/DTO/DocumentDTO.php
  3. 4
    0
      app/Filament/Company/Clusters/Settings/Resources/DocumentDefaultResource.php
  4. 100
    81
      app/Filament/Company/Resources/Purchases/BillResource.php
  5. 2
    2
      app/Filament/Company/Resources/Sales/ClientResource.php
  6. 100
    81
      app/Filament/Company/Resources/Sales/EstimateResource.php
  7. 100
    81
      app/Filament/Company/Resources/Sales/InvoiceResource.php
  8. 101
    82
      app/Filament/Company/Resources/Sales/RecurringInvoiceResource.php
  9. 86
    2
      app/Filament/Forms/Components/CustomTableRepeater.php
  10. 1
    1
      app/Models/Accounting/Document.php
  11. 1
    0
      app/Models/Accounting/DocumentLineItem.php
  12. 3
    0
      app/Models/Setting/DocumentDefault.php
  13. 1
    10
      app/Providers/Filament/CompanyPanelProvider.php
  14. 5
    1
      app/Utilities/RateCalculator.php
  15. 113
    112
      composer.lock
  16. 27
    1
      database/factories/Accounting/BillFactory.php
  17. 15
    3
      database/factories/Accounting/DocumentLineItemFactory.php
  18. 27
    1
      database/factories/Accounting/EstimateFactory.php
  19. 27
    1
      database/factories/Accounting/InvoiceFactory.php
  20. 27
    1
      database/factories/Accounting/RecurringInvoiceFactory.php
  21. 28
    0
      database/migrations/2025_05_03_150845_add_discount_method_to_document_defaults_table.php
  22. 37
    0
      database/migrations/2025_05_03_152233_update_discount_method_defaults_on_document_tables.php
  23. 28
    0
      database/migrations/2025_05_05_211551_add_sort_order_to_document_line_items.php
  24. 20
    51
      resources/css/filament/company/form-fields.css
  25. 1
    1
      resources/views/components/company/document-template/container.blade.php
  26. 1
    1
      resources/views/filament/company/components/document-templates/default.blade.php
  27. 251
    0
      resources/views/filament/forms/components/custom-table-repeater.blade.php
  28. 7
    5
      resources/views/filament/infolists/components/document-templates/classic.blade.php
  29. 7
    5
      resources/views/filament/infolists/components/document-templates/default.blade.php
  30. 7
    5
      resources/views/filament/infolists/components/document-templates/modern.blade.php

+ 2
- 1
app/Concerns/ManagesLineItems.php Wyświetl plik

@@ -16,7 +16,7 @@ trait ManagesLineItems
16 16
 {
17 17
     protected function handleLineItems(Model $record, Collection $lineItems): void
18 18
     {
19
-        foreach ($lineItems as $itemData) {
19
+        foreach ($lineItems as $index => $itemData) {
20 20
             $lineItem = isset($itemData['id'])
21 21
                 ? $record->lineItems->find($itemData['id'])
22 22
                 : $record->lineItems()->make();
@@ -26,6 +26,7 @@ trait ManagesLineItems
26 26
                 'description' => $itemData['description'],
27 27
                 'quantity' => $itemData['quantity'],
28 28
                 'unit_price' => $itemData['unit_price'],
29
+                'line_number' => $index + 1,
29 30
             ]);
30 31
 
31 32
             if (! $lineItem->exists) {

+ 13
- 4
app/DTO/DocumentDTO.php Wyświetl plik

@@ -26,7 +26,7 @@ readonly class DocumentDTO
26 26
         public string $date,
27 27
         public string $dueDate,
28 28
         public string $currencyCode,
29
-        public string $subtotal,
29
+        public ?string $subtotal,
30 30
         public ?string $discount,
31 31
         public ?string $tax,
32 32
         public string $total,
@@ -50,6 +50,15 @@ readonly class DocumentDTO
50 50
 
51 51
         $currencyCode = $document->currency_code ?? CurrencyAccessor::getDefaultCurrency();
52 52
 
53
+        $discount = $document->discount_total > 0 ? self::formatToMoney($document->discount_total, $currencyCode) : null;
54
+        $tax = $document->tax_total > 0 ? self::formatToMoney($document->tax_total, $currencyCode) : null;
55
+
56
+        if (! $discount && ! $tax) {
57
+            $subtotal = null;
58
+        } else {
59
+            $subtotal = self::formatToMoney($document->subtotal, $currencyCode);
60
+        }
61
+
53 62
         return new self(
54 63
             header: $document->header,
55 64
             subheader: $document->subheader,
@@ -61,9 +70,9 @@ readonly class DocumentDTO
61 70
             date: $document->documentDate(),
62 71
             dueDate: $document->dueDate(),
63 72
             currencyCode: $currencyCode,
64
-            subtotal: self::formatToMoney($document->subtotal, $currencyCode),
65
-            discount: self::formatToMoney($document->discount_total, $currencyCode),
66
-            tax: self::formatToMoney($document->tax_total, $currencyCode),
73
+            subtotal: $subtotal,
74
+            discount: $discount,
75
+            tax: $tax,
67 76
             total: self::formatToMoney($document->total, $currencyCode),
68 77
             amountDue: self::formatToMoney($document->amountDue(), $currencyCode),
69 78
             company: CompanyDTO::fromModel($document->company),

+ 4
- 0
app/Filament/Company/Clusters/Settings/Resources/DocumentDefaultResource.php Wyświetl plik

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Clusters\Settings\Resources;
4 4
 
5
+use App\Enums\Accounting\DocumentDiscountMethod;
5 6
 use App\Enums\Accounting\DocumentType;
6 7
 use App\Enums\Setting\Font;
7 8
 use App\Enums\Setting\PaymentTerms;
@@ -51,6 +52,9 @@ class DocumentDefaultResource extends Resource
51 52
                     ->softRequired()
52 53
                     ->localizeLabel()
53 54
                     ->options(PaymentTerms::class),
55
+                Forms\Components\Select::make('discount_method')
56
+                    ->softRequired()
57
+                    ->options(DocumentDiscountMethod::class),
54 58
             ])->columns();
55 59
     }
56 60
 

+ 100
- 81
app/Filament/Company/Resources/Purchases/BillResource.php Wyświetl plik

@@ -16,6 +16,7 @@ use App\Filament\Forms\Components\CreateAdjustmentSelect;
16 16
 use App\Filament\Forms\Components\CreateCurrencySelect;
17 17
 use App\Filament\Forms\Components\CreateOfferingSelect;
18 18
 use App\Filament\Forms\Components\CreateVendorSelect;
19
+use App\Filament\Forms\Components\CustomTableRepeater;
19 20
 use App\Filament\Forms\Components\DocumentTotals;
20 21
 use App\Filament\Tables\Actions\ReplicateBulkAction;
21 22
 use App\Filament\Tables\Columns;
@@ -29,7 +30,6 @@ use App\Models\Common\Vendor;
29 30
 use App\Utilities\Currency\CurrencyAccessor;
30 31
 use App\Utilities\Currency\CurrencyConverter;
31 32
 use App\Utilities\RateCalculator;
32
-use Awcodes\TableRepeater\Components\TableRepeater;
33 33
 use Awcodes\TableRepeater\Header;
34 34
 use Closure;
35 35
 use Filament\Forms;
@@ -166,8 +166,8 @@ class BillResource extends Resource
166 166
                                 Forms\Components\Select::make('discount_method')
167 167
                                     ->label('Discount method')
168 168
                                     ->options(DocumentDiscountMethod::class)
169
-                                    ->selectablePlaceholder(false)
170
-                                    ->default(DocumentDiscountMethod::PerLineItem)
169
+                                    ->softRequired()
170
+                                    ->default($settings->discount_method)
171 171
                                     ->afterStateUpdated(function ($state, Forms\Set $set) {
172 172
                                         $discountMethod = DocumentDiscountMethod::parse($state);
173 173
 
@@ -178,28 +178,32 @@ class BillResource extends Resource
178 178
                                     ->live(),
179 179
                             ])->grow(true),
180 180
                         ])->from('md'),
181
-                        TableRepeater::make('lineItems')
181
+                        CustomTableRepeater::make('lineItems')
182
+                            ->hiddenLabel()
182 183
                             ->relationship()
183 184
                             ->saveRelationshipsUsing(null)
184 185
                             ->dehydrated(true)
186
+                            ->reorderable()
187
+                            ->orderColumn('line_number')
188
+                            ->reorderAtStart()
189
+                            ->cloneable()
190
+                            ->addActionLabel('Add an item')
185 191
                             ->headers(function (Forms\Get $get) use ($settings) {
186 192
                                 $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
187 193
 
188 194
                                 $headers = [
189 195
                                     Header::make($settings->resolveColumnLabel('item_name', 'Items'))
190
-                                        ->width($hasDiscounts ? '15%' : '20%'),
191
-                                    Header::make('Description')
192
-                                        ->width($hasDiscounts ? '15%' : '20%'),
196
+                                        ->width('30%'),
193 197
                                     Header::make($settings->resolveColumnLabel('unit_name', 'Quantity'))
194 198
                                         ->width('10%'),
195 199
                                     Header::make($settings->resolveColumnLabel('price_name', 'Price'))
196 200
                                         ->width('10%'),
197
-                                    Header::make('Taxes')
198
-                                        ->width($hasDiscounts ? '20%' : '30%'),
199 201
                                 ];
200 202
 
201 203
                                 if ($hasDiscounts) {
202
-                                    $headers[] = Header::make('Discounts')->width('20%');
204
+                                    $headers[] = Header::make('Adjustments')->width('30%');
205
+                                } else {
206
+                                    $headers[] = Header::make('Taxes')->width('30%');
203 207
                                 }
204 208
 
205 209
                                 $headers[] = Header::make($settings->resolveColumnLabel('amount_name', 'Amount'))
@@ -209,61 +213,68 @@ class BillResource extends Resource
209 213
                                 return $headers;
210 214
                             })
211 215
                             ->schema([
212
-                                CreateOfferingSelect::make('offering_id')
213
-                                    ->label('Item')
214
-                                    ->required()
215
-                                    ->live()
216
-                                    ->purchasable()
217
-                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state, ?DocumentLineItem $record) {
218
-                                        $offeringId = $state;
219
-                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
220
-                                        $isPerLineItem = $discountMethod->isPerLineItem();
216
+                                Forms\Components\Group::make([
217
+                                    CreateOfferingSelect::make('offering_id')
218
+                                        ->label('Item')
219
+                                        ->hiddenLabel()
220
+                                        ->placeholder('Select item')
221
+                                        ->required()
222
+                                        ->live()
223
+                                        ->inlineSuffix()
224
+                                        ->purchasable()
225
+                                        ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state, ?DocumentLineItem $record) {
226
+                                            $offeringId = $state;
227
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
228
+                                            $isPerLineItem = $discountMethod->isPerLineItem();
229
+
230
+                                            $existingTaxIds = [];
231
+                                            $existingDiscountIds = [];
232
+
233
+                                            if ($record) {
234
+                                                $existingTaxIds = $record->purchaseTaxes()->pluck('adjustments.id')->toArray();
235
+                                                if ($isPerLineItem) {
236
+                                                    $existingDiscountIds = $record->purchaseDiscounts()->pluck('adjustments.id')->toArray();
237
+                                                }
238
+                                            }
221 239
 
222
-                                        $existingTaxIds = [];
223
-                                        $existingDiscountIds = [];
240
+                                            $with = [
241
+                                                'purchaseTaxes' => static function ($query) use ($existingTaxIds) {
242
+                                                    $query->where(static function ($query) use ($existingTaxIds) {
243
+                                                        $query->where('status', AdjustmentStatus::Active)
244
+                                                            ->orWhereIn('adjustments.id', $existingTaxIds);
245
+                                                    });
246
+                                                },
247
+                                            ];
224 248
 
225
-                                        if ($record) {
226
-                                            $existingTaxIds = $record->purchaseTaxes()->pluck('adjustments.id')->toArray();
227 249
                                             if ($isPerLineItem) {
228
-                                                $existingDiscountIds = $record->purchaseDiscounts()->pluck('adjustments.id')->toArray();
250
+                                                $with['purchaseDiscounts'] = static function ($query) use ($existingDiscountIds) {
251
+                                                    $query->where(static function ($query) use ($existingDiscountIds) {
252
+                                                        $query->where('status', AdjustmentStatus::Active)
253
+                                                            ->orWhereIn('adjustments.id', $existingDiscountIds);
254
+                                                    });
255
+                                                };
229 256
                                             }
230
-                                        }
231 257
 
232
-                                        $with = [
233
-                                            'purchaseTaxes' => static function ($query) use ($existingTaxIds) {
234
-                                                $query->where(static function ($query) use ($existingTaxIds) {
235
-                                                    $query->where('status', AdjustmentStatus::Active)
236
-                                                        ->orWhereIn('adjustments.id', $existingTaxIds);
237
-                                                });
238
-                                            },
239
-                                        ];
240
-
241
-                                        if ($isPerLineItem) {
242
-                                            $with['purchaseDiscounts'] = static function ($query) use ($existingDiscountIds) {
243
-                                                $query->where(static function ($query) use ($existingDiscountIds) {
244
-                                                    $query->where('status', AdjustmentStatus::Active)
245
-                                                        ->orWhereIn('adjustments.id', $existingDiscountIds);
246
-                                                });
247
-                                            };
248
-                                        }
258
+                                            $offeringRecord = Offering::with($with)->find($offeringId);
249 259
 
250
-                                        $offeringRecord = Offering::with($with)->find($offeringId);
251
-
252
-                                        if (! $offeringRecord) {
253
-                                            return;
254
-                                        }
260
+                                            if (! $offeringRecord) {
261
+                                                return;
262
+                                            }
255 263
 
256
-                                        $unitPrice = CurrencyConverter::convertToFloat($offeringRecord->price, $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency());
264
+                                            $unitPrice = CurrencyConverter::convertToFloat($offeringRecord->price, $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency());
257 265
 
258
-                                        $set('description', $offeringRecord->description);
259
-                                        $set('unit_price', $unitPrice);
260
-                                        $set('purchaseTaxes', $offeringRecord->purchaseTaxes->pluck('id')->toArray());
266
+                                            $set('description', $offeringRecord->description);
267
+                                            $set('unit_price', $unitPrice);
268
+                                            $set('purchaseTaxes', $offeringRecord->purchaseTaxes->pluck('id')->toArray());
261 269
 
262
-                                        if ($isPerLineItem) {
263
-                                            $set('purchaseDiscounts', $offeringRecord->purchaseDiscounts->pluck('id')->toArray());
264
-                                        }
265
-                                    }),
266
-                                Forms\Components\TextInput::make('description'),
270
+                                            if ($isPerLineItem) {
271
+                                                $set('purchaseDiscounts', $offeringRecord->purchaseDiscounts->pluck('id')->toArray());
272
+                                            }
273
+                                        }),
274
+                                    Forms\Components\TextInput::make('description')
275
+                                        ->placeholder('Enter item description')
276
+                                        ->hiddenLabel(),
277
+                                ])->columnSpan(1),
267 278
                                 Forms\Components\TextInput::make('quantity')
268 279
                                     ->required()
269 280
                                     ->numeric()
@@ -277,32 +288,40 @@ class BillResource extends Resource
277 288
                                     ->live()
278 289
                                     ->maxValue(9999999999.99)
279 290
                                     ->default(0),
280
-                                CreateAdjustmentSelect::make('purchaseTaxes')
281
-                                    ->label('Taxes')
282
-                                    ->category(AdjustmentCategory::Tax)
283
-                                    ->type(AdjustmentType::Purchase)
284
-                                    ->adjustmentsRelationship('purchaseTaxes')
285
-                                    ->saveRelationshipsUsing(null)
286
-                                    ->dehydrated(true)
287
-                                    ->preload()
288
-                                    ->multiple()
289
-                                    ->live()
290
-                                    ->searchable(),
291
-                                CreateAdjustmentSelect::make('purchaseDiscounts')
292
-                                    ->label('Discounts')
293
-                                    ->category(AdjustmentCategory::Discount)
294
-                                    ->type(AdjustmentType::Purchase)
295
-                                    ->adjustmentsRelationship('purchaseDiscounts')
296
-                                    ->saveRelationshipsUsing(null)
297
-                                    ->dehydrated(true)
298
-                                    ->multiple()
299
-                                    ->live()
300
-                                    ->hidden(function (Forms\Get $get) {
301
-                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
291
+                                Forms\Components\Group::make([
292
+                                    CreateAdjustmentSelect::make('purchaseTaxes')
293
+                                        ->label('Taxes')
294
+                                        ->hiddenLabel()
295
+                                        ->placeholder('Select taxes')
296
+                                        ->category(AdjustmentCategory::Tax)
297
+                                        ->type(AdjustmentType::Purchase)
298
+                                        ->adjustmentsRelationship('purchaseTaxes')
299
+                                        ->saveRelationshipsUsing(null)
300
+                                        ->dehydrated(true)
301
+                                        ->inlineSuffix()
302
+                                        ->preload()
303
+                                        ->multiple()
304
+                                        ->live()
305
+                                        ->searchable(),
306
+                                    CreateAdjustmentSelect::make('purchaseDiscounts')
307
+                                        ->label('Discounts')
308
+                                        ->hiddenLabel()
309
+                                        ->placeholder('Select discounts')
310
+                                        ->category(AdjustmentCategory::Discount)
311
+                                        ->type(AdjustmentType::Purchase)
312
+                                        ->adjustmentsRelationship('purchaseDiscounts')
313
+                                        ->saveRelationshipsUsing(null)
314
+                                        ->dehydrated(true)
315
+                                        ->inlineSuffix()
316
+                                        ->multiple()
317
+                                        ->live()
318
+                                        ->hidden(function (Forms\Get $get) {
319
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
302 320
 
303
-                                        return $discountMethod->isPerDocument();
304
-                                    })
305
-                                    ->searchable(),
321
+                                            return $discountMethod->isPerDocument();
322
+                                        })
323
+                                        ->searchable(),
324
+                                ])->columnSpan(1),
306 325
                                 Forms\Components\Placeholder::make('total')
307 326
                                     ->hiddenLabel()
308 327
                                     ->extraAttributes(['class' => 'text-left sm:text-right'])

+ 2
- 2
app/Filament/Company/Resources/Sales/ClientResource.php Wyświetl plik

@@ -252,7 +252,7 @@ class ClientResource extends Resource
252 252
                 Tables\Columns\TextColumn::make('name')
253 253
                     ->searchable()
254 254
                     ->sortable()
255
-                    ->description(static fn (Client $client) => $client->primaryContact->full_name),
255
+                    ->description(static fn (Client $client) => $client->primaryContact?->full_name),
256 256
                 Tables\Columns\TextColumn::make('primaryContact.email')
257 257
                     ->label('Email')
258 258
                     ->searchable()
@@ -260,7 +260,7 @@ class ClientResource extends Resource
260 260
                 Tables\Columns\TextColumn::make('primaryContact.phones')
261 261
                     ->label('Phone')
262 262
                     ->toggleable()
263
-                    ->state(static fn (Client $client) => $client->primaryContact->first_available_phone),
263
+                    ->state(static fn (Client $client) => $client->primaryContact?->first_available_phone),
264 264
                 Tables\Columns\TextColumn::make('billingAddress.address_string')
265 265
                     ->label('Billing address')
266 266
                     ->searchable()

+ 100
- 81
app/Filament/Company/Resources/Sales/EstimateResource.php Wyświetl plik

@@ -16,6 +16,7 @@ use App\Filament\Forms\Components\CreateAdjustmentSelect;
16 16
 use App\Filament\Forms\Components\CreateClientSelect;
17 17
 use App\Filament\Forms\Components\CreateCurrencySelect;
18 18
 use App\Filament\Forms\Components\CreateOfferingSelect;
19
+use App\Filament\Forms\Components\CustomTableRepeater;
19 20
 use App\Filament\Forms\Components\DocumentFooterSection;
20 21
 use App\Filament\Forms\Components\DocumentHeaderSection;
21 22
 use App\Filament\Forms\Components\DocumentTotals;
@@ -30,7 +31,6 @@ use App\Models\Common\Offering;
30 31
 use App\Utilities\Currency\CurrencyAccessor;
31 32
 use App\Utilities\Currency\CurrencyConverter;
32 33
 use App\Utilities\RateCalculator;
33
-use Awcodes\TableRepeater\Components\TableRepeater;
34 34
 use Awcodes\TableRepeater\Header;
35 35
 use Filament\Forms;
36 36
 use Filament\Forms\Form;
@@ -164,8 +164,8 @@ class EstimateResource extends Resource
164 164
                                 Forms\Components\Select::make('discount_method')
165 165
                                     ->label('Discount method')
166 166
                                     ->options(DocumentDiscountMethod::class)
167
-                                    ->selectablePlaceholder(false)
168
-                                    ->default(DocumentDiscountMethod::PerLineItem)
167
+                                    ->softRequired()
168
+                                    ->default($settings->discount_method)
169 169
                                     ->afterStateUpdated(function ($state, Forms\Set $set) {
170 170
                                         $discountMethod = DocumentDiscountMethod::parse($state);
171 171
 
@@ -176,28 +176,32 @@ class EstimateResource extends Resource
176 176
                                     ->live(),
177 177
                             ])->grow(true),
178 178
                         ])->from('md'),
179
-                        TableRepeater::make('lineItems')
179
+                        CustomTableRepeater::make('lineItems')
180
+                            ->hiddenLabel()
180 181
                             ->relationship()
181 182
                             ->saveRelationshipsUsing(null)
182 183
                             ->dehydrated(true)
184
+                            ->reorderable()
185
+                            ->orderColumn('line_number')
186
+                            ->reorderAtStart()
187
+                            ->cloneable()
188
+                            ->addActionLabel('Add an item')
183 189
                             ->headers(function (Forms\Get $get) use ($settings) {
184 190
                                 $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
185 191
 
186 192
                                 $headers = [
187 193
                                     Header::make($settings->resolveColumnLabel('item_name', 'Items'))
188
-                                        ->width($hasDiscounts ? '15%' : '20%'),
189
-                                    Header::make('Description')
190
-                                        ->width($hasDiscounts ? '15%' : '20%'),
194
+                                        ->width('30%'),
191 195
                                     Header::make($settings->resolveColumnLabel('unit_name', 'Quantity'))
192 196
                                         ->width('10%'),
193 197
                                     Header::make($settings->resolveColumnLabel('price_name', 'Price'))
194 198
                                         ->width('10%'),
195
-                                    Header::make('Taxes')
196
-                                        ->width($hasDiscounts ? '20%' : '30%'),
197 199
                                 ];
198 200
 
199 201
                                 if ($hasDiscounts) {
200
-                                    $headers[] = Header::make('Discounts')->width('20%');
202
+                                    $headers[] = Header::make('Adjustments')->width('30%');
203
+                                } else {
204
+                                    $headers[] = Header::make('Taxes')->width('30%');
201 205
                                 }
202 206
 
203 207
                                 $headers[] = Header::make($settings->resolveColumnLabel('amount_name', 'Amount'))
@@ -207,61 +211,68 @@ class EstimateResource extends Resource
207 211
                                 return $headers;
208 212
                             })
209 213
                             ->schema([
210
-                                CreateOfferingSelect::make('offering_id')
211
-                                    ->label('Item')
212
-                                    ->required()
213
-                                    ->live()
214
-                                    ->sellable()
215
-                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state, ?DocumentLineItem $record) {
216
-                                        $offeringId = $state;
217
-                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
218
-                                        $isPerLineItem = $discountMethod->isPerLineItem();
214
+                                Forms\Components\Group::make([
215
+                                    CreateOfferingSelect::make('offering_id')
216
+                                        ->label('Item')
217
+                                        ->hiddenLabel()
218
+                                        ->placeholder('Select item')
219
+                                        ->required()
220
+                                        ->live()
221
+                                        ->inlineSuffix()
222
+                                        ->sellable()
223
+                                        ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state, ?DocumentLineItem $record) {
224
+                                            $offeringId = $state;
225
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
226
+                                            $isPerLineItem = $discountMethod->isPerLineItem();
227
+
228
+                                            $existingTaxIds = [];
229
+                                            $existingDiscountIds = [];
230
+
231
+                                            if ($record) {
232
+                                                $existingTaxIds = $record->salesTaxes()->pluck('adjustments.id')->toArray();
233
+                                                if ($isPerLineItem) {
234
+                                                    $existingDiscountIds = $record->salesDiscounts()->pluck('adjustments.id')->toArray();
235
+                                                }
236
+                                            }
219 237
 
220
-                                        $existingTaxIds = [];
221
-                                        $existingDiscountIds = [];
238
+                                            $with = [
239
+                                                'salesTaxes' => static function ($query) use ($existingTaxIds) {
240
+                                                    $query->where(static function ($query) use ($existingTaxIds) {
241
+                                                        $query->where('status', AdjustmentStatus::Active)
242
+                                                            ->orWhereIn('adjustments.id', $existingTaxIds);
243
+                                                    });
244
+                                                },
245
+                                            ];
222 246
 
223
-                                        if ($record) {
224
-                                            $existingTaxIds = $record->salesTaxes()->pluck('adjustments.id')->toArray();
225 247
                                             if ($isPerLineItem) {
226
-                                                $existingDiscountIds = $record->salesDiscounts()->pluck('adjustments.id')->toArray();
248
+                                                $with['salesDiscounts'] = static function ($query) use ($existingDiscountIds) {
249
+                                                    $query->where(static function ($query) use ($existingDiscountIds) {
250
+                                                        $query->where('status', AdjustmentStatus::Active)
251
+                                                            ->orWhereIn('adjustments.id', $existingDiscountIds);
252
+                                                    });
253
+                                                };
227 254
                                             }
228
-                                        }
229 255
 
230
-                                        $with = [
231
-                                            'salesTaxes' => static function ($query) use ($existingTaxIds) {
232
-                                                $query->where(static function ($query) use ($existingTaxIds) {
233
-                                                    $query->where('status', AdjustmentStatus::Active)
234
-                                                        ->orWhereIn('adjustments.id', $existingTaxIds);
235
-                                                });
236
-                                            },
237
-                                        ];
238
-
239
-                                        if ($isPerLineItem) {
240
-                                            $with['salesDiscounts'] = static function ($query) use ($existingDiscountIds) {
241
-                                                $query->where(static function ($query) use ($existingDiscountIds) {
242
-                                                    $query->where('status', AdjustmentStatus::Active)
243
-                                                        ->orWhereIn('adjustments.id', $existingDiscountIds);
244
-                                                });
245
-                                            };
246
-                                        }
247
-
248
-                                        $offeringRecord = Offering::with($with)->find($offeringId);
256
+                                            $offeringRecord = Offering::with($with)->find($offeringId);
249 257
 
250
-                                        if (! $offeringRecord) {
251
-                                            return;
252
-                                        }
258
+                                            if (! $offeringRecord) {
259
+                                                return;
260
+                                            }
253 261
 
254
-                                        $unitPrice = CurrencyConverter::convertToFloat($offeringRecord->price, $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency());
262
+                                            $unitPrice = CurrencyConverter::convertToFloat($offeringRecord->price, $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency());
255 263
 
256
-                                        $set('description', $offeringRecord->description);
257
-                                        $set('unit_price', $unitPrice);
258
-                                        $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
264
+                                            $set('description', $offeringRecord->description);
265
+                                            $set('unit_price', $unitPrice);
266
+                                            $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
259 267
 
260
-                                        if ($isPerLineItem) {
261
-                                            $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
262
-                                        }
263
-                                    }),
264
-                                Forms\Components\TextInput::make('description'),
268
+                                            if ($isPerLineItem) {
269
+                                                $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
270
+                                            }
271
+                                        }),
272
+                                    Forms\Components\TextInput::make('description')
273
+                                        ->placeholder('Enter item description')
274
+                                        ->hiddenLabel(),
275
+                                ])->columnSpan(1),
265 276
                                 Forms\Components\TextInput::make('quantity')
266 277
                                     ->required()
267 278
                                     ->numeric()
@@ -274,32 +285,40 @@ class EstimateResource extends Resource
274 285
                                     ->live()
275 286
                                     ->maxValue(9999999999.99)
276 287
                                     ->default(0),
277
-                                CreateAdjustmentSelect::make('salesTaxes')
278
-                                    ->label('Taxes')
279
-                                    ->category(AdjustmentCategory::Tax)
280
-                                    ->type(AdjustmentType::Sales)
281
-                                    ->adjustmentsRelationship('salesTaxes')
282
-                                    ->saveRelationshipsUsing(null)
283
-                                    ->dehydrated(true)
284
-                                    ->preload()
285
-                                    ->multiple()
286
-                                    ->live()
287
-                                    ->searchable(),
288
-                                CreateAdjustmentSelect::make('salesDiscounts')
289
-                                    ->label('Discounts')
290
-                                    ->category(AdjustmentCategory::Discount)
291
-                                    ->type(AdjustmentType::Sales)
292
-                                    ->adjustmentsRelationship('salesDiscounts')
293
-                                    ->saveRelationshipsUsing(null)
294
-                                    ->dehydrated(true)
295
-                                    ->multiple()
296
-                                    ->live()
297
-                                    ->hidden(function (Forms\Get $get) {
298
-                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
288
+                                Forms\Components\Group::make([
289
+                                    CreateAdjustmentSelect::make('salesTaxes')
290
+                                        ->label('Taxes')
291
+                                        ->hiddenLabel()
292
+                                        ->placeholder('Select taxes')
293
+                                        ->category(AdjustmentCategory::Tax)
294
+                                        ->type(AdjustmentType::Sales)
295
+                                        ->adjustmentsRelationship('salesTaxes')
296
+                                        ->saveRelationshipsUsing(null)
297
+                                        ->dehydrated(true)
298
+                                        ->inlineSuffix()
299
+                                        ->preload()
300
+                                        ->multiple()
301
+                                        ->live()
302
+                                        ->searchable(),
303
+                                    CreateAdjustmentSelect::make('salesDiscounts')
304
+                                        ->label('Discounts')
305
+                                        ->hiddenLabel()
306
+                                        ->placeholder('Select discounts')
307
+                                        ->category(AdjustmentCategory::Discount)
308
+                                        ->type(AdjustmentType::Sales)
309
+                                        ->adjustmentsRelationship('salesDiscounts')
310
+                                        ->saveRelationshipsUsing(null)
311
+                                        ->dehydrated(true)
312
+                                        ->inlineSuffix()
313
+                                        ->multiple()
314
+                                        ->live()
315
+                                        ->hidden(function (Forms\Get $get) {
316
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
299 317
 
300
-                                        return $discountMethod->isPerDocument();
301
-                                    })
302
-                                    ->searchable(),
318
+                                            return $discountMethod->isPerDocument();
319
+                                        })
320
+                                        ->searchable(),
321
+                                ])->columnSpan(1),
303 322
                                 Forms\Components\Placeholder::make('total')
304 323
                                     ->hiddenLabel()
305 324
                                     ->extraAttributes(['class' => 'text-left sm:text-right'])

+ 100
- 81
app/Filament/Company/Resources/Sales/InvoiceResource.php Wyświetl plik

@@ -18,6 +18,7 @@ use App\Filament\Forms\Components\CreateAdjustmentSelect;
18 18
 use App\Filament\Forms\Components\CreateClientSelect;
19 19
 use App\Filament\Forms\Components\CreateCurrencySelect;
20 20
 use App\Filament\Forms\Components\CreateOfferingSelect;
21
+use App\Filament\Forms\Components\CustomTableRepeater;
21 22
 use App\Filament\Forms\Components\DocumentFooterSection;
22 23
 use App\Filament\Forms\Components\DocumentHeaderSection;
23 24
 use App\Filament\Forms\Components\DocumentTotals;
@@ -33,7 +34,6 @@ use App\Models\Common\Offering;
33 34
 use App\Utilities\Currency\CurrencyAccessor;
34 35
 use App\Utilities\Currency\CurrencyConverter;
35 36
 use App\Utilities\RateCalculator;
36
-use Awcodes\TableRepeater\Components\TableRepeater;
37 37
 use Awcodes\TableRepeater\Header;
38 38
 use Closure;
39 39
 use Filament\Forms;
@@ -177,8 +177,8 @@ class InvoiceResource extends Resource
177 177
                                 Forms\Components\Select::make('discount_method')
178 178
                                     ->label('Discount method')
179 179
                                     ->options(DocumentDiscountMethod::class)
180
-                                    ->selectablePlaceholder(false)
181
-                                    ->default(DocumentDiscountMethod::PerLineItem)
180
+                                    ->softRequired()
181
+                                    ->default($settings->discount_method)
182 182
                                     ->afterStateUpdated(function ($state, Forms\Set $set) {
183 183
                                         $discountMethod = DocumentDiscountMethod::parse($state);
184 184
 
@@ -189,28 +189,32 @@ class InvoiceResource extends Resource
189 189
                                     ->live(),
190 190
                             ])->grow(true),
191 191
                         ])->from('md'),
192
-                        TableRepeater::make('lineItems')
192
+                        CustomTableRepeater::make('lineItems')
193
+                            ->hiddenLabel()
193 194
                             ->relationship()
194 195
                             ->saveRelationshipsUsing(null)
195 196
                             ->dehydrated(true)
197
+                            ->reorderable()
198
+                            ->orderColumn('line_number')
199
+                            ->reorderAtStart()
200
+                            ->cloneable()
201
+                            ->addActionLabel('Add an item')
196 202
                             ->headers(function (Forms\Get $get) use ($settings) {
197 203
                                 $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
198 204
 
199 205
                                 $headers = [
200 206
                                     Header::make($settings->resolveColumnLabel('item_name', 'Items'))
201
-                                        ->width($hasDiscounts ? '15%' : '20%'),
202
-                                    Header::make('Description')
203
-                                        ->width($hasDiscounts ? '15%' : '20%'),
207
+                                        ->width('30%'),
204 208
                                     Header::make($settings->resolveColumnLabel('unit_name', 'Quantity'))
205 209
                                         ->width('10%'),
206 210
                                     Header::make($settings->resolveColumnLabel('price_name', 'Price'))
207 211
                                         ->width('10%'),
208
-                                    Header::make('Taxes')
209
-                                        ->width($hasDiscounts ? '20%' : '30%'),
210 212
                                 ];
211 213
 
212 214
                                 if ($hasDiscounts) {
213
-                                    $headers[] = Header::make('Discounts')->width('20%');
215
+                                    $headers[] = Header::make('Adjustments')->width('30%');
216
+                                } else {
217
+                                    $headers[] = Header::make('Taxes')->width('30%');
214 218
                                 }
215 219
 
216 220
                                 $headers[] = Header::make($settings->resolveColumnLabel('amount_name', 'Amount'))
@@ -220,61 +224,68 @@ class InvoiceResource extends Resource
220 224
                                 return $headers;
221 225
                             })
222 226
                             ->schema([
223
-                                CreateOfferingSelect::make('offering_id')
224
-                                    ->label('Item')
225
-                                    ->required()
226
-                                    ->live()
227
-                                    ->sellable()
228
-                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state, ?DocumentLineItem $record) {
229
-                                        $offeringId = $state;
230
-                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
231
-                                        $isPerLineItem = $discountMethod->isPerLineItem();
227
+                                Forms\Components\Group::make([
228
+                                    CreateOfferingSelect::make('offering_id')
229
+                                        ->label('Item')
230
+                                        ->hiddenLabel()
231
+                                        ->placeholder('Select item')
232
+                                        ->required()
233
+                                        ->live()
234
+                                        ->inlineSuffix()
235
+                                        ->sellable()
236
+                                        ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state, ?DocumentLineItem $record) {
237
+                                            $offeringId = $state;
238
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
239
+                                            $isPerLineItem = $discountMethod->isPerLineItem();
240
+
241
+                                            $existingTaxIds = [];
242
+                                            $existingDiscountIds = [];
243
+
244
+                                            if ($record) {
245
+                                                $existingTaxIds = $record->salesTaxes()->pluck('adjustments.id')->toArray();
246
+                                                if ($isPerLineItem) {
247
+                                                    $existingDiscountIds = $record->salesDiscounts()->pluck('adjustments.id')->toArray();
248
+                                                }
249
+                                            }
232 250
 
233
-                                        $existingTaxIds = [];
234
-                                        $existingDiscountIds = [];
251
+                                            $with = [
252
+                                                'salesTaxes' => static function ($query) use ($existingTaxIds) {
253
+                                                    $query->where(static function ($query) use ($existingTaxIds) {
254
+                                                        $query->where('status', AdjustmentStatus::Active)
255
+                                                            ->orWhereIn('adjustments.id', $existingTaxIds);
256
+                                                    });
257
+                                                },
258
+                                            ];
235 259
 
236
-                                        if ($record) {
237
-                                            $existingTaxIds = $record->salesTaxes()->pluck('adjustments.id')->toArray();
238 260
                                             if ($isPerLineItem) {
239
-                                                $existingDiscountIds = $record->salesDiscounts()->pluck('adjustments.id')->toArray();
261
+                                                $with['salesDiscounts'] = static function ($query) use ($existingDiscountIds) {
262
+                                                    $query->where(static function ($query) use ($existingDiscountIds) {
263
+                                                        $query->where('status', AdjustmentStatus::Active)
264
+                                                            ->orWhereIn('adjustments.id', $existingDiscountIds);
265
+                                                    });
266
+                                                };
240 267
                                             }
241
-                                        }
242 268
 
243
-                                        $with = [
244
-                                            'salesTaxes' => static function ($query) use ($existingTaxIds) {
245
-                                                $query->where(static function ($query) use ($existingTaxIds) {
246
-                                                    $query->where('status', AdjustmentStatus::Active)
247
-                                                        ->orWhereIn('adjustments.id', $existingTaxIds);
248
-                                                });
249
-                                            },
250
-                                        ];
251
-
252
-                                        if ($isPerLineItem) {
253
-                                            $with['salesDiscounts'] = static function ($query) use ($existingDiscountIds) {
254
-                                                $query->where(static function ($query) use ($existingDiscountIds) {
255
-                                                    $query->where('status', AdjustmentStatus::Active)
256
-                                                        ->orWhereIn('adjustments.id', $existingDiscountIds);
257
-                                                });
258
-                                            };
259
-                                        }
269
+                                            $offeringRecord = Offering::with($with)->find($offeringId);
260 270
 
261
-                                        $offeringRecord = Offering::with($with)->find($offeringId);
262
-
263
-                                        if (! $offeringRecord) {
264
-                                            return;
265
-                                        }
271
+                                            if (! $offeringRecord) {
272
+                                                return;
273
+                                            }
266 274
 
267
-                                        $unitPrice = CurrencyConverter::convertToFloat($offeringRecord->price, $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency());
275
+                                            $unitPrice = CurrencyConverter::convertToFloat($offeringRecord->price, $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency());
268 276
 
269
-                                        $set('description', $offeringRecord->description);
270
-                                        $set('unit_price', $unitPrice);
271
-                                        $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
277
+                                            $set('description', $offeringRecord->description);
278
+                                            $set('unit_price', $unitPrice);
279
+                                            $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
272 280
 
273
-                                        if ($isPerLineItem) {
274
-                                            $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
275
-                                        }
276
-                                    }),
277
-                                Forms\Components\TextInput::make('description'),
281
+                                            if ($isPerLineItem) {
282
+                                                $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
283
+                                            }
284
+                                        }),
285
+                                    Forms\Components\TextInput::make('description')
286
+                                        ->placeholder('Enter item description')
287
+                                        ->hiddenLabel(),
288
+                                ])->columnSpan(1),
278 289
                                 Forms\Components\TextInput::make('quantity')
279 290
                                     ->required()
280 291
                                     ->numeric()
@@ -287,32 +298,40 @@ class InvoiceResource extends Resource
287 298
                                     ->live()
288 299
                                     ->maxValue(9999999999.99)
289 300
                                     ->default(0),
290
-                                CreateAdjustmentSelect::make('salesTaxes')
291
-                                    ->label('Taxes')
292
-                                    ->category(AdjustmentCategory::Tax)
293
-                                    ->type(AdjustmentType::Sales)
294
-                                    ->adjustmentsRelationship('salesTaxes')
295
-                                    ->saveRelationshipsUsing(null)
296
-                                    ->dehydrated(true)
297
-                                    ->preload()
298
-                                    ->multiple()
299
-                                    ->live()
300
-                                    ->searchable(),
301
-                                CreateAdjustmentSelect::make('salesDiscounts')
302
-                                    ->label('Discounts')
303
-                                    ->category(AdjustmentCategory::Discount)
304
-                                    ->type(AdjustmentType::Sales)
305
-                                    ->adjustmentsRelationship('salesDiscounts')
306
-                                    ->saveRelationshipsUsing(null)
307
-                                    ->dehydrated(true)
308
-                                    ->multiple()
309
-                                    ->live()
310
-                                    ->hidden(function (Forms\Get $get) {
311
-                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
301
+                                Forms\Components\Group::make([
302
+                                    CreateAdjustmentSelect::make('salesTaxes')
303
+                                        ->label('Taxes')
304
+                                        ->hiddenLabel()
305
+                                        ->placeholder('Select taxes')
306
+                                        ->category(AdjustmentCategory::Tax)
307
+                                        ->type(AdjustmentType::Sales)
308
+                                        ->adjustmentsRelationship('salesTaxes')
309
+                                        ->saveRelationshipsUsing(null)
310
+                                        ->dehydrated(true)
311
+                                        ->inlineSuffix()
312
+                                        ->preload()
313
+                                        ->multiple()
314
+                                        ->live()
315
+                                        ->searchable(),
316
+                                    CreateAdjustmentSelect::make('salesDiscounts')
317
+                                        ->label('Discounts')
318
+                                        ->hiddenLabel()
319
+                                        ->placeholder('Select discounts')
320
+                                        ->category(AdjustmentCategory::Discount)
321
+                                        ->type(AdjustmentType::Sales)
322
+                                        ->adjustmentsRelationship('salesDiscounts')
323
+                                        ->saveRelationshipsUsing(null)
324
+                                        ->dehydrated(true)
325
+                                        ->inlineSuffix()
326
+                                        ->multiple()
327
+                                        ->live()
328
+                                        ->hidden(function (Forms\Get $get) {
329
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
312 330
 
313
-                                        return $discountMethod->isPerDocument();
314
-                                    })
315
-                                    ->searchable(),
331
+                                            return $discountMethod->isPerDocument();
332
+                                        })
333
+                                        ->searchable(),
334
+                                ])->columnSpan(1),
316 335
                                 Forms\Components\Placeholder::make('total')
317 336
                                     ->hiddenLabel()
318 337
                                     ->extraAttributes(['class' => 'text-left sm:text-right'])

+ 101
- 82
app/Filament/Company/Resources/Sales/RecurringInvoiceResource.php Wyświetl plik

@@ -15,6 +15,7 @@ use App\Filament\Forms\Components\CreateAdjustmentSelect;
15 15
 use App\Filament\Forms\Components\CreateClientSelect;
16 16
 use App\Filament\Forms\Components\CreateCurrencySelect;
17 17
 use App\Filament\Forms\Components\CreateOfferingSelect;
18
+use App\Filament\Forms\Components\CustomTableRepeater;
18 19
 use App\Filament\Forms\Components\DocumentFooterSection;
19 20
 use App\Filament\Forms\Components\DocumentHeaderSection;
20 21
 use App\Filament\Forms\Components\DocumentTotals;
@@ -27,7 +28,6 @@ use App\Models\Common\Offering;
27 28
 use App\Utilities\Currency\CurrencyAccessor;
28 29
 use App\Utilities\Currency\CurrencyConverter;
29 30
 use App\Utilities\RateCalculator;
30
-use Awcodes\TableRepeater\Components\TableRepeater;
31 31
 use Awcodes\TableRepeater\Header;
32 32
 use Filament\Forms;
33 33
 use Filament\Forms\Form;
@@ -90,8 +90,8 @@ class RecurringInvoiceResource extends Resource
90 90
                                 Forms\Components\Select::make('discount_method')
91 91
                                     ->label('Discount method')
92 92
                                     ->options(DocumentDiscountMethod::class)
93
-                                    ->selectablePlaceholder(false)
94
-                                    ->default(DocumentDiscountMethod::PerLineItem)
93
+                                    ->softRequired()
94
+                                    ->default($settings->discount_method)
95 95
                                     ->afterStateUpdated(function ($state, Forms\Set $set) {
96 96
                                         $discountMethod = DocumentDiscountMethod::parse($state);
97 97
 
@@ -102,28 +102,32 @@ class RecurringInvoiceResource extends Resource
102 102
                                     ->live(),
103 103
                             ])->grow(true),
104 104
                         ])->from('md'),
105
-                        TableRepeater::make('lineItems')
105
+                        CustomTableRepeater::make('lineItems')
106
+                            ->hiddenLabel()
106 107
                             ->relationship()
107 108
                             ->saveRelationshipsUsing(null)
108 109
                             ->dehydrated(true)
110
+                            ->reorderable()
111
+                            ->orderColumn('line_number')
112
+                            ->reorderAtStart()
113
+                            ->cloneable()
114
+                            ->addActionLabel('Add an item')
109 115
                             ->headers(function (Forms\Get $get) use ($settings) {
110 116
                                 $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
111 117
 
112 118
                                 $headers = [
113 119
                                     Header::make($settings->resolveColumnLabel('item_name', 'Items'))
114
-                                        ->width($hasDiscounts ? '15%' : '20%'),
115
-                                    Header::make('Description')
116
-                                        ->width($hasDiscounts ? '15%' : '20%'),
120
+                                        ->width('30%'),
117 121
                                     Header::make($settings->resolveColumnLabel('unit_name', 'Quantity'))
118 122
                                         ->width('10%'),
119 123
                                     Header::make($settings->resolveColumnLabel('price_name', 'Price'))
120 124
                                         ->width('10%'),
121
-                                    Header::make('Taxes')
122
-                                        ->width($hasDiscounts ? '20%' : '30%'),
123 125
                                 ];
124 126
 
125 127
                                 if ($hasDiscounts) {
126
-                                    $headers[] = Header::make('Discounts')->width('20%');
128
+                                    $headers[] = Header::make('Adjustments')->width('30%');
129
+                                } else {
130
+                                    $headers[] = Header::make('Taxes')->width('30%');
127 131
                                 }
128 132
 
129 133
                                 $headers[] = Header::make($settings->resolveColumnLabel('amount_name', 'Amount'))
@@ -133,61 +137,68 @@ class RecurringInvoiceResource extends Resource
133 137
                                 return $headers;
134 138
                             })
135 139
                             ->schema([
136
-                                CreateOfferingSelect::make('offering_id')
137
-                                    ->label('Item')
138
-                                    ->required()
139
-                                    ->live()
140
-                                    ->sellable()
141
-                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state, ?DocumentLineItem $record) {
142
-                                        $offeringId = $state;
143
-                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
144
-                                        $isPerLineItem = $discountMethod->isPerLineItem();
140
+                                Forms\Components\Group::make([
141
+                                    CreateOfferingSelect::make('offering_id')
142
+                                        ->label('Item')
143
+                                        ->hiddenLabel()
144
+                                        ->placeholder('Select item')
145
+                                        ->required()
146
+                                        ->live()
147
+                                        ->inlineSuffix()
148
+                                        ->sellable()
149
+                                        ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state, ?DocumentLineItem $record) {
150
+                                            $offeringId = $state;
151
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
152
+                                            $isPerLineItem = $discountMethod->isPerLineItem();
153
+
154
+                                            $existingTaxIds = [];
155
+                                            $existingDiscountIds = [];
156
+
157
+                                            if ($record) {
158
+                                                $existingTaxIds = $record->salesTaxes()->pluck('adjustments.id')->toArray();
159
+                                                if ($isPerLineItem) {
160
+                                                    $existingDiscountIds = $record->salesDiscounts()->pluck('adjustments.id')->toArray();
161
+                                                }
162
+                                            }
145 163
 
146
-                                        $existingTaxIds = [];
147
-                                        $existingDiscountIds = [];
164
+                                            $with = [
165
+                                                'salesTaxes' => static function ($query) use ($existingTaxIds) {
166
+                                                    $query->where(static function ($query) use ($existingTaxIds) {
167
+                                                        $query->where('status', AdjustmentStatus::Active)
168
+                                                            ->orWhereIn('adjustments.id', $existingTaxIds);
169
+                                                    });
170
+                                                },
171
+                                            ];
148 172
 
149
-                                        if ($record) {
150
-                                            $existingTaxIds = $record->salesTaxes()->pluck('adjustments.id')->toArray();
151 173
                                             if ($isPerLineItem) {
152
-                                                $existingDiscountIds = $record->salesDiscounts()->pluck('adjustments.id')->toArray();
174
+                                                $with['salesDiscounts'] = static function ($query) use ($existingDiscountIds) {
175
+                                                    $query->where(static function ($query) use ($existingDiscountIds) {
176
+                                                        $query->where('status', AdjustmentStatus::Active)
177
+                                                            ->orWhereIn('adjustments.id', $existingDiscountIds);
178
+                                                    });
179
+                                                };
153 180
                                             }
154
-                                        }
155
-
156
-                                        $with = [
157
-                                            'salesTaxes' => static function ($query) use ($existingTaxIds) {
158
-                                                $query->where(static function ($query) use ($existingTaxIds) {
159
-                                                    $query->where('status', AdjustmentStatus::Active)
160
-                                                        ->orWhereIn('adjustments.id', $existingTaxIds);
161
-                                                });
162
-                                            },
163
-                                        ];
164
-
165
-                                        if ($isPerLineItem) {
166
-                                            $with['salesDiscounts'] = static function ($query) use ($existingDiscountIds) {
167
-                                                $query->where(static function ($query) use ($existingDiscountIds) {
168
-                                                    $query->where('status', AdjustmentStatus::Active)
169
-                                                        ->orWhereIn('adjustments.id', $existingDiscountIds);
170
-                                                });
171
-                                            };
172
-                                        }
173 181
 
174
-                                        $offeringRecord = Offering::with($with)->find($offeringId);
182
+                                            $offeringRecord = Offering::with($with)->find($offeringId);
175 183
 
176
-                                        if (! $offeringRecord) {
177
-                                            return;
178
-                                        }
184
+                                            if (! $offeringRecord) {
185
+                                                return;
186
+                                            }
179 187
 
180
-                                        $unitPrice = CurrencyConverter::convertToFloat($offeringRecord->price, $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency());
188
+                                            $unitPrice = CurrencyConverter::convertToFloat($offeringRecord->price, $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency());
181 189
 
182
-                                        $set('description', $offeringRecord->description);
183
-                                        $set('unit_price', $unitPrice);
184
-                                        $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
190
+                                            $set('description', $offeringRecord->description);
191
+                                            $set('unit_price', $unitPrice);
192
+                                            $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
185 193
 
186
-                                        if ($isPerLineItem) {
187
-                                            $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
188
-                                        }
189
-                                    }),
190
-                                Forms\Components\TextInput::make('description'),
194
+                                            if ($isPerLineItem) {
195
+                                                $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
196
+                                            }
197
+                                        }),
198
+                                    Forms\Components\TextInput::make('description')
199
+                                        ->placeholder('Enter item description')
200
+                                        ->hiddenLabel(),
201
+                                ])->columnSpan(1),
191 202
                                 Forms\Components\TextInput::make('quantity')
192 203
                                     ->required()
193 204
                                     ->numeric()
@@ -200,32 +211,40 @@ class RecurringInvoiceResource extends Resource
200 211
                                     ->live()
201 212
                                     ->maxValue(9999999999.99)
202 213
                                     ->default(0),
203
-                                CreateAdjustmentSelect::make('salesTaxes')
204
-                                    ->label('Taxes')
205
-                                    ->category(AdjustmentCategory::Tax)
206
-                                    ->type(AdjustmentType::Sales)
207
-                                    ->adjustmentsRelationship('salesTaxes')
208
-                                    ->saveRelationshipsUsing(null)
209
-                                    ->dehydrated(true)
210
-                                    ->preload()
211
-                                    ->multiple()
212
-                                    ->live()
213
-                                    ->searchable(),
214
-                                CreateAdjustmentSelect::make('salesDiscounts')
215
-                                    ->label('Discounts')
216
-                                    ->category(AdjustmentCategory::Discount)
217
-                                    ->type(AdjustmentType::Sales)
218
-                                    ->adjustmentsRelationship('salesDiscounts')
219
-                                    ->saveRelationshipsUsing(null)
220
-                                    ->dehydrated(true)
221
-                                    ->multiple()
222
-                                    ->live()
223
-                                    ->hidden(function (Forms\Get $get) {
224
-                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
225
-
226
-                                        return $discountMethod->isPerDocument();
227
-                                    })
228
-                                    ->searchable(),
214
+                                Forms\Components\Group::make([
215
+                                    CreateAdjustmentSelect::make('salesTaxes')
216
+                                        ->label('Taxes')
217
+                                        ->hiddenLabel()
218
+                                        ->placeholder('Select taxes')
219
+                                        ->category(AdjustmentCategory::Tax)
220
+                                        ->type(AdjustmentType::Sales)
221
+                                        ->adjustmentsRelationship('salesTaxes')
222
+                                        ->saveRelationshipsUsing(null)
223
+                                        ->dehydrated(true)
224
+                                        ->inlineSuffix()
225
+                                        ->preload()
226
+                                        ->multiple()
227
+                                        ->live()
228
+                                        ->searchable(),
229
+                                    CreateAdjustmentSelect::make('salesDiscounts')
230
+                                        ->label('Discounts')
231
+                                        ->hiddenLabel()
232
+                                        ->placeholder('Select discounts')
233
+                                        ->category(AdjustmentCategory::Discount)
234
+                                        ->type(AdjustmentType::Sales)
235
+                                        ->adjustmentsRelationship('salesDiscounts')
236
+                                        ->saveRelationshipsUsing(null)
237
+                                        ->dehydrated(true)
238
+                                        ->inlineSuffix()
239
+                                        ->multiple()
240
+                                        ->live()
241
+                                        ->hidden(function (Forms\Get $get) {
242
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
243
+
244
+                                            return $discountMethod->isPerDocument();
245
+                                        })
246
+                                        ->searchable(),
247
+                                ])->columnSpan(1),
229 248
                                 Forms\Components\Placeholder::make('total')
230 249
                                     ->hiddenLabel()
231 250
                                     ->extraAttributes(['class' => 'text-left sm:text-right'])

+ 86
- 2
app/Filament/Forms/Components/CustomTableRepeater.php Wyświetl plik

@@ -4,10 +4,25 @@ namespace App\Filament\Forms\Components;
4 4
 
5 5
 use Awcodes\TableRepeater\Components\TableRepeater;
6 6
 use Closure;
7
+use Filament\Forms\Components\Actions\Action;
7 8
 
8 9
 class CustomTableRepeater extends TableRepeater
9 10
 {
10
-    protected bool | Closure | null $spreadsheet = null;
11
+    protected bool | Closure $spreadsheet = false;
12
+
13
+    protected bool | Closure $reorderAtStart = false;
14
+
15
+    /**
16
+     * @var array<string> | Closure | null
17
+     */
18
+    protected array | Closure | null $excludedAttributesForCloning = [
19
+        'id',
20
+        'line_number',
21
+        'created_by',
22
+        'updated_by',
23
+        'created_at',
24
+        'updated_at',
25
+    ];
11 26
 
12 27
     public function spreadsheet(bool | Closure $condition = true): static
13 28
     {
@@ -18,13 +33,45 @@ class CustomTableRepeater extends TableRepeater
18 33
 
19 34
     public function isSpreadsheet(): bool
20 35
     {
21
-        return $this->evaluate($this->spreadsheet) ?? false;
36
+        return (bool) $this->evaluate($this->spreadsheet);
37
+    }
38
+
39
+    public function reorderAtStart(bool | Closure $condition = true): static
40
+    {
41
+        $this->reorderAtStart = $condition;
42
+
43
+        return $this;
44
+    }
45
+
46
+    public function isReorderAtStart(): bool
47
+    {
48
+        return $this->evaluate($this->reorderAtStart) && $this->isReorderable();
49
+    }
50
+
51
+    /**
52
+     * @param  array<string> | Closure | null  $attributes
53
+     */
54
+    public function excludeAttributesForCloning(array | Closure | null $attributes): static
55
+    {
56
+        $this->excludedAttributesForCloning = $attributes;
57
+
58
+        return $this;
59
+    }
60
+
61
+    /**
62
+     * @return array<string> | null
63
+     */
64
+    public function getExcludedAttributesForCloning(): ?array
65
+    {
66
+        return $this->evaluate($this->excludedAttributesForCloning);
22 67
     }
23 68
 
24 69
     protected function setUp(): void
25 70
     {
26 71
         parent::setUp();
27 72
 
73
+        $this->minItems(1);
74
+
28 75
         $this->extraAttributes(function (): array {
29 76
             $attributes = [];
30 77
 
@@ -34,5 +81,42 @@ class CustomTableRepeater extends TableRepeater
34 81
 
35 82
             return $attributes;
36 83
         });
84
+
85
+        $this->reorderAction(function (Action $action) {
86
+            if ($this->isReorderAtStart()) {
87
+                $action->icon('heroicon-m-bars-3');
88
+            }
89
+
90
+            return $action;
91
+        });
92
+
93
+        $this->cloneAction(function (Action $action) {
94
+            return $action
95
+                ->action(function (array $arguments, CustomTableRepeater $component): void {
96
+                    $newUuid = $component->generateUuid();
97
+                    $items = $component->getState();
98
+
99
+                    $clone = $items[$arguments['item']];
100
+
101
+                    foreach ($component->getExcludedAttributesForCloning() as $attribute) {
102
+                        unset($clone[$attribute]);
103
+                    }
104
+
105
+                    if ($newUuid) {
106
+                        $items[$newUuid] = $clone;
107
+                    } else {
108
+                        $items[] = $clone;
109
+                    }
110
+
111
+                    $component->state($items);
112
+                    $component->collapsed(false, shouldMakeComponentCollapsible: false);
113
+                    $component->callAfterStateUpdated();
114
+                });
115
+        });
116
+    }
117
+
118
+    public function getView(): string
119
+    {
120
+        return 'filament.forms.components.custom-table-repeater';
37 121
     }
38 122
 }

+ 1
- 1
app/Models/Accounting/Document.php Wyświetl plik

@@ -27,7 +27,7 @@ abstract class Document extends Model
27 27
 
28 28
     public function lineItems(): MorphMany
29 29
     {
30
-        return $this->morphMany(DocumentLineItem::class, 'documentable');
30
+        return $this->morphMany(DocumentLineItem::class, 'documentable')->orderBy('line_number');
31 31
     }
32 32
 
33 33
     public function hasLineItems(): bool

+ 1
- 0
app/Models/Accounting/DocumentLineItem.php Wyświetl plik

@@ -36,6 +36,7 @@ class DocumentLineItem extends Model
36 36
         'unit_price',
37 37
         'tax_total',
38 38
         'discount_total',
39
+        'line_number',
39 40
         'created_by',
40 41
         'updated_by',
41 42
     ];

+ 3
- 0
app/Models/Setting/DocumentDefault.php Wyświetl plik

@@ -4,6 +4,7 @@ namespace App\Models\Setting;
4 4
 
5 5
 use App\Concerns\Blamable;
6 6
 use App\Concerns\CompanyOwned;
7
+use App\Enums\Accounting\DocumentDiscountMethod;
7 8
 use App\Enums\Accounting\DocumentType;
8 9
 use App\Enums\Setting\Font;
9 10
 use App\Enums\Setting\PaymentTerms;
@@ -32,6 +33,7 @@ class DocumentDefault extends Model
32 33
         'show_logo',
33 34
         'number_prefix',
34 35
         'payment_terms',
36
+        'discount_method',
35 37
         'header',
36 38
         'subheader',
37 39
         'terms',
@@ -51,6 +53,7 @@ class DocumentDefault extends Model
51 53
         'type' => DocumentType::class,
52 54
         'show_logo' => 'boolean',
53 55
         'payment_terms' => PaymentTerms::class,
56
+        'discount_method' => DocumentDiscountMethod::class,
54 57
         'font' => Font::class,
55 58
         'template' => Template::class,
56 59
         'item_name' => AsArrayObject::class,

+ 1
- 10
app/Providers/Filament/CompanyPanelProvider.php Wyświetl plik

@@ -298,18 +298,9 @@ class CompanyPanelProvider extends PanelProvider
298 298
     protected function configureSelect(): void
299 299
     {
300 300
         Select::configureUsing(function (Select $select): void {
301
-            $isSelectable = fn (): bool => ! $this->hasRequiredRule($select);
302
-
303 301
             $select
304 302
                 ->native(false)
305
-                ->selectablePlaceholder($isSelectable);
303
+                ->selectablePlaceholder(fn (Select $component) => ! $component->isRequired());
306 304
         });
307 305
     }
308
-
309
-    protected function hasRequiredRule(Select $component): bool
310
-    {
311
-        $rules = $component->getValidationRules();
312
-
313
-        return in_array('required', $rules, true);
314
-    }
315 306
 }

+ 5
- 1
app/Utilities/RateCalculator.php Wyświetl plik

@@ -28,8 +28,12 @@ class RateCalculator
28 28
         return (int) round($decimalRate * self::PERCENTAGE_SCALING_FACTOR);
29 29
     }
30 30
 
31
-    public static function parseLocalizedRate(string $value): int
31
+    public static function parseLocalizedRate(?string $value): int
32 32
     {
33
+        if (! $value) {
34
+            return 0;
35
+        }
36
+
33 37
         $format = Localization::firstOrFail()->number_format->value;
34 38
         [$decimalMark, $thousandsSeparator] = NumberFormat::from($format)->getFormattingParameters();
35 39
 

+ 113
- 112
composer.lock Wyświetl plik

@@ -368,16 +368,16 @@
368 368
         },
369 369
         {
370 370
             "name": "awcodes/filament-table-repeater",
371
-            "version": "v3.1.2",
371
+            "version": "v3.1.3",
372 372
             "source": {
373 373
                 "type": "git",
374 374
                 "url": "https://github.com/awcodes/filament-table-repeater.git",
375
-                "reference": "1cdfdd0fefbcc183960b4623cab17f6db880029e"
375
+                "reference": "fd8df8fbb94a41d0a031a75ef739538290a14a8c"
376 376
             },
377 377
             "dist": {
378 378
                 "type": "zip",
379
-                "url": "https://api.github.com/repos/awcodes/filament-table-repeater/zipball/1cdfdd0fefbcc183960b4623cab17f6db880029e",
380
-                "reference": "1cdfdd0fefbcc183960b4623cab17f6db880029e",
379
+                "url": "https://api.github.com/repos/awcodes/filament-table-repeater/zipball/fd8df8fbb94a41d0a031a75ef739538290a14a8c",
380
+                "reference": "fd8df8fbb94a41d0a031a75ef739538290a14a8c",
381 381
                 "shasum": ""
382 382
             },
383 383
             "require": {
@@ -431,7 +431,7 @@
431 431
             ],
432 432
             "support": {
433 433
                 "issues": "https://github.com/awcodes/filament-table-repeater/issues",
434
-                "source": "https://github.com/awcodes/filament-table-repeater/tree/v3.1.2"
434
+                "source": "https://github.com/awcodes/filament-table-repeater/tree/v3.1.3"
435 435
             },
436 436
             "funding": [
437 437
                 {
@@ -439,7 +439,7 @@
439 439
                     "type": "github"
440 440
                 }
441 441
             ],
442
-            "time": "2025-04-21T14:01:12+00:00"
442
+            "time": "2025-05-03T14:59:55+00:00"
443 443
         },
444 444
         {
445 445
             "name": "aws/aws-crt-php",
@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.343.2",
500
+            "version": "3.343.3",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "95d43e71d3395622394b36079f2fb2289d3284b3"
504
+                "reference": "d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/95d43e71d3395622394b36079f2fb2289d3284b3",
509
-                "reference": "95d43e71d3395622394b36079f2fb2289d3284b3",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979",
509
+                "reference": "d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -588,9 +588,9 @@
588 588
             "support": {
589 589
                 "forum": "https://github.com/aws/aws-sdk-php/discussions",
590 590
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
591
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.343.2"
591
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.343.3"
592 592
             },
593
-            "time": "2025-05-01T18:05:02+00:00"
593
+            "time": "2025-05-02T18:04:58+00:00"
594 594
         },
595 595
         {
596 596
             "name": "aws/aws-sdk-php-laravel",
@@ -6794,16 +6794,16 @@
6794 6794
         },
6795 6795
         {
6796 6796
             "name": "symfony/console",
6797
-            "version": "v7.2.5",
6797
+            "version": "v7.2.6",
6798 6798
             "source": {
6799 6799
                 "type": "git",
6800 6800
                 "url": "https://github.com/symfony/console.git",
6801
-                "reference": "e51498ea18570c062e7df29d05a7003585b19b88"
6801
+                "reference": "0e2e3f38c192e93e622e41ec37f4ca70cfedf218"
6802 6802
             },
6803 6803
             "dist": {
6804 6804
                 "type": "zip",
6805
-                "url": "https://api.github.com/repos/symfony/console/zipball/e51498ea18570c062e7df29d05a7003585b19b88",
6806
-                "reference": "e51498ea18570c062e7df29d05a7003585b19b88",
6805
+                "url": "https://api.github.com/repos/symfony/console/zipball/0e2e3f38c192e93e622e41ec37f4ca70cfedf218",
6806
+                "reference": "0e2e3f38c192e93e622e41ec37f4ca70cfedf218",
6807 6807
                 "shasum": ""
6808 6808
             },
6809 6809
             "require": {
@@ -6867,7 +6867,7 @@
6867 6867
                 "terminal"
6868 6868
             ],
6869 6869
             "support": {
6870
-                "source": "https://github.com/symfony/console/tree/v7.2.5"
6870
+                "source": "https://github.com/symfony/console/tree/v7.2.6"
6871 6871
             },
6872 6872
             "funding": [
6873 6873
                 {
@@ -6883,7 +6883,7 @@
6883 6883
                     "type": "tidelift"
6884 6884
                 }
6885 6885
             ],
6886
-            "time": "2025-03-12T08:11:12+00:00"
6886
+            "time": "2025-04-07T19:09:28+00:00"
6887 6887
         },
6888 6888
         {
6889 6889
             "name": "symfony/css-selector",
@@ -7314,16 +7314,16 @@
7314 7314
         },
7315 7315
         {
7316 7316
             "name": "symfony/html-sanitizer",
7317
-            "version": "v7.2.3",
7317
+            "version": "v7.2.6",
7318 7318
             "source": {
7319 7319
                 "type": "git",
7320 7320
                 "url": "https://github.com/symfony/html-sanitizer.git",
7321
-                "reference": "91443febe34cfa5e8e00425f892e6316db95bc23"
7321
+                "reference": "1bd0c8fd5938d9af3f081a7c43d360ddefd494ca"
7322 7322
             },
7323 7323
             "dist": {
7324 7324
                 "type": "zip",
7325
-                "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/91443febe34cfa5e8e00425f892e6316db95bc23",
7326
-                "reference": "91443febe34cfa5e8e00425f892e6316db95bc23",
7325
+                "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/1bd0c8fd5938d9af3f081a7c43d360ddefd494ca",
7326
+                "reference": "1bd0c8fd5938d9af3f081a7c43d360ddefd494ca",
7327 7327
                 "shasum": ""
7328 7328
             },
7329 7329
             "require": {
@@ -7363,7 +7363,7 @@
7363 7363
                 "sanitizer"
7364 7364
             ],
7365 7365
             "support": {
7366
-                "source": "https://github.com/symfony/html-sanitizer/tree/v7.2.3"
7366
+                "source": "https://github.com/symfony/html-sanitizer/tree/v7.2.6"
7367 7367
             },
7368 7368
             "funding": [
7369 7369
                 {
@@ -7379,20 +7379,20 @@
7379 7379
                     "type": "tidelift"
7380 7380
                 }
7381 7381
             ],
7382
-            "time": "2025-01-27T11:08:17+00:00"
7382
+            "time": "2025-03-31T08:29:03+00:00"
7383 7383
         },
7384 7384
         {
7385 7385
             "name": "symfony/http-foundation",
7386
-            "version": "v7.2.5",
7386
+            "version": "v7.2.6",
7387 7387
             "source": {
7388 7388
                 "type": "git",
7389 7389
                 "url": "https://github.com/symfony/http-foundation.git",
7390
-                "reference": "371272aeb6286f8135e028ca535f8e4d6f114126"
7390
+                "reference": "6023ec7607254c87c5e69fb3558255aca440d72b"
7391 7391
             },
7392 7392
             "dist": {
7393 7393
                 "type": "zip",
7394
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/371272aeb6286f8135e028ca535f8e4d6f114126",
7395
-                "reference": "371272aeb6286f8135e028ca535f8e4d6f114126",
7394
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6023ec7607254c87c5e69fb3558255aca440d72b",
7395
+                "reference": "6023ec7607254c87c5e69fb3558255aca440d72b",
7396 7396
                 "shasum": ""
7397 7397
             },
7398 7398
             "require": {
@@ -7441,7 +7441,7 @@
7441 7441
             "description": "Defines an object-oriented layer for the HTTP specification",
7442 7442
             "homepage": "https://symfony.com",
7443 7443
             "support": {
7444
-                "source": "https://github.com/symfony/http-foundation/tree/v7.2.5"
7444
+                "source": "https://github.com/symfony/http-foundation/tree/v7.2.6"
7445 7445
             },
7446 7446
             "funding": [
7447 7447
                 {
@@ -7457,20 +7457,20 @@
7457 7457
                     "type": "tidelift"
7458 7458
                 }
7459 7459
             ],
7460
-            "time": "2025-03-25T15:54:33+00:00"
7460
+            "time": "2025-04-09T08:14:01+00:00"
7461 7461
         },
7462 7462
         {
7463 7463
             "name": "symfony/http-kernel",
7464
-            "version": "v7.2.5",
7464
+            "version": "v7.2.6",
7465 7465
             "source": {
7466 7466
                 "type": "git",
7467 7467
                 "url": "https://github.com/symfony/http-kernel.git",
7468
-                "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54"
7468
+                "reference": "f9dec01e6094a063e738f8945ef69c0cfcf792ec"
7469 7469
             },
7470 7470
             "dist": {
7471 7471
                 "type": "zip",
7472
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b1fe91bc1fa454a806d3f98db4ba826eb9941a54",
7473
-                "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54",
7472
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f9dec01e6094a063e738f8945ef69c0cfcf792ec",
7473
+                "reference": "f9dec01e6094a063e738f8945ef69c0cfcf792ec",
7474 7474
                 "shasum": ""
7475 7475
             },
7476 7476
             "require": {
@@ -7555,7 +7555,7 @@
7555 7555
             "description": "Provides a structured process for converting a Request into a Response",
7556 7556
             "homepage": "https://symfony.com",
7557 7557
             "support": {
7558
-                "source": "https://github.com/symfony/http-kernel/tree/v7.2.5"
7558
+                "source": "https://github.com/symfony/http-kernel/tree/v7.2.6"
7559 7559
             },
7560 7560
             "funding": [
7561 7561
                 {
@@ -7571,20 +7571,20 @@
7571 7571
                     "type": "tidelift"
7572 7572
                 }
7573 7573
             ],
7574
-            "time": "2025-03-28T13:32:50+00:00"
7574
+            "time": "2025-05-02T09:04:03+00:00"
7575 7575
         },
7576 7576
         {
7577 7577
             "name": "symfony/intl",
7578
-            "version": "v6.4.15",
7578
+            "version": "v6.4.21",
7579 7579
             "source": {
7580 7580
                 "type": "git",
7581 7581
                 "url": "https://github.com/symfony/intl.git",
7582
-                "reference": "b1d5e8d82615b60f229216edfee0b59e2ef66da6"
7582
+                "reference": "b248d227fa10fd6345efd4c1c74efaa1c1de6f76"
7583 7583
             },
7584 7584
             "dist": {
7585 7585
                 "type": "zip",
7586
-                "url": "https://api.github.com/repos/symfony/intl/zipball/b1d5e8d82615b60f229216edfee0b59e2ef66da6",
7587
-                "reference": "b1d5e8d82615b60f229216edfee0b59e2ef66da6",
7586
+                "url": "https://api.github.com/repos/symfony/intl/zipball/b248d227fa10fd6345efd4c1c74efaa1c1de6f76",
7587
+                "reference": "b248d227fa10fd6345efd4c1c74efaa1c1de6f76",
7588 7588
                 "shasum": ""
7589 7589
             },
7590 7590
             "require": {
@@ -7638,7 +7638,7 @@
7638 7638
                 "localization"
7639 7639
             ],
7640 7640
             "support": {
7641
-                "source": "https://github.com/symfony/intl/tree/v6.4.15"
7641
+                "source": "https://github.com/symfony/intl/tree/v6.4.21"
7642 7642
             },
7643 7643
             "funding": [
7644 7644
                 {
@@ -7654,20 +7654,20 @@
7654 7654
                     "type": "tidelift"
7655 7655
                 }
7656 7656
             ],
7657
-            "time": "2024-11-08T15:28:48+00:00"
7657
+            "time": "2025-04-07T19:02:30+00:00"
7658 7658
         },
7659 7659
         {
7660 7660
             "name": "symfony/mailer",
7661
-            "version": "v7.2.3",
7661
+            "version": "v7.2.6",
7662 7662
             "source": {
7663 7663
                 "type": "git",
7664 7664
                 "url": "https://github.com/symfony/mailer.git",
7665
-                "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3"
7665
+                "reference": "998692469d6e698c6eadc7ef37a6530a9eabb356"
7666 7666
             },
7667 7667
             "dist": {
7668 7668
                 "type": "zip",
7669
-                "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3",
7670
-                "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3",
7669
+                "url": "https://api.github.com/repos/symfony/mailer/zipball/998692469d6e698c6eadc7ef37a6530a9eabb356",
7670
+                "reference": "998692469d6e698c6eadc7ef37a6530a9eabb356",
7671 7671
                 "shasum": ""
7672 7672
             },
7673 7673
             "require": {
@@ -7718,7 +7718,7 @@
7718 7718
             "description": "Helps sending emails",
7719 7719
             "homepage": "https://symfony.com",
7720 7720
             "support": {
7721
-                "source": "https://github.com/symfony/mailer/tree/v7.2.3"
7721
+                "source": "https://github.com/symfony/mailer/tree/v7.2.6"
7722 7722
             },
7723 7723
             "funding": [
7724 7724
                 {
@@ -7734,20 +7734,20 @@
7734 7734
                     "type": "tidelift"
7735 7735
                 }
7736 7736
             ],
7737
-            "time": "2025-01-27T11:08:17+00:00"
7737
+            "time": "2025-04-04T09:50:51+00:00"
7738 7738
         },
7739 7739
         {
7740 7740
             "name": "symfony/mime",
7741
-            "version": "v7.2.4",
7741
+            "version": "v7.2.6",
7742 7742
             "source": {
7743 7743
                 "type": "git",
7744 7744
                 "url": "https://github.com/symfony/mime.git",
7745
-                "reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
7745
+                "reference": "706e65c72d402539a072d0d6ad105fff6c161ef1"
7746 7746
             },
7747 7747
             "dist": {
7748 7748
                 "type": "zip",
7749
-                "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
7750
-                "reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
7749
+                "url": "https://api.github.com/repos/symfony/mime/zipball/706e65c72d402539a072d0d6ad105fff6c161ef1",
7750
+                "reference": "706e65c72d402539a072d0d6ad105fff6c161ef1",
7751 7751
                 "shasum": ""
7752 7752
             },
7753 7753
             "require": {
@@ -7802,7 +7802,7 @@
7802 7802
                 "mime-type"
7803 7803
             ],
7804 7804
             "support": {
7805
-                "source": "https://github.com/symfony/mime/tree/v7.2.4"
7805
+                "source": "https://github.com/symfony/mime/tree/v7.2.6"
7806 7806
             },
7807 7807
             "funding": [
7808 7808
                 {
@@ -7818,11 +7818,11 @@
7818 7818
                     "type": "tidelift"
7819 7819
                 }
7820 7820
             ],
7821
-            "time": "2025-02-19T08:51:20+00:00"
7821
+            "time": "2025-04-27T13:34:41+00:00"
7822 7822
         },
7823 7823
         {
7824 7824
             "name": "symfony/polyfill-ctype",
7825
-            "version": "v1.31.0",
7825
+            "version": "v1.32.0",
7826 7826
             "source": {
7827 7827
                 "type": "git",
7828 7828
                 "url": "https://github.com/symfony/polyfill-ctype.git",
@@ -7881,7 +7881,7 @@
7881 7881
                 "portable"
7882 7882
             ],
7883 7883
             "support": {
7884
-                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
7884
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
7885 7885
             },
7886 7886
             "funding": [
7887 7887
                 {
@@ -7901,7 +7901,7 @@
7901 7901
         },
7902 7902
         {
7903 7903
             "name": "symfony/polyfill-intl-grapheme",
7904
-            "version": "v1.31.0",
7904
+            "version": "v1.32.0",
7905 7905
             "source": {
7906 7906
                 "type": "git",
7907 7907
                 "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
@@ -7959,7 +7959,7 @@
7959 7959
                 "shim"
7960 7960
             ],
7961 7961
             "support": {
7962
-                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
7962
+                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0"
7963 7963
             },
7964 7964
             "funding": [
7965 7965
                 {
@@ -7979,16 +7979,16 @@
7979 7979
         },
7980 7980
         {
7981 7981
             "name": "symfony/polyfill-intl-idn",
7982
-            "version": "v1.31.0",
7982
+            "version": "v1.32.0",
7983 7983
             "source": {
7984 7984
                 "type": "git",
7985 7985
                 "url": "https://github.com/symfony/polyfill-intl-idn.git",
7986
-                "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
7986
+                "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
7987 7987
             },
7988 7988
             "dist": {
7989 7989
                 "type": "zip",
7990
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
7991
-                "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
7990
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
7991
+                "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
7992 7992
                 "shasum": ""
7993 7993
             },
7994 7994
             "require": {
@@ -8042,7 +8042,7 @@
8042 8042
                 "shim"
8043 8043
             ],
8044 8044
             "support": {
8045
-                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
8045
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
8046 8046
             },
8047 8047
             "funding": [
8048 8048
                 {
@@ -8058,11 +8058,11 @@
8058 8058
                     "type": "tidelift"
8059 8059
                 }
8060 8060
             ],
8061
-            "time": "2024-09-09T11:45:10+00:00"
8061
+            "time": "2024-09-10T14:38:51+00:00"
8062 8062
         },
8063 8063
         {
8064 8064
             "name": "symfony/polyfill-intl-normalizer",
8065
-            "version": "v1.31.0",
8065
+            "version": "v1.32.0",
8066 8066
             "source": {
8067 8067
                 "type": "git",
8068 8068
                 "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -8123,7 +8123,7 @@
8123 8123
                 "shim"
8124 8124
             ],
8125 8125
             "support": {
8126
-                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
8126
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
8127 8127
             },
8128 8128
             "funding": [
8129 8129
                 {
@@ -8143,19 +8143,20 @@
8143 8143
         },
8144 8144
         {
8145 8145
             "name": "symfony/polyfill-mbstring",
8146
-            "version": "v1.31.0",
8146
+            "version": "v1.32.0",
8147 8147
             "source": {
8148 8148
                 "type": "git",
8149 8149
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
8150
-                "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
8150
+                "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
8151 8151
             },
8152 8152
             "dist": {
8153 8153
                 "type": "zip",
8154
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
8155
-                "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
8154
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
8155
+                "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
8156 8156
                 "shasum": ""
8157 8157
             },
8158 8158
             "require": {
8159
+                "ext-iconv": "*",
8159 8160
                 "php": ">=7.2"
8160 8161
             },
8161 8162
             "provide": {
@@ -8203,7 +8204,7 @@
8203 8204
                 "shim"
8204 8205
             ],
8205 8206
             "support": {
8206
-                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
8207
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
8207 8208
             },
8208 8209
             "funding": [
8209 8210
                 {
@@ -8219,20 +8220,20 @@
8219 8220
                     "type": "tidelift"
8220 8221
                 }
8221 8222
             ],
8222
-            "time": "2024-09-09T11:45:10+00:00"
8223
+            "time": "2024-12-23T08:48:59+00:00"
8223 8224
         },
8224 8225
         {
8225 8226
             "name": "symfony/polyfill-php80",
8226
-            "version": "v1.31.0",
8227
+            "version": "v1.32.0",
8227 8228
             "source": {
8228 8229
                 "type": "git",
8229 8230
                 "url": "https://github.com/symfony/polyfill-php80.git",
8230
-                "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
8231
+                "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
8231 8232
             },
8232 8233
             "dist": {
8233 8234
                 "type": "zip",
8234
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
8235
-                "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
8235
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
8236
+                "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
8236 8237
                 "shasum": ""
8237 8238
             },
8238 8239
             "require": {
@@ -8283,7 +8284,7 @@
8283 8284
                 "shim"
8284 8285
             ],
8285 8286
             "support": {
8286
-                "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
8287
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
8287 8288
             },
8288 8289
             "funding": [
8289 8290
                 {
@@ -8299,11 +8300,11 @@
8299 8300
                     "type": "tidelift"
8300 8301
                 }
8301 8302
             ],
8302
-            "time": "2024-09-09T11:45:10+00:00"
8303
+            "time": "2025-01-02T08:10:11+00:00"
8303 8304
         },
8304 8305
         {
8305 8306
             "name": "symfony/polyfill-php83",
8306
-            "version": "v1.31.0",
8307
+            "version": "v1.32.0",
8307 8308
             "source": {
8308 8309
                 "type": "git",
8309 8310
                 "url": "https://github.com/symfony/polyfill-php83.git",
@@ -8359,7 +8360,7 @@
8359 8360
                 "shim"
8360 8361
             ],
8361 8362
             "support": {
8362
-                "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
8363
+                "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0"
8363 8364
             },
8364 8365
             "funding": [
8365 8366
                 {
@@ -8379,7 +8380,7 @@
8379 8380
         },
8380 8381
         {
8381 8382
             "name": "symfony/polyfill-uuid",
8382
-            "version": "v1.31.0",
8383
+            "version": "v1.32.0",
8383 8384
             "source": {
8384 8385
                 "type": "git",
8385 8386
                 "url": "https://github.com/symfony/polyfill-uuid.git",
@@ -8438,7 +8439,7 @@
8438 8439
                 "uuid"
8439 8440
             ],
8440 8441
             "support": {
8441
-                "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0"
8442
+                "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0"
8442 8443
             },
8443 8444
             "funding": [
8444 8445
                 {
@@ -8683,16 +8684,16 @@
8683 8684
         },
8684 8685
         {
8685 8686
             "name": "symfony/string",
8686
-            "version": "v7.2.0",
8687
+            "version": "v7.2.6",
8687 8688
             "source": {
8688 8689
                 "type": "git",
8689 8690
                 "url": "https://github.com/symfony/string.git",
8690
-                "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82"
8691
+                "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931"
8691 8692
             },
8692 8693
             "dist": {
8693 8694
                 "type": "zip",
8694
-                "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82",
8695
-                "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82",
8695
+                "url": "https://api.github.com/repos/symfony/string/zipball/a214fe7d62bd4df2a76447c67c6b26e1d5e74931",
8696
+                "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931",
8696 8697
                 "shasum": ""
8697 8698
             },
8698 8699
             "require": {
@@ -8750,7 +8751,7 @@
8750 8751
                 "utf8"
8751 8752
             ],
8752 8753
             "support": {
8753
-                "source": "https://github.com/symfony/string/tree/v7.2.0"
8754
+                "source": "https://github.com/symfony/string/tree/v7.2.6"
8754 8755
             },
8755 8756
             "funding": [
8756 8757
                 {
@@ -8766,20 +8767,20 @@
8766 8767
                     "type": "tidelift"
8767 8768
                 }
8768 8769
             ],
8769
-            "time": "2024-11-13T13:31:26+00:00"
8770
+            "time": "2025-04-20T20:18:16+00:00"
8770 8771
         },
8771 8772
         {
8772 8773
             "name": "symfony/translation",
8773
-            "version": "v7.2.4",
8774
+            "version": "v7.2.6",
8774 8775
             "source": {
8775 8776
                 "type": "git",
8776 8777
                 "url": "https://github.com/symfony/translation.git",
8777
-                "reference": "283856e6981286cc0d800b53bd5703e8e363f05a"
8778
+                "reference": "e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6"
8778 8779
             },
8779 8780
             "dist": {
8780 8781
                 "type": "zip",
8781
-                "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a",
8782
-                "reference": "283856e6981286cc0d800b53bd5703e8e363f05a",
8782
+                "url": "https://api.github.com/repos/symfony/translation/zipball/e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6",
8783
+                "reference": "e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6",
8783 8784
                 "shasum": ""
8784 8785
             },
8785 8786
             "require": {
@@ -8845,7 +8846,7 @@
8845 8846
             "description": "Provides tools to internationalize your application",
8846 8847
             "homepage": "https://symfony.com",
8847 8848
             "support": {
8848
-                "source": "https://github.com/symfony/translation/tree/v7.2.4"
8849
+                "source": "https://github.com/symfony/translation/tree/v7.2.6"
8849 8850
             },
8850 8851
             "funding": [
8851 8852
                 {
@@ -8861,7 +8862,7 @@
8861 8862
                     "type": "tidelift"
8862 8863
                 }
8863 8864
             ],
8864
-            "time": "2025-02-13T10:27:23+00:00"
8865
+            "time": "2025-04-07T19:09:28+00:00"
8865 8866
         },
8866 8867
         {
8867 8868
             "name": "symfony/translation-contracts",
@@ -9017,16 +9018,16 @@
9017 9018
         },
9018 9019
         {
9019 9020
             "name": "symfony/var-dumper",
9020
-            "version": "v7.2.3",
9021
+            "version": "v7.2.6",
9021 9022
             "source": {
9022 9023
                 "type": "git",
9023 9024
                 "url": "https://github.com/symfony/var-dumper.git",
9024
-                "reference": "82b478c69745d8878eb60f9a049a4d584996f73a"
9025
+                "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb"
9025 9026
             },
9026 9027
             "dist": {
9027 9028
                 "type": "zip",
9028
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a",
9029
-                "reference": "82b478c69745d8878eb60f9a049a4d584996f73a",
9029
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9c46038cd4ed68952166cf7001b54eb539184ccb",
9030
+                "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb",
9030 9031
                 "shasum": ""
9031 9032
             },
9032 9033
             "require": {
@@ -9080,7 +9081,7 @@
9080 9081
                 "dump"
9081 9082
             ],
9082 9083
             "support": {
9083
-                "source": "https://github.com/symfony/var-dumper/tree/v7.2.3"
9084
+                "source": "https://github.com/symfony/var-dumper/tree/v7.2.6"
9084 9085
             },
9085 9086
             "funding": [
9086 9087
                 {
@@ -9096,7 +9097,7 @@
9096 9097
                     "type": "tidelift"
9097 9098
                 }
9098 9099
             ],
9099
-            "time": "2025-01-17T11:39:41+00:00"
9100
+            "time": "2025-04-09T08:14:01+00:00"
9100 9101
         },
9101 9102
         {
9102 9103
             "name": "tijsverkoyen/css-to-inline-styles",
@@ -13098,16 +13099,16 @@
13098 13099
         },
13099 13100
         {
13100 13101
             "name": "symfony/polyfill-iconv",
13101
-            "version": "v1.31.0",
13102
+            "version": "v1.32.0",
13102 13103
             "source": {
13103 13104
                 "type": "git",
13104 13105
                 "url": "https://github.com/symfony/polyfill-iconv.git",
13105
-                "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956"
13106
+                "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa"
13106 13107
             },
13107 13108
             "dist": {
13108 13109
                 "type": "zip",
13109
-                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956",
13110
-                "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956",
13110
+                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/5f3b930437ae03ae5dff61269024d8ea1b3774aa",
13111
+                "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa",
13111 13112
                 "shasum": ""
13112 13113
             },
13113 13114
             "require": {
@@ -13158,7 +13159,7 @@
13158 13159
                 "shim"
13159 13160
             ],
13160 13161
             "support": {
13161
-                "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0"
13162
+                "source": "https://github.com/symfony/polyfill-iconv/tree/v1.32.0"
13162 13163
             },
13163 13164
             "funding": [
13164 13165
                 {
@@ -13174,7 +13175,7 @@
13174 13175
                     "type": "tidelift"
13175 13176
                 }
13176 13177
             ],
13177
-            "time": "2024-09-09T11:45:10+00:00"
13178
+            "time": "2024-09-17T14:58:18+00:00"
13178 13179
         },
13179 13180
         {
13180 13181
             "name": "symfony/stopwatch",
@@ -13240,16 +13241,16 @@
13240 13241
         },
13241 13242
         {
13242 13243
             "name": "symfony/yaml",
13243
-            "version": "v7.2.5",
13244
+            "version": "v7.2.6",
13244 13245
             "source": {
13245 13246
                 "type": "git",
13246 13247
                 "url": "https://github.com/symfony/yaml.git",
13247
-                "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912"
13248
+                "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23"
13248 13249
             },
13249 13250
             "dist": {
13250 13251
                 "type": "zip",
13251
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912",
13252
-                "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912",
13252
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/0feafffb843860624ddfd13478f481f4c3cd8b23",
13253
+                "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23",
13253 13254
                 "shasum": ""
13254 13255
             },
13255 13256
             "require": {
@@ -13292,7 +13293,7 @@
13292 13293
             "description": "Loads and dumps YAML files",
13293 13294
             "homepage": "https://symfony.com",
13294 13295
             "support": {
13295
-                "source": "https://github.com/symfony/yaml/tree/v7.2.5"
13296
+                "source": "https://github.com/symfony/yaml/tree/v7.2.6"
13296 13297
             },
13297 13298
             "funding": [
13298 13299
                 {
@@ -13308,7 +13309,7 @@
13308 13309
                     "type": "tidelift"
13309 13310
                 }
13310 13311
             ],
13311
-            "time": "2025-03-03T07:12:39+00:00"
13312
+            "time": "2025-04-04T10:10:11+00:00"
13312 13313
         },
13313 13314
         {
13314 13315
             "name": "ta-tikoma/phpunit-architecture-test",

+ 27
- 1
database/factories/Accounting/BillFactory.php Wyświetl plik

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace Database\Factories\Accounting;
4 4
 
5
+use App\Enums\Accounting\AdjustmentComputation;
5 6
 use App\Enums\Accounting\BillStatus;
7
+use App\Enums\Accounting\DocumentDiscountMethod;
6 8
 use App\Enums\Accounting\PaymentMethod;
7 9
 use App\Models\Accounting\Bill;
8 10
 use App\Models\Accounting\DocumentLineItem;
@@ -11,6 +13,7 @@ use App\Models\Common\Vendor;
11 13
 use App\Models\Company;
12 14
 use App\Models\Setting\DocumentDefault;
13 15
 use App\Utilities\Currency\CurrencyConverter;
16
+use App\Utilities\RateCalculator;
14 17
 use Illuminate\Database\Eloquent\Factories\Factory;
15 18
 use Illuminate\Support\Carbon;
16 19
 
@@ -49,6 +52,17 @@ class BillFactory extends Factory
49 52
             'date' => $billDate,
50 53
             'due_date' => Carbon::parse($billDate)->addDays($dueDays),
51 54
             'status' => BillStatus::Open,
55
+            'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
56
+            'discount_computation' => AdjustmentComputation::Percentage,
57
+            'discount_rate' => function (array $attributes) {
58
+                $discountMethod = DocumentDiscountMethod::parse($attributes['discount_method']);
59
+
60
+                if ($discountMethod?->isPerDocument()) {
61
+                    return $this->faker->numberBetween(50000, 200000); // 5% - 20%
62
+                }
63
+
64
+                return 0;
65
+            },
52 66
             'currency_code' => function (array $attributes) {
53 67
                 $vendor = Vendor::find($attributes['vendor_id']);
54 68
 
@@ -226,7 +240,19 @@ class BillFactory extends Factory
226 240
 
227 241
         $subtotalCents = $bill->lineItems()->sum('subtotal');
228 242
         $taxTotalCents = $bill->lineItems()->sum('tax_total');
229
-        $discountTotalCents = $bill->lineItems()->sum('discount_total');
243
+
244
+        $discountTotalCents = 0;
245
+
246
+        if ($bill->discount_method?->isPerLineItem()) {
247
+            $discountTotalCents = $bill->lineItems()->sum('discount_total');
248
+        } elseif ($bill->discount_method?->isPerDocument() && $bill->discount_rate) {
249
+            if ($bill->discount_computation?->isPercentage()) {
250
+                $scaledRate = RateCalculator::parseLocalizedRate($bill->discount_rate);
251
+                $discountTotalCents = RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
252
+            } else {
253
+                $discountTotalCents = CurrencyConverter::convertToCents($bill->discount_rate, $bill->currency_code);
254
+            }
255
+        }
230 256
 
231 257
         $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
232 258
         $currencyCode = $bill->currency_code;

+ 15
- 3
database/factories/Accounting/DocumentLineItemFactory.php Wyświetl plik

@@ -56,7 +56,11 @@ class DocumentLineItemFactory extends Factory
56 56
                 ]);
57 57
 
58 58
                 $lineItem->salesTaxes()->syncWithoutDetaching($offering->salesTaxes->pluck('id')->toArray());
59
-                $lineItem->salesDiscounts()->syncWithoutDetaching($offering->salesDiscounts->pluck('id')->toArray());
59
+
60
+                // Only sync discounts if the discount method is per_line_item
61
+                if ($lineItem->documentable->discount_method?->isPerLineItem() ?? true) {
62
+                    $lineItem->salesDiscounts()->syncWithoutDetaching($offering->salesDiscounts->pluck('id')->toArray());
63
+                }
60 64
 
61 65
                 $lineItem->refresh();
62 66
 
@@ -88,7 +92,11 @@ class DocumentLineItemFactory extends Factory
88 92
                 ]);
89 93
 
90 94
                 $lineItem->salesTaxes()->syncWithoutDetaching($offering->salesTaxes->pluck('id')->toArray());
91
-                $lineItem->salesDiscounts()->syncWithoutDetaching($offering->salesDiscounts->pluck('id')->toArray());
95
+
96
+                // Only sync discounts if the discount method is per_line_item
97
+                if ($lineItem->documentable->discount_method?->isPerLineItem() ?? true) {
98
+                    $lineItem->salesDiscounts()->syncWithoutDetaching($offering->salesDiscounts->pluck('id')->toArray());
99
+                }
92 100
 
93 101
                 $lineItem->refresh();
94 102
 
@@ -120,7 +128,11 @@ class DocumentLineItemFactory extends Factory
120 128
                 ]);
121 129
 
122 130
                 $lineItem->purchaseTaxes()->syncWithoutDetaching($offering->purchaseTaxes->pluck('id')->toArray());
123
-                $lineItem->purchaseDiscounts()->syncWithoutDetaching($offering->purchaseDiscounts->pluck('id')->toArray());
131
+
132
+                // Only sync discounts if the discount method is per_line_item
133
+                if ($lineItem->documentable->discount_method?->isPerLineItem() ?? true) {
134
+                    $lineItem->purchaseDiscounts()->syncWithoutDetaching($offering->purchaseDiscounts->pluck('id')->toArray());
135
+                }
124 136
 
125 137
                 $lineItem->refresh();
126 138
 

+ 27
- 1
database/factories/Accounting/EstimateFactory.php Wyświetl plik

@@ -2,6 +2,8 @@
2 2
 
3 3
 namespace Database\Factories\Accounting;
4 4
 
5
+use App\Enums\Accounting\AdjustmentComputation;
6
+use App\Enums\Accounting\DocumentDiscountMethod;
5 7
 use App\Enums\Accounting\EstimateStatus;
6 8
 use App\Models\Accounting\DocumentLineItem;
7 9
 use App\Models\Accounting\Estimate;
@@ -9,6 +11,7 @@ use App\Models\Common\Client;
9 11
 use App\Models\Company;
10 12
 use App\Models\Setting\DocumentDefault;
11 13
 use App\Utilities\Currency\CurrencyConverter;
14
+use App\Utilities\RateCalculator;
12 15
 use Illuminate\Database\Eloquent\Factories\Factory;
13 16
 use Illuminate\Support\Carbon;
14 17
 
@@ -41,6 +44,17 @@ class EstimateFactory extends Factory
41 44
             'date' => $estimateDate,
42 45
             'expiration_date' => Carbon::parse($estimateDate)->addDays($this->faker->numberBetween(14, 30)),
43 46
             'status' => EstimateStatus::Draft,
47
+            'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
48
+            'discount_computation' => AdjustmentComputation::Percentage,
49
+            'discount_rate' => function (array $attributes) {
50
+                $discountMethod = DocumentDiscountMethod::parse($attributes['discount_method']);
51
+
52
+                if ($discountMethod?->isPerDocument()) {
53
+                    return $this->faker->numberBetween(50000, 200000); // 5% - 20%
54
+                }
55
+
56
+                return 0;
57
+            },
44 58
             'currency_code' => function (array $attributes) {
45 59
                 $client = Client::find($attributes['client_id']);
46 60
 
@@ -207,7 +221,19 @@ class EstimateFactory extends Factory
207 221
 
208 222
         $subtotalCents = $estimate->lineItems()->sum('subtotal');
209 223
         $taxTotalCents = $estimate->lineItems()->sum('tax_total');
210
-        $discountTotalCents = $estimate->lineItems()->sum('discount_total');
224
+
225
+        $discountTotalCents = 0;
226
+
227
+        if ($estimate->discount_method?->isPerLineItem()) {
228
+            $discountTotalCents = $estimate->lineItems()->sum('discount_total');
229
+        } elseif ($estimate->discount_method?->isPerDocument() && $estimate->discount_rate) {
230
+            if ($estimate->discount_computation?->isPercentage()) {
231
+                $scaledRate = RateCalculator::parseLocalizedRate($estimate->discount_rate);
232
+                $discountTotalCents = RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
233
+            } else {
234
+                $discountTotalCents = CurrencyConverter::convertToCents($estimate->discount_rate, $estimate->currency_code);
235
+            }
236
+        }
211 237
 
212 238
         $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
213 239
         $currencyCode = $estimate->currency_code;

+ 27
- 1
database/factories/Accounting/InvoiceFactory.php Wyświetl plik

@@ -2,6 +2,8 @@
2 2
 
3 3
 namespace Database\Factories\Accounting;
4 4
 
5
+use App\Enums\Accounting\AdjustmentComputation;
6
+use App\Enums\Accounting\DocumentDiscountMethod;
5 7
 use App\Enums\Accounting\InvoiceStatus;
6 8
 use App\Enums\Accounting\PaymentMethod;
7 9
 use App\Models\Accounting\DocumentLineItem;
@@ -11,6 +13,7 @@ use App\Models\Common\Client;
11 13
 use App\Models\Company;
12 14
 use App\Models\Setting\DocumentDefault;
13 15
 use App\Utilities\Currency\CurrencyConverter;
16
+use App\Utilities\RateCalculator;
14 17
 use Illuminate\Database\Eloquent\Factories\Factory;
15 18
 use Illuminate\Support\Carbon;
16 19
 
@@ -43,6 +46,17 @@ class InvoiceFactory extends Factory
43 46
             'date' => $invoiceDate,
44 47
             'due_date' => Carbon::parse($invoiceDate)->addDays($this->faker->numberBetween(14, 60)),
45 48
             'status' => InvoiceStatus::Draft,
49
+            'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
50
+            'discount_computation' => AdjustmentComputation::Percentage,
51
+            'discount_rate' => function (array $attributes) {
52
+                $discountMethod = DocumentDiscountMethod::parse($attributes['discount_method']);
53
+
54
+                if ($discountMethod?->isPerDocument()) {
55
+                    return $this->faker->numberBetween(50000, 200000); // 5% - 20%
56
+                }
57
+
58
+                return 0;
59
+            },
46 60
             'currency_code' => function (array $attributes) {
47 61
                 $client = Client::find($attributes['client_id']);
48 62
 
@@ -251,7 +265,19 @@ class InvoiceFactory extends Factory
251 265
 
252 266
         $subtotalCents = $invoice->lineItems()->sum('subtotal');
253 267
         $taxTotalCents = $invoice->lineItems()->sum('tax_total');
254
-        $discountTotalCents = $invoice->lineItems()->sum('discount_total');
268
+
269
+        $discountTotalCents = 0;
270
+
271
+        if ($invoice->discount_method?->isPerLineItem()) {
272
+            $discountTotalCents = $invoice->lineItems()->sum('discount_total');
273
+        } elseif ($invoice->discount_method?->isPerDocument() && $invoice->discount_rate) {
274
+            if ($invoice->discount_computation?->isPercentage()) {
275
+                $scaledRate = RateCalculator::parseLocalizedRate($invoice->discount_rate);
276
+                $discountTotalCents = RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
277
+            } else {
278
+                $discountTotalCents = CurrencyConverter::convertToCents($invoice->discount_rate, $invoice->currency_code);
279
+            }
280
+        }
255 281
 
256 282
         $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
257 283
         $currencyCode = $invoice->currency_code;

+ 27
- 1
database/factories/Accounting/RecurringInvoiceFactory.php Wyświetl plik

@@ -2,8 +2,10 @@
2 2
 
3 3
 namespace Database\Factories\Accounting;
4 4
 
5
+use App\Enums\Accounting\AdjustmentComputation;
5 6
 use App\Enums\Accounting\DayOfMonth;
6 7
 use App\Enums\Accounting\DayOfWeek;
8
+use App\Enums\Accounting\DocumentDiscountMethod;
7 9
 use App\Enums\Accounting\EndType;
8 10
 use App\Enums\Accounting\Frequency;
9 11
 use App\Enums\Accounting\IntervalType;
@@ -15,6 +17,7 @@ use App\Models\Accounting\RecurringInvoice;
15 17
 use App\Models\Common\Client;
16 18
 use App\Models\Company;
17 19
 use App\Utilities\Currency\CurrencyConverter;
20
+use App\Utilities\RateCalculator;
18 21
 use Illuminate\Database\Eloquent\Factories\Factory;
19 22
 use Illuminate\Support\Carbon;
20 23
 
@@ -43,6 +46,17 @@ class RecurringInvoiceFactory extends Factory
43 46
             'order_number' => $this->faker->unique()->numerify('ORD-####'),
44 47
             'payment_terms' => PaymentTerms::Net30,
45 48
             'status' => RecurringInvoiceStatus::Draft,
49
+            'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
50
+            'discount_computation' => AdjustmentComputation::Percentage,
51
+            'discount_rate' => function (array $attributes) {
52
+                $discountMethod = DocumentDiscountMethod::parse($attributes['discount_method']);
53
+
54
+                if ($discountMethod?->isPerDocument()) {
55
+                    return $this->faker->numberBetween(50000, 200000); // 5% - 20%
56
+                }
57
+
58
+                return 0;
59
+            },
46 60
             'currency_code' => function (array $attributes) {
47 61
                 $client = Client::find($attributes['client_id']);
48 62
 
@@ -301,7 +315,19 @@ class RecurringInvoiceFactory extends Factory
301 315
 
302 316
         $subtotalCents = $recurringInvoice->lineItems()->sum('subtotal');
303 317
         $taxTotalCents = $recurringInvoice->lineItems()->sum('tax_total');
304
-        $discountTotalCents = $recurringInvoice->lineItems()->sum('discount_total');
318
+
319
+        $discountTotalCents = 0;
320
+
321
+        if ($recurringInvoice->discount_method?->isPerLineItem()) {
322
+            $discountTotalCents = $recurringInvoice->lineItems()->sum('discount_total');
323
+        } elseif ($recurringInvoice->discount_method?->isPerDocument() && $recurringInvoice->discount_rate) {
324
+            if ($recurringInvoice->discount_computation?->isPercentage()) {
325
+                $scaledRate = RateCalculator::parseLocalizedRate($recurringInvoice->discount_rate);
326
+                $discountTotalCents = RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
327
+            } else {
328
+                $discountTotalCents = CurrencyConverter::convertToCents($recurringInvoice->discount_rate, $recurringInvoice->currency_code);
329
+            }
330
+        }
305 331
 
306 332
         $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
307 333
         $currencyCode = $recurringInvoice->currency_code;

+ 28
- 0
database/migrations/2025_05_03_150845_add_discount_method_to_document_defaults_table.php Wyświetl plik

@@ -0,0 +1,28 @@
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::table('document_defaults', function (Blueprint $table) {
15
+            $table->string('discount_method')->default('per_document')->after('payment_terms');
16
+        });
17
+    }
18
+
19
+    /**
20
+     * Reverse the migrations.
21
+     */
22
+    public function down(): void
23
+    {
24
+        Schema::table('document_defaults', function (Blueprint $table) {
25
+            $table->dropColumn('discount_method');
26
+        });
27
+    }
28
+};

+ 37
- 0
database/migrations/2025_05_03_152233_update_discount_method_defaults_on_document_tables.php Wyświetl plik

@@ -0,0 +1,37 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+
6
+return new class extends Migration
7
+{
8
+    /**
9
+     * Run the migrations.
10
+     */
11
+    public function up(): void
12
+    {
13
+        if (Schema::hasColumn('bills', 'discount_method')) {
14
+            Schema::table('bills', function (Blueprint $table) {
15
+                $table->string('discount_method')->default('per_document')->change();
16
+            });
17
+        }
18
+
19
+        if (Schema::hasColumn('estimates', 'discount_method')) {
20
+            Schema::table('estimates', function (Blueprint $table) {
21
+                $table->string('discount_method')->default('per_document')->change();
22
+            });
23
+        }
24
+
25
+        if (Schema::hasColumn('recurring_invoices', 'discount_method')) {
26
+            Schema::table('recurring_invoices', function (Blueprint $table) {
27
+                $table->string('discount_method')->default('per_document')->change();
28
+            });
29
+        }
30
+
31
+        if (Schema::hasColumn('invoices', 'discount_method')) {
32
+            Schema::table('invoices', function (Blueprint $table) {
33
+                $table->string('discount_method')->default('per_document')->change();
34
+            });
35
+        }
36
+    }
37
+};

+ 28
- 0
database/migrations/2025_05_05_211551_add_sort_order_to_document_line_items.php Wyświetl plik

@@ -0,0 +1,28 @@
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::table('document_line_items', function (Blueprint $table) {
15
+            $table->integer('line_number')->nullable()->after('documentable_id');
16
+        });
17
+    }
18
+
19
+    /**
20
+     * Reverse the migrations.
21
+     */
22
+    public function down(): void
23
+    {
24
+        Schema::table('document_line_items', function (Blueprint $table) {
25
+            $table->dropColumn('line_number');
26
+        });
27
+    }
28
+};

+ 20
- 51
resources/css/filament/company/form-fields.css Wyświetl plik

@@ -42,46 +42,46 @@
42 42
 }
43 43
 
44 44
 /* Table Repeater Styles */
45
-:not(.is-spreadsheet) {
46
-    .table-repeater-container {
47
-        @apply rounded-none ring-0;
45
+.table-repeater-component:not(.is-spreadsheet) {
46
+    .table-repeater-row {
47
+        @apply hover:bg-gray-50 dark:hover:bg-white/5 transition-colors;
48 48
     }
49 49
 
50
-    .table-repeater-component {
51
-        @apply space-y-10;
50
+    .table-repeater-column {
51
+        @apply p-3;
52 52
     }
53 53
 
54
-    .table-repeater-component ul {
55
-        @apply justify-start;
54
+    .table-repeater-header {
55
+        @apply bg-gray-50 dark:bg-white/5 border-b border-gray-200 dark:border-white/5;
56 56
     }
57 57
 
58 58
     .table-repeater-row {
59 59
         @apply divide-x-0 !important;
60 60
     }
61 61
 
62
-    .table-repeater-column {
63
-        @apply py-2 !important;
64
-    }
65
-
66
-    .table-repeater-header {
67
-        @apply rounded-t-none !important;
62
+    .table-repeater-header tr {
63
+        @apply divide-x-0 text-sm;
68 64
     }
69 65
 
70
-    .table-repeater-rows-wrapper {
71
-        @apply divide-gray-300 last:border-b last:border-gray-300 dark:divide-white/20 dark:last:border-white/20;
66
+    .table-repeater-header-column {
67
+        @apply p-3 text-sm font-semibold text-gray-950 dark:text-white bg-gray-50 dark:bg-white/5;
72 68
     }
73 69
 
74
-    .table-repeater-header tr {
75
-        @apply divide-x-0 text-base sm:text-sm sm:leading-6 !important;
70
+    /* Chrome, Safari, Edge, Opera */
71
+    input::-webkit-outer-spin-button,
72
+    input::-webkit-inner-spin-button {
73
+        -webkit-appearance: none;
74
+        margin: 0;
76 75
     }
77 76
 
78
-    .table-repeater-header-column {
79
-        @apply ps-3 pe-3 font-semibold bg-gray-200 dark:bg-gray-800 rounded-none !important;
77
+    /* Firefox */
78
+    input[type=number] {
79
+        -moz-appearance: textfield;
80 80
     }
81 81
 }
82 82
 
83 83
 /* Excel/Spreadsheet styling */
84
-.is-spreadsheet {
84
+.table-repeater-component.is-spreadsheet {
85 85
     .table-repeater-container {
86 86
         overflow-x: auto;
87 87
         max-width: 100%;
@@ -165,37 +165,6 @@
165 165
     }
166 166
 }
167 167
 
168
-/* Responsive behavior */
169
-@media (max-width: theme('screens.sm')) {
170
-    .table-repeater-component.break-point-sm .table-repeater-container {
171
-        overflow-x: visible;
172
-    }
173
-}
174
-
175
-@media (max-width: theme('screens.md')) {
176
-    .table-repeater-component.break-point-md .table-repeater-container {
177
-        overflow-x: visible;
178
-    }
179
-}
180
-
181
-@media (max-width: theme('screens.lg')) {
182
-    .table-repeater-component.break-point-lg .table-repeater-container {
183
-        overflow-x: visible;
184
-    }
185
-}
186
-
187
-@media (max-width: theme('screens.xl')) {
188
-    .table-repeater-component.break-point-xl .table-repeater-container {
189
-        overflow-x: visible;
190
-    }
191
-}
192
-
193
-@media (max-width: theme('screens.2xl')) {
194
-    .table-repeater-component.break-point-2xl .table-repeater-container {
195
-        overflow-x: visible;
196
-    }
197
-}
198
-
199 168
 .is-spreadsheet .table-repeater-column .fi-input-wrp-suffix {
200 169
     padding-right: 0 !important;
201 170
 }

+ 1
- 1
resources/views/components/company/document-template/container.blade.php Wyświetl plik

@@ -6,7 +6,7 @@
6 6
     <div
7 7
         @class([
8 8
             'doc-template-paper bg-[#ffffff] dark:bg-gray-800 rounded-sm shadow-xl',
9
-            'w-full max-w-[820px] max-h-[1024px] overflow-y-auto' => $preview === false,
9
+            'w-full max-w-[820px] min-h-[1066px] max-h-[1200px] overflow-y-auto' => $preview === false,
10 10
             'w-[38.25rem] h-[49.5rem] overflow-hidden' => $preview === true,
11 11
         ])
12 12
         @style([

+ 1
- 1
resources/views/filament/company/components/document-templates/default.blade.php Wyświetl plik

@@ -128,7 +128,7 @@
128 128
     </x-company.document-template.line-items>
129 129
 
130 130
     <!-- Footer Notes -->
131
-    <x-company.document-template.footer class="classic-template-footer min-h-48 flex flex-col text-xs p-6">
131
+    <x-company.document-template.footer class="default-template-footer min-h-48 flex flex-col text-xs p-6">
132 132
         <div>
133 133
             <h4 class="font-semibold mb-2">Terms & Conditions</h4>
134 134
             <p class="break-words line-clamp-4">{{ $document->terms }}</p>

+ 251
- 0
resources/views/filament/forms/components/custom-table-repeater.blade.php Wyświetl plik

@@ -0,0 +1,251 @@
1
+@php
2
+    use Filament\Forms\Components\Actions\Action;
3
+    use Filament\Support\Enums\Alignment;
4
+    use Filament\Support\Enums\MaxWidth;
5
+
6
+    $containers = $getChildComponentContainers();
7
+
8
+    $addAction = $getAction($getAddActionName());
9
+    $cloneAction = $getAction($getCloneActionName());
10
+    $deleteAction = $getAction($getDeleteActionName());
11
+    $moveDownAction = $getAction($getMoveDownActionName());
12
+    $moveUpAction = $getAction($getMoveUpActionName());
13
+    $reorderAction = $getAction($getReorderActionName());
14
+    $isReorderableWithButtons = $isReorderableWithButtons();
15
+    $extraItemActions = $getExtraItemActions();
16
+    $extraActions = $getExtraActions();
17
+    $visibleExtraItemActions = [];
18
+    $visibleExtraActions = [];
19
+
20
+    $headers = $getHeaders();
21
+    $renderHeader = $shouldRenderHeader();
22
+    $stackAt = $getStackAt();
23
+    $hasContainers = count($containers) > 0;
24
+    $emptyLabel = $getEmptyLabel();
25
+    $streamlined = $isStreamlined();
26
+
27
+    $reorderAtStart = $isReorderAtStart();
28
+
29
+    $statePath = $getStatePath();
30
+
31
+    foreach ($extraActions as $extraAction) {
32
+        $visibleExtraActions = array_filter(
33
+            $extraActions,
34
+            fn (Action $action): bool => $action->isVisible(),
35
+        );
36
+    }
37
+
38
+    foreach ($extraItemActions as $extraItemAction) {
39
+        $visibleExtraItemActions = array_filter(
40
+            $extraItemActions,
41
+            fn (Action $action): bool => $action->isVisible(),
42
+        );
43
+    }
44
+
45
+    $hasActions = $reorderAction->isVisible()
46
+        || $cloneAction->isVisible()
47
+        || $deleteAction->isVisible()
48
+        || $moveUpAction->isVisible()
49
+        || $moveDownAction->isVisible()
50
+        || filled($visibleExtraItemActions);
51
+@endphp
52
+
53
+<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
54
+    <div
55
+        x-data="{}"
56
+        {{ $attributes->merge($getExtraAttributes())->class([
57
+            'table-repeater-component space-y-6 relative',
58
+            'streamlined' => $streamlined,
59
+            match ($stackAt) {
60
+                'sm', MaxWidth::Small => 'break-point-sm',
61
+                'lg', MaxWidth::Large => 'break-point-lg',
62
+                'xl', MaxWidth::ExtraLarge => 'break-point-xl',
63
+                '2xl', MaxWidth::TwoExtraLarge => 'break-point-2xl',
64
+                default => 'break-point-md',
65
+            }
66
+        ]) }}
67
+    >
68
+        @if (count($containers) || $emptyLabel !== false)
69
+            <div class="table-repeater-container rounded-sm relative ring-1 ring-gray-950/5 dark:ring-white/20">
70
+                <table class="w-full">
71
+                    <thead @class([
72
+                        'table-repeater-header-hidden sr-only' => ! $renderHeader,
73
+                        'table-repeater-header overflow-hidden border-b border-gray-950/5 dark:border-white/20' => $renderHeader,
74
+                    ])>
75
+                    <tr class="text-xs md:divide-x rtl:divide-x-reverse md:divide-gray-950/5 dark:md:divide-white/20">
76
+                        {{-- Move actions column to start if reorderAtStart is true --}}
77
+                        @if ($hasActions && count($containers) && $reorderAtStart)
78
+                            <th class="table-repeater-header-column w-px first:rounded-tl-sm rtl:first:rounded-tr-sm rtl:first:rounded-tl-none p-2 bg-gray-100 dark:bg-gray-900/60">
79
+                                <span class="sr-only">
80
+                                    {{ trans('table-repeater::components.repeater.row_actions.label') }}
81
+                                </span>
82
+                            </th>
83
+                        @endif
84
+
85
+                        @foreach ($headers as $key => $header)
86
+                            <th
87
+                                @class([
88
+                                    'table-repeater-header-column p-2 font-medium first:rounded-tl-sm rtl:first:rounded-tr-sm rtl:first:rounded-tl-none last:rounded-tr-sm bg-gray-100 dark:text-gray-300 dark:bg-gray-900/60',
89
+                                    match($header->getAlignment()) {
90
+                                      'center', Alignment::Center => 'text-center',
91
+                                      'right', 'end', Alignment::Right, Alignment::End => 'text-end',
92
+                                      default => 'text-start'
93
+                                    }
94
+                                ])
95
+                                style="width: {{ $header->getWidth() }}"
96
+                            >
97
+                                {{ $header->getLabel() }}
98
+                                @if ($header->isRequired())
99
+                                    <span class="whitespace-nowrap">
100
+                                        <sup class="font-medium text-danger-700 dark:text-danger-400">*</sup>
101
+                                    </span>
102
+                                @endif
103
+                            </th>
104
+                        @endforeach
105
+
106
+                        @if ($hasActions && count($containers))
107
+                            <th class="table-repeater-header-column w-px last:rounded-tr-sm rtl:last:rounded-tr-none rtl:last:rounded-tl-sm p-2 bg-gray-100 dark:bg-gray-900/60">
108
+                                <span class="sr-only">
109
+                                    {{ trans('table-repeater::components.repeater.row_actions.label') }}
110
+                                </span>
111
+                            </th>
112
+                        @endif
113
+                    </tr>
114
+                    </thead>
115
+                    <tbody
116
+                        x-sortable
117
+                        wire:end.stop="{{ 'mountFormComponentAction(\'' . $statePath . '\', \'reorder\', { items: $event.target.sortable.toArray() })' }}"
118
+                        class="table-repeater-rows-wrapper divide-y divide-gray-950/5 dark:divide-white/20"
119
+                    >
120
+                    @if (count($containers))
121
+                        @foreach ($containers as $uuid => $row)
122
+                            @php
123
+                                $visibleExtraItemActions = array_filter(
124
+                                    $extraItemActions,
125
+                                    fn (Action $action): bool => $action(['item' => $uuid])->isVisible(),
126
+                                );
127
+                            @endphp
128
+                            <tr
129
+                                wire:key="{{ $this->getId() }}.{{ $row->getStatePath() }}.{{ $field::class }}.item"
130
+                                x-sortable-item="{{ $uuid }}"
131
+                                class="table-repeater-row"
132
+                            >
133
+                                {{-- Add reorder action column at start if reorderAtStart is true --}}
134
+                                @if ($hasActions && $reorderAtStart && $reorderAction->isVisible())
135
+                                    <td class="table-repeater-column p-2 w-px align-top">
136
+                                        <ul class="flex items-center table-repeater-row-actions gap-x-3 px-2">
137
+                                            <li x-sortable-handle class="shrink-0">
138
+                                                {{ $reorderAction }}
139
+                                            </li>
140
+                                        </ul>
141
+                                    </td>
142
+                                @endif
143
+
144
+                                @php($counter = 0)
145
+                                @foreach($row->getComponents() as $cell)
146
+                                    @if($cell instanceof \Filament\Forms\Components\Hidden || $cell->isHidden())
147
+                                        {{ $cell }}
148
+                                    @else
149
+                                        <td
150
+                                            @class([
151
+                                                'table-repeater-column align-top',
152
+                                                'p-2' => ! $streamlined,
153
+                                                'has-hidden-label' => $cell->isLabelHidden(),
154
+                                                match($headers[$counter++]->getAlignment()) {
155
+                                                  'center', Alignment::Center => 'text-center',
156
+                                                  'right', 'end', Alignment::Right, Alignment::End => 'text-end',
157
+                                                  default => 'text-start'
158
+                                                }
159
+                                            ])
160
+                                            style="width: {{ $cell->getMaxWidth() ?? 'auto' }}"
161
+                                        >
162
+                                            {{ $cell }}
163
+                                        </td>
164
+                                    @endif
165
+                                @endforeach
166
+
167
+                                @if ($hasActions)
168
+                                    <td class="table-repeater-column p-2 w-px align-top">
169
+                                        <ul class="flex items-center table-repeater-row-actions gap-x-3 px-2">
170
+                                            @foreach ($visibleExtraItemActions as $extraItemAction)
171
+                                                <li>
172
+                                                    {{ $extraItemAction(['item' => $uuid]) }}
173
+                                                </li>
174
+                                            @endforeach
175
+
176
+                                            @if ($reorderAction->isVisible() && ! $reorderAtStart)
177
+                                                <li x-sortable-handle class="shrink-0">
178
+                                                    {{ $reorderAction }}
179
+                                                </li>
180
+                                            @endif
181
+
182
+                                            @if ($isReorderableWithButtons)
183
+                                                @if (! $loop->first)
184
+                                                    <li>
185
+                                                        {{ $moveUpAction(['item' => $uuid]) }}
186
+                                                    </li>
187
+                                                @endif
188
+
189
+                                                @if (! $loop->last)
190
+                                                    <li>
191
+                                                        {{ $moveDownAction(['item' => $uuid]) }}
192
+                                                    </li>
193
+                                                @endif
194
+                                            @endif
195
+
196
+                                            @if ($cloneAction->isVisible())
197
+                                                <li>
198
+                                                    {{ $cloneAction(['item' => $uuid]) }}
199
+                                                </li>
200
+                                            @endif
201
+
202
+                                            @if ($deleteAction->isVisible())
203
+                                                <li>
204
+                                                    {{ $deleteAction(['item' => $uuid]) }}
205
+                                                </li>
206
+                                            @endif
207
+                                        </ul>
208
+                                    </td>
209
+                                @endif
210
+                            </tr>
211
+                        @endforeach
212
+                    @else
213
+                        <tr class="table-repeater-row table-repeater-empty-row">
214
+                            <td colspan="{{ count($headers) + intval($hasActions) }}"
215
+                                class="table-repeater-column table-repeater-empty-column p-4 w-px text-center italic">
216
+                                {{ $emptyLabel ?: trans('table-repeater::components.repeater.empty.label') }}
217
+                            </td>
218
+                        </tr>
219
+                    @endif
220
+                    </tbody>
221
+                </table>
222
+            </div>
223
+        @endif
224
+
225
+        @if ($addAction->isVisible() || filled($visibleExtraActions))
226
+            <ul
227
+                @class([
228
+                    'relative flex gap-4',
229
+                    match ($getAddActionAlignment()) {
230
+                        Alignment::Start, Alignment::Left => 'justify-start',
231
+                        Alignment::End, Alignment::Right => 'justify-end',
232
+                        default =>  'justify-center',
233
+                    },
234
+                ])
235
+            >
236
+                @if ($addAction->isVisible())
237
+                    <li>
238
+                        {{ $addAction }}
239
+                    </li>
240
+                @endif
241
+                @if (filled($visibleExtraActions))
242
+                    @foreach ($visibleExtraActions as $extraAction)
243
+                        <li>
244
+                            {{ ($extraAction) }}
245
+                        </li>
246
+                    @endforeach
247
+                @endif
248
+            </ul>
249
+        @endif
250
+    </div>
251
+</x-dynamic-component>

+ 7
- 5
resources/views/filament/infolists/components/document-templates/classic.blade.php Wyświetl plik

@@ -107,10 +107,12 @@
107 107
             <div class="w-[40%]">
108 108
                 <table class="w-full table-fixed whitespace-nowrap">
109 109
                     <tbody class="text-sm">
110
-                    <tr>
111
-                        <td class="text-right font-semibold py-2">Subtotal:</td>
112
-                        <td class="text-right py-2">{{ $document->subtotal }}</td>
113
-                    </tr>
110
+                    @if($document->subtotal)
111
+                        <tr>
112
+                            <td class="text-right font-semibold py-2">Subtotal:</td>
113
+                            <td class="text-right py-2">{{ $document->subtotal }}</td>
114
+                        </tr>
115
+                    @endif
114 116
                     @if($document->discount)
115 117
                         <tr class="text-success-800 dark:text-success-600">
116 118
                             <td class="text-right py-2">Discount:</td>
@@ -144,7 +146,7 @@
144 146
     </x-company.document-template.line-items>
145 147
 
146 148
     <!-- Footer -->
147
-    <x-company.document-template.footer class="classic-template-footer min-h-48 p-6 text-sm">
149
+    <x-company.document-template.footer class="default-template-footer min-h-48 p-6 text-sm">
148 150
         <h4 class="font-semibold mb-2">Terms & Conditions</h4>
149 151
         <p class="break-words line-clamp-4">{{ $document->terms }}</p>
150 152
     </x-company.document-template.footer>

+ 7
- 5
resources/views/filament/infolists/components/document-templates/default.blade.php Wyświetl plik

@@ -90,11 +90,13 @@
90 90
             @endforeach
91 91
             </tbody>
92 92
             <tfoot class="text-sm summary-section">
93
-            <tr>
94
-                <td class="pl-6 py-2" colspan="2"></td>
95
-                <td class="text-right font-semibold py-2">Subtotal:</td>
96
-                <td class="text-right pr-6 py-2">{{ $document->subtotal }}</td>
97
-            </tr>
93
+            @if($document->subtotal)
94
+                <tr>
95
+                    <td class="pl-6 py-2" colspan="2"></td>
96
+                    <td class="text-right font-semibold py-2">Subtotal:</td>
97
+                    <td class="text-right pr-6 py-2">{{ $document->subtotal }}</td>
98
+                </tr>
99
+            @endif
98 100
             @if($document->discount)
99 101
                 <tr class="text-success-800 dark:text-success-600">
100 102
                     <td class="pl-6 py-2" colspan="2"></td>

+ 7
- 5
resources/views/filament/infolists/components/document-templates/modern.blade.php Wyświetl plik

@@ -92,11 +92,13 @@
92 92
             @endforeach
93 93
             </tbody>
94 94
             <tfoot class="text-sm summary-section">
95
-            <tr>
96
-                <td class="pl-6 py-2" colspan="2"></td>
97
-                <td class="text-right font-semibold py-2">Subtotal:</td>
98
-                <td class="text-right pr-6 py-2">{{ $document->subtotal }}</td>
99
-            </tr>
95
+            @if($document->subtotal)
96
+                <tr>
97
+                    <td class="pl-6 py-2" colspan="2"></td>
98
+                    <td class="text-right font-semibold py-2">Subtotal:</td>
99
+                    <td class="text-right pr-6 py-2">{{ $document->subtotal }}</td>
100
+                </tr>
101
+            @endif
100 102
             @if($document->discount)
101 103
                 <tr class="text-success-800 dark:text-success-600">
102 104
                     <td class="pl-6 py-2" colspan="2"></td>

Ładowanie…
Anuluj
Zapisz