Bläddra i källkod

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

Development 3.x
3.x
Andrew Wallo 7 månader sedan
förälder
incheckning
c1dbffdb41
Inget konto är kopplat till bidragsgivarens mejladress
60 ändrade filer med 2121 tillägg och 1821 borttagningar
  1. 4
    2
      app/DTO/DocumentDTO.php
  2. 3
    6
      app/DTO/DocumentPreviewDTO.php
  3. 14
    4
      app/Enums/Accounting/DocumentType.php
  4. 2
    10
      app/Enums/Setting/DocumentType.php
  5. 0
    187
      app/Filament/Company/Clusters/Settings/Pages/Appearance.php
  6. 0
    350
      app/Filament/Company/Clusters/Settings/Pages/Invoice.php
  7. 282
    0
      app/Filament/Company/Clusters/Settings/Resources/DocumentDefaultResource.php
  8. 26
    0
      app/Filament/Company/Clusters/Settings/Resources/DocumentDefaultResource/Pages/EditDocumentDefault.php
  9. 17
    0
      app/Filament/Company/Clusters/Settings/Resources/DocumentDefaultResource/Pages/ListDocumentDefaults.php
  10. 1
    1
      app/Filament/Company/Resources/Purchases/BillResource.php
  11. 27
    62
      app/Filament/Company/Resources/Sales/EstimateResource.php
  12. 27
    63
      app/Filament/Company/Resources/Sales/InvoiceResource.php
  13. 25
    62
      app/Filament/Company/Resources/Sales/RecurringInvoiceResource.php
  14. 38
    0
      app/Filament/Forms/Components/DocumentFooterSection.php
  15. 97
    0
      app/Filament/Forms/Components/DocumentHeaderSection.php
  16. 17
    0
      app/Filament/Infolists/Components/DocumentPreview.php
  17. 9
    1
      app/Http/Middleware/Authenticate.php
  18. 24
    0
      app/Http/Responses/LoginRedirectResponse.php
  19. 0
    15
      app/Http/Responses/LoginResponse.php
  20. 0
    7
      app/Listeners/ConfigureCompanyDefault.php
  21. 13
    2
      app/Listeners/CreateEmployeeContact.php
  22. 7
    9
      app/Models/Accounting/Bill.php
  23. 1
    1
      app/Models/Accounting/Document.php
  24. 8
    10
      app/Models/Accounting/Estimate.php
  25. 4
    7
      app/Models/Accounting/Invoice.php
  26. 1
    1
      app/Models/Accounting/RecurringInvoice.php
  27. 18
    7
      app/Models/Company.php
  28. 0
    39
      app/Models/Setting/Appearance.php
  29. 15
    30
      app/Models/Setting/DocumentDefault.php
  30. 7
    0
      app/Models/User.php
  31. 4
    2
      app/Providers/AppServiceProvider.php
  32. 1
    0
      app/Providers/Filament/CompanyPanelProvider.php
  33. 12
    1
      app/Providers/Filament/UserPanelProvider.php
  34. 1
    17
      app/Services/CompanySettingsService.php
  35. 0
    1
      bootstrap/providers.php
  36. 423
    422
      composer.lock
  37. 6
    5
      database/factories/Accounting/BillFactory.php
  38. 6
    5
      database/factories/Accounting/EstimateFactory.php
  39. 6
    5
      database/factories/Accounting/InvoiceFactory.php
  40. 1
    1
      database/factories/Accounting/RecurringInvoiceFactory.php
  41. 0
    24
      database/factories/Setting/AppearanceFactory.php
  42. 4
    9
      database/factories/Setting/CompanyDefaultFactory.php
  43. 31
    9
      database/factories/Setting/DocumentDefaultFactory.php
  44. 0
    32
      database/migrations/2023_09_12_014413_create_appearances_table.php
  45. 3
    5
      database/migrations/2023_09_12_032057_create_document_defaults_table.php
  46. 389
    219
      package-lock.json
  47. 4
    2
      resources/data/lang/en.json
  48. 10
    0
      resources/js/history-fix.js
  49. 0
    0
      resources/js/top-navigation.js
  50. 4
    1
      resources/views/components/company/invoice/container.blade.php
  51. 19
    0
      resources/views/components/icons/document-header-decoration.blade.php
  52. 5
    10
      resources/views/filament/company/components/invoice-layouts/classic.blade.php
  53. 0
    17
      resources/views/filament/company/pages/setting/appearance.blade.php
  54. 0
    10
      resources/views/filament/company/pages/setting/invoice.blade.php
  55. 5
    145
      resources/views/filament/infolists/components/document-preview.blade.php
  56. 148
    0
      resources/views/filament/infolists/components/document-templates/classic.blade.php
  57. 138
    0
      resources/views/filament/infolists/components/document-templates/default.blade.php
  58. 145
    0
      resources/views/filament/infolists/components/document-templates/modern.blade.php
  59. 3
    3
      resources/views/welcome.blade.php
  60. 66
    0
      tests/Feature/Setting/LocalizationTest.php

+ 4
- 2
app/DTO/DocumentDTO.php Visa fil

@@ -44,7 +44,9 @@ readonly class DocumentDTO
44 44
     public static function fromModel(Document $document): self
45 45
     {
46 46
         /** @var DocumentDefault $settings */
47
-        $settings = $document->company->defaultInvoice;
47
+        $settings = $document->company->documentDefaults()
48
+            ->type($document::documentType())
49
+            ->first() ?? $document->company->defaultInvoice;
48 50
 
49 51
         $currencyCode = $document->currency_code ?? CurrencyAccessor::getDefaultCurrency();
50 52
 
@@ -67,7 +69,7 @@ readonly class DocumentDTO
67 69
             company: CompanyDTO::fromModel($document->company),
68 70
             client: ClientDTO::fromModel($document->client),
69 71
             lineItems: $document->lineItems->map(fn ($item) => LineItemDTO::fromModel($item)),
70
-            label: $document->documentType()->getLabels(),
72
+            label: $document::documentType()->getLabels(),
71 73
             columnLabel: DocumentColumnLabelDTO::fromModel($settings),
72 74
             accentColor: $settings->accent_color ?? '#000000',
73 75
             showLogo: $settings->show_logo ?? false,

+ 3
- 6
app/DTO/DocumentPreviewDTO.php Visa fil

@@ -2,7 +2,6 @@
2 2
 
3 3
 namespace App\DTO;
4 4
 
5
-use App\Enums\Accounting\DocumentType;
6 5
 use App\Enums\Setting\Font;
7 6
 use App\Enums\Setting\PaymentTerms;
8 7
 use App\Models\Setting\DocumentDefault;
@@ -23,7 +22,7 @@ readonly class DocumentPreviewDTO extends DocumentDTO
23 22
             terms: $data['terms'] ?? $settings->terms,
24 23
             logo: $settings->logo_url,
25 24
             number: self::generatePreviewNumber($settings, $data),
26
-            referenceNumber: 'ORD-00001',
25
+            referenceNumber: $settings->getNumberNext('ORD-'),
27 26
             date: $company->locale->date_format->getLabel(),
28 27
             dueDate: $paymentTerms->getDueDate($company->locale->date_format->value),
29 28
             currencyCode: CurrencyAccessor::getDefaultCurrency(),
@@ -35,7 +34,7 @@ readonly class DocumentPreviewDTO extends DocumentDTO
35 34
             company: CompanyDTO::fromModel($company),
36 35
             client: ClientPreviewDTO::fake(),
37 36
             lineItems: LineItemPreviewDTO::fakeItems(),
38
-            label: DocumentType::Invoice->getLabels(),
37
+            label: $settings->type->getLabels(),
39 38
             columnLabel: self::generateColumnLabels($settings, $data),
40 39
             accentColor: $data['accent_color'] ?? $settings->accent_color ?? '#000000',
41 40
             showLogo: $data['show_logo'] ?? $settings->show_logo ?? true,
@@ -46,10 +45,8 @@ readonly class DocumentPreviewDTO extends DocumentDTO
46 45
     protected static function generatePreviewNumber(DocumentDefault $settings, ?array $data): string
47 46
     {
48 47
         $prefix = $data['number_prefix'] ?? $settings->number_prefix ?? 'INV-';
49
-        $digits = $data['number_digits'] ?? $settings->number_digits ?? 5;
50
-        $next = $data['number_next'] ?? $settings->number_next;
51 48
 
52
-        return $settings->getNumberNext(padded: true, format: true, prefix: $prefix, digits: $digits, next: $next);
49
+        return $settings->getNumberNext($prefix);
53 50
     }
54 51
 
55 52
     protected static function generateColumnLabels(DocumentDefault $settings, ?array $data): DocumentColumnLabelDTO

+ 14
- 4
app/Enums/Accounting/DocumentType.php Visa fil

@@ -58,7 +58,7 @@ enum DocumentType: string implements HasIcon, HasLabel
58 58
     {
59 59
         return match ($this) {
60 60
             self::Invoice => new DocumentLabelDTO(
61
-                title: 'Invoice',
61
+                title: self::Invoice->getLabel(),
62 62
                 number: 'Invoice Number',
63 63
                 referenceNumber: 'P.O/S.O Number',
64 64
                 date: 'Invoice Date',
@@ -66,7 +66,7 @@ enum DocumentType: string implements HasIcon, HasLabel
66 66
                 amountDue: 'Amount Due',
67 67
             ),
68 68
             self::RecurringInvoice => new DocumentLabelDTO(
69
-                title: 'Recurring Invoice',
69
+                title: self::RecurringInvoice->getLabel(),
70 70
                 number: 'Invoice Number',
71 71
                 referenceNumber: 'P.O/S.O Number',
72 72
                 date: 'Invoice Date',
@@ -74,7 +74,7 @@ enum DocumentType: string implements HasIcon, HasLabel
74 74
                 amountDue: 'Amount Due',
75 75
             ),
76 76
             self::Estimate => new DocumentLabelDTO(
77
-                title: 'Estimate',
77
+                title: self::Estimate->getLabel(),
78 78
                 number: 'Estimate Number',
79 79
                 referenceNumber: 'Reference Number',
80 80
                 date: 'Estimate Date',
@@ -82,7 +82,7 @@ enum DocumentType: string implements HasIcon, HasLabel
82 82
                 amountDue: 'Grand Total',
83 83
             ),
84 84
             self::Bill => new DocumentLabelDTO(
85
-                title: 'Bill',
85
+                title: self::Bill->getLabel(),
86 86
                 number: 'Bill Number',
87 87
                 referenceNumber: 'P.O/S.O Number',
88 88
                 date: 'Bill Date',
@@ -91,4 +91,14 @@ enum DocumentType: string implements HasIcon, HasLabel
91 91
             ),
92 92
         };
93 93
     }
94
+
95
+    public function getDefaultPrefix(): ?string
96
+    {
97
+        return match ($this) {
98
+            self::Invoice => 'INV-',
99
+            self::Estimate => 'EST-',
100
+            self::Bill => 'BILL-',
101
+            default => null,
102
+        };
103
+    }
94 104
 }

+ 2
- 10
app/Enums/Setting/DocumentType.php Visa fil

@@ -2,13 +2,13 @@
2 2
 
3 3
 namespace App\Enums\Setting;
4 4
 
5
-use Filament\Support\Contracts\HasIcon;
6 5
 use Filament\Support\Contracts\HasLabel;
7 6
 
8
-enum DocumentType: string implements HasIcon, HasLabel
7
+enum DocumentType: string implements HasLabel
9 8
 {
10 9
     case Invoice = 'invoice';
11 10
     case Bill = 'bill';
11
+    case Estimate = 'estimate';
12 12
 
13 13
     public const DEFAULT = self::Invoice->value;
14 14
 
@@ -16,12 +16,4 @@ enum DocumentType: string implements HasIcon, HasLabel
16 16
     {
17 17
         return $this->name;
18 18
     }
19
-
20
-    public function getIcon(): ?string
21
-    {
22
-        return match ($this->value) {
23
-            self::Invoice->value => 'heroicon-o-document-duplicate',
24
-            self::Bill->value => 'heroicon-o-clipboard-document-list',
25
-        };
26
-    }
27 19
 }

+ 0
- 187
app/Filament/Company/Clusters/Settings/Pages/Appearance.php Visa fil

@@ -1,187 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Clusters\Settings\Pages;
4
-
5
-use App\Enums\Setting\Font;
6
-use App\Enums\Setting\PrimaryColor;
7
-use App\Filament\Company\Clusters\Settings;
8
-use App\Models\Setting\Appearance as AppearanceModel;
9
-use App\Services\CompanySettingsService;
10
-use Filament\Actions\Action;
11
-use Filament\Actions\ActionGroup;
12
-use Filament\Forms\Components\Component;
13
-use Filament\Forms\Components\Section;
14
-use Filament\Forms\Components\Select;
15
-use Filament\Forms\Form;
16
-use Filament\Notifications\Notification;
17
-use Filament\Pages\Concerns\InteractsWithFormActions;
18
-use Filament\Pages\Page;
19
-use Filament\Support\Enums\MaxWidth;
20
-use Filament\Support\Exceptions\Halt;
21
-use Illuminate\Auth\Access\AuthorizationException;
22
-use Illuminate\Contracts\Support\Htmlable;
23
-use Illuminate\Database\Eloquent\Model;
24
-use Livewire\Attributes\Locked;
25
-
26
-use function Filament\authorize;
27
-
28
-/**
29
- * @property Form $form
30
- */
31
-class Appearance extends Page
32
-{
33
-    use InteractsWithFormActions;
34
-
35
-    protected static ?string $title = 'Appearance';
36
-
37
-    protected static string $view = 'filament.company.pages.setting.appearance';
38
-
39
-    protected static ?string $cluster = Settings::class;
40
-
41
-    public ?array $data = [];
42
-
43
-    #[Locked]
44
-    public ?AppearanceModel $record = null;
45
-
46
-    public function getTitle(): string | Htmlable
47
-    {
48
-        return translate(static::$title);
49
-    }
50
-
51
-    public function getMaxContentWidth(): MaxWidth | string | null
52
-    {
53
-        return MaxWidth::ScreenTwoExtraLarge;
54
-    }
55
-
56
-    public static function getNavigationLabel(): string
57
-    {
58
-        return translate(static::$title);
59
-    }
60
-
61
-    public function mount(): void
62
-    {
63
-        $this->record = AppearanceModel::firstOrNew([
64
-            'company_id' => auth()->user()->currentCompany->id,
65
-        ]);
66
-
67
-        abort_unless(static::canView($this->record), 404);
68
-
69
-        $this->fillForm();
70
-    }
71
-
72
-    public function fillForm(): void
73
-    {
74
-        $data = $this->record->attributesToArray();
75
-
76
-        $this->form->fill($data);
77
-    }
78
-
79
-    public function save(): void
80
-    {
81
-        try {
82
-            $data = $this->form->getState();
83
-
84
-            $this->handleRecordUpdate($this->record, $data);
85
-
86
-        } catch (Halt $exception) {
87
-            return;
88
-        }
89
-
90
-        $this->getSavedNotification()->send();
91
-    }
92
-
93
-    protected function getSavedNotification(): Notification
94
-    {
95
-        return Notification::make()
96
-            ->success()
97
-            ->title(__('filament-panels::resources/pages/edit-record.notifications.saved.title'));
98
-    }
99
-
100
-    public function form(Form $form): Form
101
-    {
102
-        return $form
103
-            ->schema([
104
-                $this->getGeneralSection(),
105
-            ])
106
-            ->model($this->record)
107
-            ->statePath('data')
108
-            ->operation('edit');
109
-    }
110
-
111
-    protected function getGeneralSection(): Component
112
-    {
113
-        return Section::make('General')
114
-            ->schema([
115
-                Select::make('primary_color')
116
-                    ->allowHtml()
117
-                    ->softRequired()
118
-                    ->localizeLabel()
119
-                    ->options(
120
-                        collect(PrimaryColor::cases())
121
-                            ->sort(static fn ($a, $b) => $a->value <=> $b->value)
122
-                            ->mapWithKeys(static fn ($case) => [
123
-                                $case->value => "<span class='flex items-center gap-x-4'>
124
-                                <span class='rounded-full w-4 h-4' style='background:rgb(" . $case->getColor()[600] . ")'></span>
125
-                                <span>" . $case->getLabel() . '</span>
126
-                                </span>',
127
-                            ]),
128
-                    ),
129
-                Select::make('font')
130
-                    ->allowHtml()
131
-                    ->softRequired()
132
-                    ->localizeLabel()
133
-                    ->options(
134
-                        collect(Font::cases())
135
-                            ->mapWithKeys(static fn ($case) => [
136
-                                $case->value => "<span style='font-family:{$case->getLabel()}'>{$case->getLabel()}</span>",
137
-                            ]),
138
-                    ),
139
-            ])->columns();
140
-    }
141
-
142
-    protected function handleRecordUpdate(AppearanceModel $record, array $data): AppearanceModel
143
-    {
144
-        $record->fill($data);
145
-
146
-        $keysToWatch = [
147
-            'primary_color',
148
-            'font',
149
-        ];
150
-
151
-        if ($record->isDirty($keysToWatch)) {
152
-            CompanySettingsService::invalidateSettings($record->company_id);
153
-            $this->dispatch('appearanceUpdated');
154
-        }
155
-
156
-        $record->save();
157
-
158
-        return $record;
159
-    }
160
-
161
-    /**
162
-     * @return array<Action | ActionGroup>
163
-     */
164
-    protected function getFormActions(): array
165
-    {
166
-        return [
167
-            $this->getSaveFormAction(),
168
-        ];
169
-    }
170
-
171
-    protected function getSaveFormAction(): Action
172
-    {
173
-        return Action::make('save')
174
-            ->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
175
-            ->submit('save')
176
-            ->keyBindings(['mod+s']);
177
-    }
178
-
179
-    public static function canView(Model $record): bool
180
-    {
181
-        try {
182
-            return authorize('update', $record)->allowed();
183
-        } catch (AuthorizationException $exception) {
184
-            return $exception->toResponse()->allowed();
185
-        }
186
-    }
187
-}

+ 0
- 350
app/Filament/Company/Clusters/Settings/Pages/Invoice.php Visa fil

@@ -1,350 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Clusters\Settings\Pages;
4
-
5
-use App\Enums\Setting\DocumentType;
6
-use App\Enums\Setting\Font;
7
-use App\Enums\Setting\PaymentTerms;
8
-use App\Enums\Setting\Template;
9
-use App\Filament\Company\Clusters\Settings;
10
-use App\Models\Setting\DocumentDefault as InvoiceModel;
11
-use Filament\Actions\Action;
12
-use Filament\Actions\ActionGroup;
13
-use Filament\Forms\Components\Checkbox;
14
-use Filament\Forms\Components\ColorPicker;
15
-use Filament\Forms\Components\Component;
16
-use Filament\Forms\Components\FileUpload;
17
-use Filament\Forms\Components\Grid;
18
-use Filament\Forms\Components\Section;
19
-use Filament\Forms\Components\Select;
20
-use Filament\Forms\Components\Textarea;
21
-use Filament\Forms\Components\TextInput;
22
-use Filament\Forms\Components\ViewField;
23
-use Filament\Forms\Form;
24
-use Filament\Forms\Get;
25
-use Filament\Forms\Set;
26
-use Filament\Notifications\Notification;
27
-use Filament\Pages\Concerns\InteractsWithFormActions;
28
-use Filament\Pages\Page;
29
-use Filament\Support\Enums\MaxWidth;
30
-use Filament\Support\Exceptions\Halt;
31
-use Illuminate\Auth\Access\AuthorizationException;
32
-use Illuminate\Contracts\Support\Htmlable;
33
-use Illuminate\Database\Eloquent\Model;
34
-use Illuminate\Support\Facades\Auth;
35
-use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
36
-
37
-use function Filament\authorize;
38
-
39
-/**
40
- * @property Form $form
41
- */
42
-class Invoice extends Page
43
-{
44
-    use InteractsWithFormActions;
45
-
46
-    protected static ?string $title = 'Invoice';
47
-
48
-    protected static string $view = 'filament.company.pages.setting.invoice';
49
-
50
-    protected static ?string $cluster = Settings::class;
51
-
52
-    public ?array $data = [];
53
-
54
-    public ?InvoiceModel $record = null;
55
-
56
-    public function getTitle(): string | Htmlable
57
-    {
58
-        return translate(static::$title);
59
-    }
60
-
61
-    public static function getNavigationLabel(): string
62
-    {
63
-        return translate(static::$title);
64
-    }
65
-
66
-    public function getMaxContentWidth(): MaxWidth | string | null
67
-    {
68
-        return MaxWidth::ScreenTwoExtraLarge;
69
-    }
70
-
71
-    public function mount(): void
72
-    {
73
-        $this->record = InvoiceModel::invoice()
74
-            ->firstOrNew([
75
-                'company_id' => auth()->user()->currentCompany->id,
76
-                'type' => DocumentType::Invoice->value,
77
-            ]);
78
-
79
-        abort_unless(static::canView($this->record), 404);
80
-
81
-        $this->fillForm();
82
-    }
83
-
84
-    public function fillForm(): void
85
-    {
86
-        $data = $this->record->attributesToArray();
87
-
88
-        $this->form->fill($data);
89
-    }
90
-
91
-    public function save(): void
92
-    {
93
-        try {
94
-            $data = $this->form->getState();
95
-
96
-            $this->handleRecordUpdate($this->record, $data);
97
-
98
-        } catch (Halt $exception) {
99
-            return;
100
-        }
101
-
102
-        $this->getSavedNotification()->send();
103
-    }
104
-
105
-    protected function getSavedNotification(): Notification
106
-    {
107
-        return Notification::make()
108
-            ->success()
109
-            ->title(__('filament-panels::resources/pages/edit-record.notifications.saved.title'));
110
-    }
111
-
112
-    public function form(Form $form): Form
113
-    {
114
-        return $form
115
-            ->live()
116
-            ->schema([
117
-                $this->getGeneralSection(),
118
-                $this->getContentSection(),
119
-                $this->getTemplateSection(),
120
-            ])
121
-            ->model($this->record)
122
-            ->statePath('data')
123
-            ->operation('edit');
124
-    }
125
-
126
-    protected function getGeneralSection(): Component
127
-    {
128
-        return Section::make('General')
129
-            ->schema([
130
-                TextInput::make('number_prefix')
131
-                    ->localizeLabel()
132
-                    ->nullable(),
133
-                Select::make('number_digits')
134
-                    ->softRequired()
135
-                    ->localizeLabel()
136
-                    ->options(InvoiceModel::availableNumberDigits()),
137
-                TextInput::make('number_next')
138
-                    ->softRequired()
139
-                    ->localizeLabel()
140
-                    ->mask(static function (Get $get) {
141
-                        return str_repeat('9', $get('number_digits'));
142
-                    })
143
-                    ->hint(function (Get $get, $state) {
144
-                        $number_prefix = $get('number_prefix');
145
-                        $number_digits = $get('number_digits');
146
-                        $number_next = $state;
147
-
148
-                        return $this->record->getNumberNext(true, true, $number_prefix, $number_digits, $number_next);
149
-                    }),
150
-                Select::make('payment_terms')
151
-                    ->softRequired()
152
-                    ->localizeLabel()
153
-                    ->options(PaymentTerms::class),
154
-            ])->columns();
155
-    }
156
-
157
-    protected function getContentSection(): Component
158
-    {
159
-        return Section::make('Content')
160
-            ->schema([
161
-                TextInput::make('header')
162
-                    ->localizeLabel()
163
-                    ->nullable(),
164
-                TextInput::make('subheader')
165
-                    ->localizeLabel()
166
-                    ->nullable(),
167
-                Textarea::make('terms')
168
-                    ->localizeLabel()
169
-                    ->nullable(),
170
-                Textarea::make('footer')
171
-                    ->localizeLabel('Footer')
172
-                    ->nullable(),
173
-            ])->columns();
174
-    }
175
-
176
-    protected function getTemplateSection(): Component
177
-    {
178
-        return Section::make('Template')
179
-            ->description('Choose the template and edit the column names.')
180
-            ->schema([
181
-                Grid::make(1)
182
-                    ->schema([
183
-                        FileUpload::make('logo')
184
-                            ->openable()
185
-                            ->maxSize(1024)
186
-                            ->localizeLabel()
187
-                            ->visibility('public')
188
-                            ->disk('public')
189
-                            ->directory('logos/document')
190
-                            ->imageResizeMode('contain')
191
-                            ->imageCropAspectRatio('3:2')
192
-                            ->panelAspectRatio('3:2')
193
-                            ->panelLayout('integrated')
194
-                            ->removeUploadedFileButtonPosition('center bottom')
195
-                            ->uploadButtonPosition('center bottom')
196
-                            ->uploadProgressIndicatorPosition('center bottom')
197
-                            ->getUploadedFileNameForStorageUsing(
198
-                                static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
199
-                                    ->prepend(Auth::user()->currentCompany->id . '_'),
200
-                            )
201
-                            ->extraAttributes([
202
-                                'class' => 'aspect-[3/2] w-[9.375rem] max-w-full',
203
-                            ])
204
-                            ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
205
-                        Checkbox::make('show_logo')
206
-                            ->localizeLabel(),
207
-                        ColorPicker::make('accent_color')
208
-                            ->localizeLabel(),
209
-                        Select::make('font')
210
-                            ->softRequired()
211
-                            ->localizeLabel()
212
-                            ->allowHtml()
213
-                            ->options(
214
-                                collect(Font::cases())
215
-                                    ->mapWithKeys(static fn ($case) => [
216
-                                        $case->value => "<span style='font-family:{$case->getLabel()}'>{$case->getLabel()}</span>",
217
-                                    ]),
218
-                            ),
219
-                        Select::make('template')
220
-                            ->softRequired()
221
-                            ->localizeLabel()
222
-                            ->options(Template::class),
223
-                        Select::make('item_name.option')
224
-                            ->softRequired()
225
-                            ->localizeLabel('Item name')
226
-                            ->options(InvoiceModel::getAvailableItemNameOptions())
227
-                            ->afterStateUpdated(static function (Get $get, Set $set, $state, $old) {
228
-                                if ($state !== 'other' && $old === 'other' && filled($get('item_name.custom'))) {
229
-                                    $set('item_name.old_custom', $get('item_name.custom'));
230
-                                    $set('item_name.custom', null);
231
-                                }
232
-
233
-                                if ($state === 'other' && $old !== 'other') {
234
-                                    $set('item_name.custom', $get('item_name.old_custom'));
235
-                                }
236
-                            }),
237
-                        TextInput::make('item_name.custom')
238
-                            ->hiddenLabel()
239
-                            ->disabled(static fn (callable $get) => $get('item_name.option') !== 'other')
240
-                            ->nullable(),
241
-                        Select::make('unit_name.option')
242
-                            ->softRequired()
243
-                            ->localizeLabel('Unit name')
244
-                            ->options(InvoiceModel::getAvailableUnitNameOptions())
245
-                            ->afterStateUpdated(static function (Get $get, Set $set, $state, $old) {
246
-                                if ($state !== 'other' && $old === 'other' && filled($get('unit_name.custom'))) {
247
-                                    $set('unit_name.old_custom', $get('unit_name.custom'));
248
-                                    $set('unit_name.custom', null);
249
-                                }
250
-
251
-                                if ($state === 'other' && $old !== 'other') {
252
-                                    $set('unit_name.custom', $get('unit_name.old_custom'));
253
-                                }
254
-                            }),
255
-                        TextInput::make('unit_name.custom')
256
-                            ->hiddenLabel()
257
-                            ->disabled(static fn (callable $get) => $get('unit_name.option') !== 'other')
258
-                            ->nullable(),
259
-                        Select::make('price_name.option')
260
-                            ->softRequired()
261
-                            ->localizeLabel('Price name')
262
-                            ->options(InvoiceModel::getAvailablePriceNameOptions())
263
-                            ->afterStateUpdated(static function (Get $get, Set $set, $state, $old) {
264
-                                if ($state !== 'other' && $old === 'other' && filled($get('price_name.custom'))) {
265
-                                    $set('price_name.old_custom', $get('price_name.custom'));
266
-                                    $set('price_name.custom', null);
267
-                                }
268
-
269
-                                if ($state === 'other' && $old !== 'other') {
270
-                                    $set('price_name.custom', $get('price_name.old_custom'));
271
-                                }
272
-                            }),
273
-                        TextInput::make('price_name.custom')
274
-                            ->hiddenLabel()
275
-                            ->disabled(static fn (callable $get) => $get('price_name.option') !== 'other')
276
-                            ->nullable(),
277
-                        Select::make('amount_name.option')
278
-                            ->softRequired()
279
-                            ->localizeLabel('Amount name')
280
-                            ->options(InvoiceModel::getAvailableAmountNameOptions())
281
-                            ->afterStateUpdated(static function (Get $get, Set $set, $state, $old) {
282
-                                if ($state !== 'other' && $old === 'other' && filled($get('amount_name.custom'))) {
283
-                                    $set('amount_name.old_custom', $get('amount_name.custom'));
284
-                                    $set('amount_name.custom', null);
285
-                                }
286
-
287
-                                if ($state === 'other' && $old !== 'other') {
288
-                                    $set('amount_name.custom', $get('amount_name.old_custom'));
289
-                                }
290
-                            }),
291
-                        TextInput::make('amount_name.custom')
292
-                            ->hiddenLabel()
293
-                            ->disabled(static fn (callable $get) => $get('amount_name.option') !== 'other')
294
-                            ->nullable(),
295
-                    ])->columnSpan(1),
296
-                Grid::make()
297
-                    ->schema([
298
-                        ViewField::make('preview.default')
299
-                            ->columnSpan(2)
300
-                            ->hiddenLabel()
301
-                            ->visible(static fn (Get $get) => $get('template') === 'default')
302
-                            ->view('filament.company.components.invoice-layouts.default'),
303
-                        ViewField::make('preview.modern')
304
-                            ->columnSpan(2)
305
-                            ->hiddenLabel()
306
-                            ->visible(static fn (Get $get) => $get('template') === 'modern')
307
-                            ->view('filament.company.components.invoice-layouts.modern'),
308
-                        ViewField::make('preview.classic')
309
-                            ->columnSpan(2)
310
-                            ->hiddenLabel()
311
-                            ->visible(static fn (Get $get) => $get('template') === 'classic')
312
-                            ->view('filament.company.components.invoice-layouts.classic'),
313
-                    ])->columnSpan(2),
314
-            ])->columns(3);
315
-    }
316
-
317
-    protected function handleRecordUpdate(InvoiceModel $record, array $data): InvoiceModel
318
-    {
319
-        $record->update($data);
320
-
321
-        return $record;
322
-    }
323
-
324
-    /**
325
-     * @return array<Action | ActionGroup>
326
-     */
327
-    protected function getFormActions(): array
328
-    {
329
-        return [
330
-            $this->getSaveFormAction(),
331
-        ];
332
-    }
333
-
334
-    protected function getSaveFormAction(): Action
335
-    {
336
-        return Action::make('save')
337
-            ->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
338
-            ->submit('save')
339
-            ->keyBindings(['mod+s']);
340
-    }
341
-
342
-    public static function canView(Model $record): bool
343
-    {
344
-        try {
345
-            return authorize('update', $record)->allowed();
346
-        } catch (AuthorizationException $exception) {
347
-            return $exception->toResponse()->allowed();
348
-        }
349
-    }
350
-}

+ 282
- 0
app/Filament/Company/Clusters/Settings/Resources/DocumentDefaultResource.php Visa fil

@@ -0,0 +1,282 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Clusters\Settings\Resources;
4
+
5
+use App\Enums\Accounting\DocumentType;
6
+use App\Enums\Setting\Font;
7
+use App\Enums\Setting\PaymentTerms;
8
+use App\Enums\Setting\Template;
9
+use App\Filament\Company\Clusters\Settings;
10
+use App\Filament\Company\Clusters\Settings\Resources\DocumentDefaultResource\Pages;
11
+use App\Models\Setting\DocumentDefault;
12
+use Filament\Forms;
13
+use Filament\Forms\Components\Component;
14
+use Filament\Forms\Form;
15
+use Filament\Forms\Get;
16
+use Filament\Forms\Set;
17
+use Filament\Resources\Resource;
18
+use Filament\Tables;
19
+use Filament\Tables\Table;
20
+use Illuminate\Support\Facades\Auth;
21
+use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
22
+
23
+class DocumentDefaultResource extends Resource
24
+{
25
+    protected static ?string $model = DocumentDefault::class;
26
+
27
+    protected static ?string $cluster = Settings::class;
28
+
29
+    protected static ?string $modelLabel = 'document template';
30
+
31
+    public static function form(Form $form): Form
32
+    {
33
+        return $form
34
+            ->live()
35
+            ->schema([
36
+                self::getGeneralSection(),
37
+                self::getContentSection(),
38
+                self::getTemplateSection(),
39
+                self::getBillColumnLabelsSection(),
40
+            ]);
41
+    }
42
+
43
+    public static function getGeneralSection(): Forms\Components\Component
44
+    {
45
+        return Forms\Components\Section::make('General')
46
+            ->schema([
47
+                Forms\Components\TextInput::make('number_prefix')
48
+                    ->localizeLabel()
49
+                    ->nullable(),
50
+                Forms\Components\Select::make('payment_terms')
51
+                    ->softRequired()
52
+                    ->localizeLabel()
53
+                    ->options(PaymentTerms::class),
54
+            ])->columns();
55
+    }
56
+
57
+    public static function getContentSection(): Forms\Components\Component
58
+    {
59
+        return Forms\Components\Section::make('Content')
60
+            ->hidden(static fn (DocumentDefault $record) => $record->type === DocumentType::Bill)
61
+            ->schema([
62
+                Forms\Components\TextInput::make('header')
63
+                    ->localizeLabel()
64
+                    ->nullable(),
65
+                Forms\Components\TextInput::make('subheader')
66
+                    ->localizeLabel()
67
+                    ->nullable(),
68
+                Forms\Components\Textarea::make('terms')
69
+                    ->localizeLabel()
70
+                    ->nullable(),
71
+                Forms\Components\Textarea::make('footer')
72
+                    ->localizeLabel('Footer')
73
+                    ->nullable(),
74
+            ])->columns();
75
+    }
76
+
77
+    public static function getTemplateSection(): Component
78
+    {
79
+        return Forms\Components\Section::make('Template')
80
+            ->description('Choose the template and edit the column names.')
81
+            ->hidden(static fn (DocumentDefault $record) => $record->type === DocumentType::Bill)
82
+            ->schema([
83
+                Forms\Components\Grid::make(1)
84
+                    ->schema([
85
+                        Forms\Components\FileUpload::make('logo')
86
+                            ->openable()
87
+                            ->maxSize(1024)
88
+                            ->localizeLabel()
89
+                            ->visibility('public')
90
+                            ->disk('public')
91
+                            ->directory('logos/document')
92
+                            ->imageResizeMode('contain')
93
+                            ->imageCropAspectRatio('3:2')
94
+                            ->panelAspectRatio('3:2')
95
+                            ->panelLayout('integrated')
96
+                            ->removeUploadedFileButtonPosition('center bottom')
97
+                            ->uploadButtonPosition('center bottom')
98
+                            ->uploadProgressIndicatorPosition('center bottom')
99
+                            ->getUploadedFileNameForStorageUsing(
100
+                                static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
101
+                                    ->prepend(Auth::user()->currentCompany->id . '_'),
102
+                            )
103
+                            ->extraAttributes([
104
+                                'class' => 'aspect-[3/2] w-[9.375rem] max-w-full',
105
+                            ])
106
+                            ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
107
+                        Forms\Components\Checkbox::make('show_logo')
108
+                            ->localizeLabel(),
109
+                        Forms\Components\ColorPicker::make('accent_color')
110
+                            ->localizeLabel(),
111
+                        Forms\Components\Select::make('font')
112
+                            ->softRequired()
113
+                            ->localizeLabel()
114
+                            ->allowHtml()
115
+                            ->options(
116
+                                collect(Font::cases())
117
+                                    ->mapWithKeys(static fn ($case) => [
118
+                                        $case->value => "<span style='font-family:{$case->getLabel()}'>{$case->getLabel()}</span>",
119
+                                    ]),
120
+                            ),
121
+                        Forms\Components\Select::make('template')
122
+                            ->softRequired()
123
+                            ->localizeLabel()
124
+                            ->options(Template::class),
125
+                        ...static::getColumnLabelsSchema(),
126
+                    ])->columnSpan(1),
127
+                Forms\Components\Grid::make()
128
+                    ->schema([
129
+                        Forms\Components\ViewField::make('preview.default')
130
+                            ->columnSpan(2)
131
+                            ->hiddenLabel()
132
+                            ->visible(static fn (Get $get) => $get('template') === 'default')
133
+                            ->view('filament.company.components.invoice-layouts.default'),
134
+                        Forms\Components\ViewField::make('preview.modern')
135
+                            ->columnSpan(2)
136
+                            ->hiddenLabel()
137
+                            ->visible(static fn (Get $get) => $get('template') === 'modern')
138
+                            ->view('filament.company.components.invoice-layouts.modern'),
139
+                        Forms\Components\ViewField::make('preview.classic')
140
+                            ->columnSpan(2)
141
+                            ->hiddenLabel()
142
+                            ->visible(static fn (Get $get) => $get('template') === 'classic')
143
+                            ->view('filament.company.components.invoice-layouts.classic'),
144
+                    ])->columnSpan(2),
145
+            ])->columns(3);
146
+    }
147
+
148
+    public static function getBillColumnLabelsSection(): Component
149
+    {
150
+        return Forms\Components\Section::make('Column Labels')
151
+            ->visible(static fn (DocumentDefault $record) => $record->type === DocumentType::Bill)
152
+            ->schema(static::getColumnLabelsSchema())->columns();
153
+    }
154
+
155
+    public static function getColumnLabelsSchema(): array
156
+    {
157
+        return [
158
+            Forms\Components\Select::make('item_name.option')
159
+                ->softRequired()
160
+                ->localizeLabel('Item name')
161
+                ->options(DocumentDefault::getAvailableItemNameOptions())
162
+                ->afterStateUpdated(static function (Get $get, Set $set, $state, $old) {
163
+                    if ($state !== 'other' && $old === 'other' && filled($get('item_name.custom'))) {
164
+                        $set('item_name.old_custom', $get('item_name.custom'));
165
+                        $set('item_name.custom', null);
166
+                    }
167
+
168
+                    if ($state === 'other' && $old !== 'other') {
169
+                        $set('item_name.custom', $get('item_name.old_custom'));
170
+                    }
171
+                }),
172
+            Forms\Components\TextInput::make('item_name.custom')
173
+                ->hiddenLabel()
174
+                ->extraFieldWrapperAttributes(static fn (DocumentDefault $record) => [
175
+                    'class' => $record->type === DocumentType::Bill ? 'report-hidden-label' : '',
176
+                ])
177
+                ->disabled(static fn (callable $get) => $get('item_name.option') !== 'other')
178
+                ->nullable(),
179
+            Forms\Components\Select::make('unit_name.option')
180
+                ->softRequired()
181
+                ->localizeLabel('Unit name')
182
+                ->options(DocumentDefault::getAvailableUnitNameOptions())
183
+                ->afterStateUpdated(static function (Get $get, Set $set, $state, $old) {
184
+                    if ($state !== 'other' && $old === 'other' && filled($get('unit_name.custom'))) {
185
+                        $set('unit_name.old_custom', $get('unit_name.custom'));
186
+                        $set('unit_name.custom', null);
187
+                    }
188
+
189
+                    if ($state === 'other' && $old !== 'other') {
190
+                        $set('unit_name.custom', $get('unit_name.old_custom'));
191
+                    }
192
+                }),
193
+            Forms\Components\TextInput::make('unit_name.custom')
194
+                ->hiddenLabel()
195
+                ->extraFieldWrapperAttributes(static fn (DocumentDefault $record) => [
196
+                    'class' => $record->type === DocumentType::Bill ? 'report-hidden-label' : '',
197
+                ])
198
+                ->disabled(static fn (callable $get) => $get('unit_name.option') !== 'other')
199
+                ->nullable(),
200
+            Forms\Components\Select::make('price_name.option')
201
+                ->softRequired()
202
+                ->localizeLabel('Price name')
203
+                ->options(DocumentDefault::getAvailablePriceNameOptions())
204
+                ->afterStateUpdated(static function (Get $get, Set $set, $state, $old) {
205
+                    if ($state !== 'other' && $old === 'other' && filled($get('price_name.custom'))) {
206
+                        $set('price_name.old_custom', $get('price_name.custom'));
207
+                        $set('price_name.custom', null);
208
+                    }
209
+
210
+                    if ($state === 'other' && $old !== 'other') {
211
+                        $set('price_name.custom', $get('price_name.old_custom'));
212
+                    }
213
+                }),
214
+            Forms\Components\TextInput::make('price_name.custom')
215
+                ->hiddenLabel()
216
+                ->extraFieldWrapperAttributes(static fn (DocumentDefault $record) => [
217
+                    'class' => $record->type === DocumentType::Bill ? 'report-hidden-label' : '',
218
+                ])
219
+                ->disabled(static fn (callable $get) => $get('price_name.option') !== 'other')
220
+                ->nullable(),
221
+            Forms\Components\Select::make('amount_name.option')
222
+                ->softRequired()
223
+                ->localizeLabel('Amount name')
224
+                ->options(DocumentDefault::getAvailableAmountNameOptions())
225
+                ->afterStateUpdated(static function (Get $get, Set $set, $state, $old) {
226
+                    if ($state !== 'other' && $old === 'other' && filled($get('amount_name.custom'))) {
227
+                        $set('amount_name.old_custom', $get('amount_name.custom'));
228
+                        $set('amount_name.custom', null);
229
+                    }
230
+
231
+                    if ($state === 'other' && $old !== 'other') {
232
+                        $set('amount_name.custom', $get('amount_name.old_custom'));
233
+                    }
234
+                }),
235
+            Forms\Components\TextInput::make('amount_name.custom')
236
+                ->hiddenLabel()
237
+                ->extraFieldWrapperAttributes(static fn (DocumentDefault $record) => [
238
+                    'class' => $record->type === DocumentType::Bill ? 'report-hidden-label' : '',
239
+                ])
240
+                ->disabled(static fn (callable $get) => $get('amount_name.option') !== 'other')
241
+                ->nullable(),
242
+        ];
243
+    }
244
+
245
+    public static function table(Table $table): Table
246
+    {
247
+        return $table
248
+            ->columns([
249
+                Tables\Columns\TextColumn::make('type')
250
+                    ->badge(),
251
+                Tables\Columns\TextColumn::make('number_prefix'),
252
+                Tables\Columns\TextColumn::make('template')
253
+                    ->badge(),
254
+                Tables\Columns\IconColumn::make('show_logo')
255
+                    ->boolean(),
256
+            ])
257
+            ->filters([
258
+                //
259
+            ])
260
+            ->actions([
261
+                Tables\Actions\EditAction::make(),
262
+            ])
263
+            ->bulkActions([
264
+                //
265
+            ]);
266
+    }
267
+
268
+    public static function getRelations(): array
269
+    {
270
+        return [
271
+            //
272
+        ];
273
+    }
274
+
275
+    public static function getPages(): array
276
+    {
277
+        return [
278
+            'index' => Pages\ListDocumentDefaults::route('/'),
279
+            'edit' => Pages\EditDocumentDefault::route('/{record}/edit'),
280
+        ];
281
+    }
282
+}

+ 26
- 0
app/Filament/Company/Clusters/Settings/Resources/DocumentDefaultResource/Pages/EditDocumentDefault.php Visa fil

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Clusters\Settings\Resources\DocumentDefaultResource\Pages;
4
+
5
+use App\Filament\Company\Clusters\Settings\Resources\DocumentDefaultResource;
6
+use Filament\Resources\Pages\EditRecord;
7
+use Illuminate\Contracts\Support\Htmlable;
8
+
9
+class EditDocumentDefault extends EditRecord
10
+{
11
+    protected static string $resource = DocumentDefaultResource::class;
12
+
13
+    public function getRecordTitle(): string | Htmlable
14
+    {
15
+        return $this->record->type->getLabel();
16
+    }
17
+
18
+    public function getBreadcrumbs(): array
19
+    {
20
+        $breadcrumbs = parent::getBreadcrumbs();
21
+
22
+        array_pop($breadcrumbs);
23
+
24
+        return $breadcrumbs;
25
+    }
26
+}

+ 17
- 0
app/Filament/Company/Clusters/Settings/Resources/DocumentDefaultResource/Pages/ListDocumentDefaults.php Visa fil

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Clusters\Settings\Resources\DocumentDefaultResource\Pages;
4
+
5
+use App\Filament\Company\Clusters\Settings\Resources\DocumentDefaultResource;
6
+use Filament\Resources\Pages\ListRecords;
7
+use Filament\Support\Enums\MaxWidth;
8
+
9
+class ListDocumentDefaults extends ListRecords
10
+{
11
+    protected static string $resource = DocumentDefaultResource::class;
12
+
13
+    public function getMaxContentWidth(): MaxWidth | string | null
14
+    {
15
+        return MaxWidth::ScreenTwoExtraLarge;
16
+    }
17
+}

+ 1
- 1
app/Filament/Company/Resources/Purchases/BillResource.php Visa fil

@@ -72,7 +72,7 @@ class BillResource extends Resource
72 72
                             Forms\Components\Group::make([
73 73
                                 Forms\Components\TextInput::make('bill_number')
74 74
                                     ->label('Bill number')
75
-                                    ->default(fn () => Bill::getNextDocumentNumber())
75
+                                    ->default(static fn () => Bill::getNextDocumentNumber())
76 76
                                     ->required(),
77 77
                                 Forms\Components\TextInput::make('order_number')
78 78
                                     ->label('P.O/S.O Number'),

+ 27
- 62
app/Filament/Company/Resources/Sales/EstimateResource.php Visa fil

@@ -8,6 +8,8 @@ use App\Enums\Accounting\EstimateStatus;
8 8
 use App\Filament\Company\Resources\Sales\EstimateResource\Pages;
9 9
 use App\Filament\Company\Resources\Sales\EstimateResource\Widgets;
10 10
 use App\Filament\Forms\Components\CreateCurrencySelect;
11
+use App\Filament\Forms\Components\DocumentFooterSection;
12
+use App\Filament\Forms\Components\DocumentHeaderSection;
11 13
 use App\Filament\Forms\Components\DocumentTotals;
12 14
 use App\Filament\Tables\Actions\ReplicateBulkAction;
13 15
 use App\Filament\Tables\Columns;
@@ -22,7 +24,6 @@ use App\Utilities\RateCalculator;
22 24
 use Awcodes\TableRepeater\Components\TableRepeater;
23 25
 use Awcodes\TableRepeater\Header;
24 26
 use Filament\Forms;
25
-use Filament\Forms\Components\FileUpload;
26 27
 use Filament\Forms\Form;
27 28
 use Filament\Notifications\Notification;
28 29
 use Filament\Resources\Resource;
@@ -31,7 +32,6 @@ use Filament\Tables;
31 32
 use Filament\Tables\Table;
32 33
 use Illuminate\Database\Eloquent\Collection;
33 34
 use Illuminate\Support\Facades\Auth;
34
-use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
35 35
 
36 36
 class EstimateResource extends Resource
37 37
 {
@@ -41,51 +41,13 @@ class EstimateResource extends Resource
41 41
     {
42 42
         $company = Auth::user()->currentCompany;
43 43
 
44
+        $settings = $company->defaultEstimate;
45
+
44 46
         return $form
45 47
             ->schema([
46
-                Forms\Components\Section::make('Estimate Header')
47
-                    ->collapsible()
48
-                    ->collapsed()
49
-                    ->schema([
50
-                        Forms\Components\Split::make([
51
-                            Forms\Components\Group::make([
52
-                                FileUpload::make('logo')
53
-                                    ->openable()
54
-                                    ->maxSize(1024)
55
-                                    ->localizeLabel()
56
-                                    ->visibility('public')
57
-                                    ->disk('public')
58
-                                    ->directory('logos/document')
59
-                                    ->imageResizeMode('contain')
60
-                                    ->imageCropAspectRatio('3:2')
61
-                                    ->panelAspectRatio('3:2')
62
-                                    ->maxWidth(MaxWidth::ExtraSmall)
63
-                                    ->panelLayout('integrated')
64
-                                    ->removeUploadedFileButtonPosition('center bottom')
65
-                                    ->uploadButtonPosition('center bottom')
66
-                                    ->uploadProgressIndicatorPosition('center bottom')
67
-                                    ->getUploadedFileNameForStorageUsing(
68
-                                        static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
69
-                                            ->prepend(Auth::user()->currentCompany->id . '_'),
70
-                                    )
71
-                                    ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
72
-                            ]),
73
-                            Forms\Components\Group::make([
74
-                                Forms\Components\TextInput::make('header')
75
-                                    ->default('Estimate'),
76
-                                Forms\Components\TextInput::make('subheader'),
77
-                                Forms\Components\View::make('filament.forms.components.company-info')
78
-                                    ->viewData([
79
-                                        'company_name' => $company->name,
80
-                                        'company_address' => $company->profile->address,
81
-                                        'company_city' => $company->profile->city?->name,
82
-                                        'company_state' => $company->profile->state?->name,
83
-                                        'company_zip' => $company->profile->zip_code,
84
-                                        'company_country' => $company->profile->state?->country->name,
85
-                                    ]),
86
-                            ])->grow(true),
87
-                        ])->from('md'),
88
-                    ]),
48
+                DocumentHeaderSection::make('Estimate Header')
49
+                    ->defaultHeader($settings->header)
50
+                    ->defaultSubheader($settings->subheader),
89 51
                 Forms\Components\Section::make('Estimate Details')
90 52
                     ->schema([
91 53
                         Forms\Components\Split::make([
@@ -112,7 +74,7 @@ class EstimateResource extends Resource
112 74
                             Forms\Components\Group::make([
113 75
                                 Forms\Components\TextInput::make('estimate_number')
114 76
                                     ->label('Estimate number')
115
-                                    ->default(fn () => Estimate::getNextDocumentNumber()),
77
+                                    ->default(static fn () => Estimate::getNextDocumentNumber()),
116 78
                                 Forms\Components\TextInput::make('reference_number')
117 79
                                     ->label('Reference number'),
118 80
                                 Forms\Components\DatePicker::make('date')
@@ -129,8 +91,8 @@ class EstimateResource extends Resource
129 91
                                     }),
130 92
                                 Forms\Components\DatePicker::make('expiration_date')
131 93
                                     ->label('Expiration date')
132
-                                    ->default(function () use ($company) {
133
-                                        return now()->addDays($company->defaultInvoice->payment_terms->getDays());
94
+                                    ->default(function () use ($settings) {
95
+                                        return now()->addDays($settings->payment_terms->getDays());
134 96
                                     })
135 97
                                     ->minDate(static function (Forms\Get $get) {
136 98
                                         return $get('date') ?? now();
@@ -154,22 +116,29 @@ class EstimateResource extends Resource
154 116
                             ->relationship()
155 117
                             ->saveRelationshipsUsing(null)
156 118
                             ->dehydrated(true)
157
-                            ->headers(function (Forms\Get $get) {
119
+                            ->headers(function (Forms\Get $get) use ($settings) {
158 120
                                 $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
159 121
 
160 122
                                 $headers = [
161
-                                    Header::make('Items')->width($hasDiscounts ? '15%' : '20%'),
162
-                                    Header::make('Description')->width($hasDiscounts ? '25%' : '30%'),  // Increase when no discounts
163
-                                    Header::make('Quantity')->width('10%'),
164
-                                    Header::make('Price')->width('10%'),
165
-                                    Header::make('Taxes')->width($hasDiscounts ? '15%' : '20%'),       // Increase when no discounts
123
+                                    Header::make($settings->resolveColumnLabel('item_name', 'Items'))
124
+                                        ->width($hasDiscounts ? '15%' : '20%'),
125
+                                    Header::make('Description')
126
+                                        ->width($hasDiscounts ? '25%' : '30%'),
127
+                                    Header::make($settings->resolveColumnLabel('unit_name', 'Quantity'))
128
+                                        ->width('10%'),
129
+                                    Header::make($settings->resolveColumnLabel('price_name', 'Price'))
130
+                                        ->width('10%'),
131
+                                    Header::make('Taxes')
132
+                                        ->width($hasDiscounts ? '15%' : '20%'),
166 133
                                 ];
167 134
 
168 135
                                 if ($hasDiscounts) {
169 136
                                     $headers[] = Header::make('Discounts')->width('15%');
170 137
                                 }
171 138
 
172
-                                $headers[] = Header::make('Amount')->width('10%')->align('right');
139
+                                $headers[] = Header::make($settings->resolveColumnLabel('amount_name', 'Amount'))
140
+                                    ->width('10%')
141
+                                    ->align('right');
173 142
 
174 143
                                 return $headers;
175 144
                             })
@@ -270,15 +239,11 @@ class EstimateResource extends Resource
270 239
                         DocumentTotals::make()
271 240
                             ->type(DocumentType::Estimate),
272 241
                         Forms\Components\Textarea::make('terms')
242
+                            ->default($settings->terms)
273 243
                             ->columnSpanFull(),
274 244
                     ]),
275
-                Forms\Components\Section::make('Estimate Footer')
276
-                    ->collapsible()
277
-                    ->collapsed()
278
-                    ->schema([
279
-                        Forms\Components\Textarea::make('footer')
280
-                            ->columnSpanFull(),
281
-                    ]),
245
+                DocumentFooterSection::make('Estimate Footer')
246
+                    ->defaultFooter($settings->footer),
282 247
             ]);
283 248
     }
284 249
 

+ 27
- 63
app/Filament/Company/Resources/Sales/InvoiceResource.php Visa fil

@@ -12,6 +12,8 @@ use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
12 12
 use App\Filament\Company\Resources\Sales\InvoiceResource\RelationManagers;
13 13
 use App\Filament\Company\Resources\Sales\InvoiceResource\Widgets;
14 14
 use App\Filament\Forms\Components\CreateCurrencySelect;
15
+use App\Filament\Forms\Components\DocumentFooterSection;
16
+use App\Filament\Forms\Components\DocumentHeaderSection;
15 17
 use App\Filament\Forms\Components\DocumentTotals;
16 18
 use App\Filament\Tables\Actions\ReplicateBulkAction;
17 19
 use App\Filament\Tables\Columns;
@@ -28,7 +30,6 @@ use Awcodes\TableRepeater\Components\TableRepeater;
28 30
 use Awcodes\TableRepeater\Header;
29 31
 use Closure;
30 32
 use Filament\Forms;
31
-use Filament\Forms\Components\FileUpload;
32 33
 use Filament\Forms\Form;
33 34
 use Filament\Notifications\Notification;
34 35
 use Filament\Resources\Resource;
@@ -39,7 +40,6 @@ use Filament\Tables\Table;
39 40
 use Illuminate\Database\Eloquent\Builder;
40 41
 use Illuminate\Database\Eloquent\Collection;
41 42
 use Illuminate\Support\Facades\Auth;
42
-use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
43 43
 
44 44
 class InvoiceResource extends Resource
45 45
 {
@@ -49,52 +49,13 @@ class InvoiceResource extends Resource
49 49
     {
50 50
         $company = Auth::user()->currentCompany;
51 51
 
52
+        $settings = $company->defaultInvoice;
53
+
52 54
         return $form
53 55
             ->schema([
54
-                Forms\Components\Section::make('Invoice Header')
55
-                    ->collapsible()
56
-                    ->collapsed()
57
-                    ->schema([
58
-                        Forms\Components\Split::make([
59
-                            Forms\Components\Group::make([
60
-                                FileUpload::make('logo')
61
-                                    ->openable()
62
-                                    ->maxSize(1024)
63
-                                    ->localizeLabel()
64
-                                    ->visibility('public')
65
-                                    ->disk('public')
66
-                                    ->directory('logos/document')
67
-                                    ->imageResizeMode('contain')
68
-                                    ->imageCropAspectRatio('3:2')
69
-                                    ->panelAspectRatio('3:2')
70
-                                    ->maxWidth(MaxWidth::ExtraSmall)
71
-                                    ->panelLayout('integrated')
72
-                                    ->removeUploadedFileButtonPosition('center bottom')
73
-                                    ->uploadButtonPosition('center bottom')
74
-                                    ->uploadProgressIndicatorPosition('center bottom')
75
-                                    ->getUploadedFileNameForStorageUsing(
76
-                                        static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
77
-                                            ->prepend(Auth::user()->currentCompany->id . '_'),
78
-                                    )
79
-                                    ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
80
-                            ]),
81
-                            Forms\Components\Group::make([
82
-                                Forms\Components\TextInput::make('header')
83
-                                    ->default(fn () => $company->defaultInvoice->header),
84
-                                Forms\Components\TextInput::make('subheader')
85
-                                    ->default(fn () => $company->defaultInvoice->subheader),
86
-                                Forms\Components\View::make('filament.forms.components.company-info')
87
-                                    ->viewData([
88
-                                        'company_name' => $company->name,
89
-                                        'company_address' => $company->profile->address,
90
-                                        'company_city' => $company->profile->city?->name,
91
-                                        'company_state' => $company->profile->state?->name,
92
-                                        'company_zip' => $company->profile->zip_code,
93
-                                        'company_country' => $company->profile->state?->country->name,
94
-                                    ]),
95
-                            ])->grow(true),
96
-                        ])->from('md'),
97
-                    ]),
56
+                DocumentHeaderSection::make('Invoice Header')
57
+                    ->defaultHeader($settings->header)
58
+                    ->defaultSubheader($settings->subheader),
98 59
                 Forms\Components\Section::make('Invoice Details')
99 60
                     ->schema([
100 61
                         Forms\Components\Split::make([
@@ -124,7 +85,7 @@ class InvoiceResource extends Resource
124 85
                             Forms\Components\Group::make([
125 86
                                 Forms\Components\TextInput::make('invoice_number')
126 87
                                     ->label('Invoice number')
127
-                                    ->default(fn () => Invoice::getNextDocumentNumber()),
88
+                                    ->default(static fn () => Invoice::getNextDocumentNumber()),
128 89
                                 Forms\Components\TextInput::make('order_number')
129 90
                                     ->label('P.O/S.O Number'),
130 91
                                 Forms\Components\DatePicker::make('date')
@@ -144,8 +105,8 @@ class InvoiceResource extends Resource
144 105
                                     }),
145 106
                                 Forms\Components\DatePicker::make('due_date')
146 107
                                     ->label('Payment due')
147
-                                    ->default(function () use ($company) {
148
-                                        return now()->addDays($company->defaultInvoice->payment_terms->getDays());
108
+                                    ->default(function () use ($settings) {
109
+                                        return now()->addDays($settings->payment_terms->getDays());
149 110
                                     })
150 111
                                     ->minDate(static function (Forms\Get $get) {
151 112
                                         return $get('date') ?? now();
@@ -169,22 +130,29 @@ class InvoiceResource extends Resource
169 130
                             ->relationship()
170 131
                             ->saveRelationshipsUsing(null)
171 132
                             ->dehydrated(true)
172
-                            ->headers(function (Forms\Get $get) {
133
+                            ->headers(function (Forms\Get $get) use ($settings) {
173 134
                                 $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
174 135
 
175 136
                                 $headers = [
176
-                                    Header::make('Items')->width($hasDiscounts ? '15%' : '20%'),
177
-                                    Header::make('Description')->width($hasDiscounts ? '25%' : '30%'),  // Increase when no discounts
178
-                                    Header::make('Quantity')->width('10%'),
179
-                                    Header::make('Price')->width('10%'),
180
-                                    Header::make('Taxes')->width($hasDiscounts ? '15%' : '20%'),       // Increase when no discounts
137
+                                    Header::make($settings->resolveColumnLabel('item_name', 'Items'))
138
+                                        ->width($hasDiscounts ? '15%' : '20%'),
139
+                                    Header::make('Description')
140
+                                        ->width($hasDiscounts ? '25%' : '30%'),
141
+                                    Header::make($settings->resolveColumnLabel('unit_name', 'Quantity'))
142
+                                        ->width('10%'),
143
+                                    Header::make($settings->resolveColumnLabel('price_name', 'Price'))
144
+                                        ->width('10%'),
145
+                                    Header::make('Taxes')
146
+                                        ->width($hasDiscounts ? '15%' : '20%'),
181 147
                                 ];
182 148
 
183 149
                                 if ($hasDiscounts) {
184 150
                                     $headers[] = Header::make('Discounts')->width('15%');
185 151
                                 }
186 152
 
187
-                                $headers[] = Header::make('Amount')->width('10%')->align('right');
153
+                                $headers[] = Header::make($settings->resolveColumnLabel('amount_name', 'Amount'))
154
+                                    ->width('10%')
155
+                                    ->align('right');
188 156
 
189 157
                                 return $headers;
190 158
                             })
@@ -285,15 +253,11 @@ class InvoiceResource extends Resource
285 253
                         DocumentTotals::make()
286 254
                             ->type(DocumentType::Invoice),
287 255
                         Forms\Components\Textarea::make('terms')
256
+                            ->default($settings->terms)
288 257
                             ->columnSpanFull(),
289 258
                     ]),
290
-                Forms\Components\Section::make('Invoice Footer')
291
-                    ->collapsible()
292
-                    ->collapsed()
293
-                    ->schema([
294
-                        Forms\Components\Textarea::make('footer')
295
-                            ->columnSpanFull(),
296
-                    ]),
259
+                DocumentFooterSection::make('Invoice Footer')
260
+                    ->defaultFooter($settings->footer),
297 261
             ]);
298 262
     }
299 263
 

+ 25
- 62
app/Filament/Company/Resources/Sales/RecurringInvoiceResource.php Visa fil

@@ -8,6 +8,8 @@ use App\Enums\Accounting\RecurringInvoiceStatus;
8 8
 use App\Enums\Setting\PaymentTerms;
9 9
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource\Pages;
10 10
 use App\Filament\Forms\Components\CreateCurrencySelect;
11
+use App\Filament\Forms\Components\DocumentFooterSection;
12
+use App\Filament\Forms\Components\DocumentHeaderSection;
11 13
 use App\Filament\Forms\Components\DocumentTotals;
12 14
 use App\Filament\Tables\Columns;
13 15
 use App\Models\Accounting\Adjustment;
@@ -20,14 +22,11 @@ use App\Utilities\RateCalculator;
20 22
 use Awcodes\TableRepeater\Components\TableRepeater;
21 23
 use Awcodes\TableRepeater\Header;
22 24
 use Filament\Forms;
23
-use Filament\Forms\Components\FileUpload;
24 25
 use Filament\Forms\Form;
25 26
 use Filament\Resources\Resource;
26
-use Filament\Support\Enums\MaxWidth;
27 27
 use Filament\Tables;
28 28
 use Filament\Tables\Table;
29 29
 use Illuminate\Support\Facades\Auth;
30
-use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
31 30
 
32 31
 class RecurringInvoiceResource extends Resource
33 32
 {
@@ -37,52 +36,13 @@ class RecurringInvoiceResource extends Resource
37 36
     {
38 37
         $company = Auth::user()->currentCompany;
39 38
 
39
+        $settings = $company->defaultInvoice;
40
+
40 41
         return $form
41 42
             ->schema([
42
-                Forms\Components\Section::make('Invoice Header')
43
-                    ->collapsible()
44
-                    ->collapsed()
45
-                    ->schema([
46
-                        Forms\Components\Split::make([
47
-                            Forms\Components\Group::make([
48
-                                FileUpload::make('logo')
49
-                                    ->openable()
50
-                                    ->maxSize(1024)
51
-                                    ->localizeLabel()
52
-                                    ->visibility('public')
53
-                                    ->disk('public')
54
-                                    ->directory('logos/document')
55
-                                    ->imageResizeMode('contain')
56
-                                    ->imageCropAspectRatio('3:2')
57
-                                    ->panelAspectRatio('3:2')
58
-                                    ->maxWidth(MaxWidth::ExtraSmall)
59
-                                    ->panelLayout('integrated')
60
-                                    ->removeUploadedFileButtonPosition('center bottom')
61
-                                    ->uploadButtonPosition('center bottom')
62
-                                    ->uploadProgressIndicatorPosition('center bottom')
63
-                                    ->getUploadedFileNameForStorageUsing(
64
-                                        static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
65
-                                            ->prepend(Auth::user()->currentCompany->id . '_'),
66
-                                    )
67
-                                    ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
68
-                            ]),
69
-                            Forms\Components\Group::make([
70
-                                Forms\Components\TextInput::make('header')
71
-                                    ->default(fn () => $company->defaultInvoice->header),
72
-                                Forms\Components\TextInput::make('subheader')
73
-                                    ->default(fn () => $company->defaultInvoice->subheader),
74
-                                Forms\Components\View::make('filament.forms.components.company-info')
75
-                                    ->viewData([
76
-                                        'company_name' => $company->name,
77
-                                        'company_address' => $company->profile->address,
78
-                                        'company_city' => $company->profile->city?->name,
79
-                                        'company_state' => $company->profile->state?->name,
80
-                                        'company_zip' => $company->profile->zip_code,
81
-                                        'company_country' => $company->profile->state?->country->name,
82
-                                    ]),
83
-                            ])->grow(true),
84
-                        ])->from('md'),
85
-                    ]),
43
+                DocumentHeaderSection::make('Invoice Header')
44
+                    ->defaultHeader($settings->header)
45
+                    ->defaultSubheader($settings->subheader),
86 46
                 Forms\Components\Section::make('Invoice Details')
87 47
                     ->schema([
88 48
                         Forms\Components\Split::make([
@@ -119,7 +79,7 @@ class RecurringInvoiceResource extends Resource
119 79
                                     ->label('Payment due')
120 80
                                     ->options(PaymentTerms::class)
121 81
                                     ->softRequired()
122
-                                    ->default($company->defaultInvoice->payment_terms)
82
+                                    ->default($settings->payment_terms)
123 83
                                     ->live(),
124 84
                                 Forms\Components\Select::make('discount_method')
125 85
                                     ->label('Discount method')
@@ -140,22 +100,29 @@ class RecurringInvoiceResource extends Resource
140 100
                             ->relationship()
141 101
                             ->saveRelationshipsUsing(null)
142 102
                             ->dehydrated(true)
143
-                            ->headers(function (Forms\Get $get) {
103
+                            ->headers(function (Forms\Get $get) use ($settings) {
144 104
                                 $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
145 105
 
146 106
                                 $headers = [
147
-                                    Header::make('Items')->width($hasDiscounts ? '15%' : '20%'),
148
-                                    Header::make('Description')->width($hasDiscounts ? '25%' : '30%'),  // Increase when no discounts
149
-                                    Header::make('Quantity')->width('10%'),
150
-                                    Header::make('Price')->width('10%'),
151
-                                    Header::make('Taxes')->width($hasDiscounts ? '15%' : '20%'),       // Increase when no discounts
107
+                                    Header::make($settings->resolveColumnLabel('item_name', 'Items'))
108
+                                        ->width($hasDiscounts ? '15%' : '20%'),
109
+                                    Header::make('Description')
110
+                                        ->width($hasDiscounts ? '25%' : '30%'),
111
+                                    Header::make($settings->resolveColumnLabel('unit_name', 'Quantity'))
112
+                                        ->width('10%'),
113
+                                    Header::make($settings->resolveColumnLabel('price_name', 'Price'))
114
+                                        ->width('10%'),
115
+                                    Header::make('Taxes')
116
+                                        ->width($hasDiscounts ? '15%' : '20%'),
152 117
                                 ];
153 118
 
154 119
                                 if ($hasDiscounts) {
155 120
                                     $headers[] = Header::make('Discounts')->width('15%');
156 121
                                 }
157 122
 
158
-                                $headers[] = Header::make('Amount')->width('10%')->align('right');
123
+                                $headers[] = Header::make($settings->resolveColumnLabel('amount_name', 'Amount'))
124
+                                    ->width('10%')
125
+                                    ->align('right');
159 126
 
160 127
                                 return $headers;
161 128
                             })
@@ -256,15 +223,11 @@ class RecurringInvoiceResource extends Resource
256 223
                         DocumentTotals::make()
257 224
                             ->type(DocumentType::Invoice),
258 225
                         Forms\Components\Textarea::make('terms')
226
+                            ->default($settings->terms)
259 227
                             ->columnSpanFull(),
260 228
                     ]),
261
-                Forms\Components\Section::make('Invoice Footer')
262
-                    ->collapsible()
263
-                    ->collapsed()
264
-                    ->schema([
265
-                        Forms\Components\Textarea::make('footer')
266
-                            ->columnSpanFull(),
267
-                    ]),
229
+                DocumentFooterSection::make('Invoice Footer')
230
+                    ->defaultFooter($settings->footer),
268 231
             ]);
269 232
     }
270 233
 

+ 38
- 0
app/Filament/Forms/Components/DocumentFooterSection.php Visa fil

@@ -0,0 +1,38 @@
1
+<?php
2
+
3
+namespace App\Filament\Forms\Components;
4
+
5
+use Closure;
6
+use Filament\Forms\Components\Section;
7
+use Filament\Forms\Components\Textarea;
8
+
9
+class DocumentFooterSection extends Section
10
+{
11
+    protected string | Closure | null $defaultFooter = null;
12
+
13
+    public function defaultFooter(string | Closure | null $footer): static
14
+    {
15
+        $this->defaultFooter = $footer;
16
+
17
+        return $this;
18
+    }
19
+
20
+    protected function setUp(): void
21
+    {
22
+        parent::setUp();
23
+
24
+        $this->collapsible();
25
+        $this->collapsed();
26
+
27
+        $this->schema([
28
+            Textarea::make('footer')
29
+                ->default(fn () => $this->getDefaultFooter())
30
+                ->columnSpanFull(),
31
+        ]);
32
+    }
33
+
34
+    public function getDefaultFooter(): ?string
35
+    {
36
+        return $this->evaluate($this->defaultFooter);
37
+    }
38
+}

+ 97
- 0
app/Filament/Forms/Components/DocumentHeaderSection.php Visa fil

@@ -0,0 +1,97 @@
1
+<?php
2
+
3
+namespace App\Filament\Forms\Components;
4
+
5
+use Closure;
6
+use Filament\Forms\Components\FileUpload;
7
+use Filament\Forms\Components\Group;
8
+use Filament\Forms\Components\Section;
9
+use Filament\Forms\Components\Split;
10
+use Filament\Forms\Components\TextInput;
11
+use Filament\Forms\Components\View;
12
+use Filament\Support\Enums\MaxWidth;
13
+use Illuminate\Support\Facades\Auth;
14
+use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
15
+
16
+class DocumentHeaderSection extends Section
17
+{
18
+    protected string | Closure | null $defaultHeader = null;
19
+
20
+    protected string | Closure | null $defaultSubheader = null;
21
+
22
+    public function defaultHeader(string | Closure | null $header): static
23
+    {
24
+        $this->defaultHeader = $header;
25
+
26
+        return $this;
27
+    }
28
+
29
+    public function defaultSubheader(string | Closure | null $subheader): static
30
+    {
31
+        $this->defaultSubheader = $subheader;
32
+
33
+        return $this;
34
+    }
35
+
36
+    protected function setUp(): void
37
+    {
38
+        parent::setUp();
39
+
40
+        $this->collapsible();
41
+        $this->collapsed();
42
+
43
+        $company = Auth::user()->currentCompany;
44
+
45
+        $this->schema([
46
+            Split::make([
47
+                Group::make([
48
+                    FileUpload::make('logo')
49
+                        ->openable()
50
+                        ->maxSize(1024)
51
+                        ->localizeLabel()
52
+                        ->visibility('public')
53
+                        ->disk('public')
54
+                        ->directory('logos/document')
55
+                        ->imageResizeMode('contain')
56
+                        ->imageCropAspectRatio('3:2')
57
+                        ->panelAspectRatio('3:2')
58
+                        ->maxWidth(MaxWidth::ExtraSmall)
59
+                        ->panelLayout('integrated')
60
+                        ->removeUploadedFileButtonPosition('center bottom')
61
+                        ->uploadButtonPosition('center bottom')
62
+                        ->uploadProgressIndicatorPosition('center bottom')
63
+                        ->getUploadedFileNameForStorageUsing(
64
+                            static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
65
+                                ->prepend(Auth::user()->currentCompany->id . '_'),
66
+                        )
67
+                        ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
68
+                ]),
69
+                Group::make([
70
+                    TextInput::make('header')
71
+                        ->default(fn () => $this->getDefaultHeader()),
72
+                    TextInput::make('subheader')
73
+                        ->default(fn () => $this->getDefaultSubheader()),
74
+                    View::make('filament.forms.components.company-info')
75
+                        ->viewData([
76
+                            'company_name' => $company->name,
77
+                            'company_address' => $company->profile->address,
78
+                            'company_city' => $company->profile->city?->name,
79
+                            'company_state' => $company->profile->state?->name,
80
+                            'company_zip' => $company->profile->zip_code,
81
+                            'company_country' => $company->profile->state?->country->name,
82
+                        ]),
83
+                ])->grow(true),
84
+            ])->from('md'),
85
+        ]);
86
+    }
87
+
88
+    public function getDefaultHeader(): ?string
89
+    {
90
+        return $this->evaluate($this->defaultHeader);
91
+    }
92
+
93
+    public function getDefaultSubheader(): ?string
94
+    {
95
+        return $this->evaluate($this->defaultSubheader);
96
+    }
97
+}

+ 17
- 0
app/Filament/Infolists/Components/DocumentPreview.php Visa fil

@@ -3,6 +3,8 @@
3 3
 namespace App\Filament\Infolists\Components;
4 4
 
5 5
 use App\Enums\Accounting\DocumentType;
6
+use App\Enums\Setting\Template;
7
+use App\Models\Setting\DocumentDefault;
6 8
 use Filament\Infolists\Components\Grid;
7 9
 
8 10
 class DocumentPreview extends Grid
@@ -33,4 +35,19 @@ class DocumentPreview extends Grid
33 35
     {
34 36
         return $this->documentType;
35 37
     }
38
+
39
+    public function getTemplate(): Template
40
+    {
41
+        if ($this->documentType === DocumentType::RecurringInvoice) {
42
+            $lookupType = DocumentType::Invoice;
43
+        } else {
44
+            $lookupType = $this->documentType;
45
+        }
46
+
47
+        $defaults = DocumentDefault::query()
48
+            ->type($lookupType)
49
+            ->first();
50
+
51
+        return $defaults?->template ?? Template::Default;
52
+    }
36 53
 }

+ 9
- 1
app/Http/Middleware/Authenticate.php Visa fil

@@ -2,8 +2,10 @@
2 2
 
3 3
 namespace App\Http\Middleware;
4 4
 
5
+use Filament\Exceptions\NoDefaultPanelSetException;
5 6
 use Filament\Facades\Filament;
6 7
 use Filament\Http\Middleware\Authenticate as Middleware;
8
+use Wallo\FilamentCompanies\FilamentCompanies;
7 9
 
8 10
 class Authenticate extends Middleware
9 11
 {
@@ -12,6 +14,12 @@ class Authenticate extends Middleware
12 14
      */
13 15
     protected function redirectTo($request): ?string
14 16
     {
15
-        return Filament::getDefaultPanel()->getLoginUrl();
17
+        try {
18
+            $defaultPanelLoginUrl = Filament::getDefaultPanel()->getLoginUrl();
19
+        } catch (NoDefaultPanelSetException) {
20
+            $defaultPanelLoginUrl = Filament::getPanel(FilamentCompanies::getCompanyPanel())->getLoginUrl();
21
+        }
22
+
23
+        return $defaultPanelLoginUrl;
16 24
     }
17 25
 }

+ 24
- 0
app/Http/Responses/LoginRedirectResponse.php Visa fil

@@ -0,0 +1,24 @@
1
+<?php
2
+
3
+namespace App\Http\Responses;
4
+
5
+use Filament\Exceptions\NoDefaultPanelSetException;
6
+use Filament\Facades\Filament;
7
+use Filament\Http\Responses\Auth\LoginResponse;
8
+use Illuminate\Http\RedirectResponse;
9
+use Livewire\Features\SupportRedirects\Redirector;
10
+use Wallo\FilamentCompanies\FilamentCompanies;
11
+
12
+class LoginRedirectResponse extends LoginResponse
13
+{
14
+    public function toResponse($request): RedirectResponse | Redirector
15
+    {
16
+        try {
17
+            $defaultPanelUrl = Filament::getDefaultPanel()->getUrl();
18
+        } catch (NoDefaultPanelSetException) {
19
+            $defaultPanelUrl = Filament::getPanel(FilamentCompanies::getCompanyPanel())->getUrl();
20
+        }
21
+
22
+        return redirect()->intended($defaultPanelUrl);
23
+    }
24
+}

+ 0
- 15
app/Http/Responses/LoginResponse.php Visa fil

@@ -1,15 +0,0 @@
1
-<?php
2
-
3
-namespace App\Http\Responses;
4
-
5
-use Filament\Facades\Filament;
6
-use Illuminate\Http\RedirectResponse;
7
-use Livewire\Features\SupportRedirects\Redirector;
8
-
9
-class LoginResponse extends \Filament\Http\Responses\Auth\LoginResponse
10
-{
11
-    public function toResponse($request): RedirectResponse | Redirector
12
-    {
13
-        return redirect()->to(Filament::getDefaultPanel()->getUrl());
14
-    }
15
-}

+ 0
- 7
app/Listeners/ConfigureCompanyDefault.php Visa fil

@@ -2,7 +2,6 @@
2 2
 
3 3
 namespace App\Listeners;
4 4
 
5
-use App\Enums\Setting\PrimaryColor;
6 5
 use App\Events\CompanyConfigured;
7 6
 use App\Services\CompanySettingsService;
8 7
 use App\Utilities\Currency\ConfigureCurrencies;
@@ -11,7 +10,6 @@ use Filament\Forms\Components\DatePicker;
11 10
 use Filament\Forms\Components\Section;
12 11
 use Filament\Forms\Components\Tabs\Tab;
13 12
 use Filament\Resources\Components\Tab as ResourcesTab;
14
-use Filament\Support\Facades\FilamentColor;
15 13
 
16 14
 class ConfigureCompanyDefault
17 15
 {
@@ -32,12 +30,7 @@ class ConfigureCompanyDefault
32 30
         config(['app.timezone' => $settings['default_timezone']]);
33 31
         date_default_timezone_set($settings['default_timezone']);
34 32
 
35
-        FilamentColor::register([
36
-            'primary' => PrimaryColor::from($settings['default_primary_color'])->getColor(),
37
-        ]);
38
-
39 33
         Filament::getPanel('company')
40
-            ->font($settings['default_font'])
41 34
             ->brandName($company->name);
42 35
 
43 36
         DatePicker::configureUsing(static function (DatePicker $component) use ($settings) {

+ 13
- 2
app/Listeners/CreateEmployeeContact.php Visa fil

@@ -3,6 +3,8 @@
3 3
 namespace App\Listeners;
4 4
 
5 5
 use App\Enums\Common\ContactType;
6
+use App\Models\Company;
7
+use App\Models\User;
6 8
 use Wallo\FilamentCompanies\Events\CompanyEmployeeAdded;
7 9
 
8 10
 class CreateEmployeeContact
@@ -20,12 +22,21 @@ class CreateEmployeeContact
20 22
      */
21 23
     public function handle(CompanyEmployeeAdded $event): void
22 24
     {
25
+        /** @var Company $company */
23 26
         $company = $event->company;
27
+
28
+        /** @var User $employee */
24 29
         $employee = $event->user;
25 30
 
26
-        $company->contacts()->create([
31
+        $nameParts = explode(' ', $employee->name, 2);
32
+        $firstName = $nameParts[0];
33
+        $lastName = $nameParts[1] ?? '';
34
+
35
+        $employee->contacts()->create([
36
+            'company_id' => $company->id,
27 37
             'type' => ContactType::Employee,
28
-            'name' => $employee->name,
38
+            'first_name' => $firstName,
39
+            'last_name' => $lastName,
29 40
             'email' => $employee->email,
30 41
             'created_by' => $company->owner->id,
31 42
             'updated_by' => $company->owner->id,

+ 7
- 9
app/Models/Accounting/Bill.php Visa fil

@@ -14,7 +14,9 @@ use App\Enums\Accounting\TransactionType;
14 14
 use App\Filament\Company\Resources\Purchases\BillResource;
15 15
 use App\Models\Banking\BankAccount;
16 16
 use App\Models\Common\Vendor;
17
+use App\Models\Company;
17 18
 use App\Models\Setting\Currency;
19
+use App\Models\Setting\DocumentDefault;
18 20
 use App\Observers\BillObserver;
19 21
 use App\Utilities\Currency\CurrencyAccessor;
20 22
 use App\Utilities\Currency\CurrencyConverter;
@@ -106,7 +108,7 @@ class Bill extends Document
106 108
             ->where('type', TransactionType::Journal);
107 109
     }
108 110
 
109
-    public function documentType(): DocumentType
111
+    public static function documentType(): DocumentType
110 112
     {
111 113
         return DocumentType::Bill;
112 114
     }
@@ -171,9 +173,9 @@ class Bill extends Document
171 173
         return $this->payments->isNotEmpty();
172 174
     }
173 175
 
174
-    public static function getNextDocumentNumber(): string
176
+    public static function getNextDocumentNumber(?Company $company = null): string
175 177
     {
176
-        $company = auth()->user()->currentCompany;
178
+        $company ??= auth()->user()?->currentCompany;
177 179
 
178 180
         if (! $company) {
179 181
             throw new \RuntimeException('No current company is set for the user.');
@@ -181,8 +183,7 @@ class Bill extends Document
181 183
 
182 184
         $defaultBillSettings = $company->defaultBill;
183 185
 
184
-        $numberPrefix = $defaultBillSettings->number_prefix;
185
-        $numberDigits = $defaultBillSettings->number_digits;
186
+        $numberPrefix = $defaultBillSettings->number_prefix ?? '';
186 187
 
187 188
         $latestDocument = static::query()
188 189
             ->whereNotNull('bill_number')
@@ -191,15 +192,12 @@ class Bill extends Document
191 192
 
192 193
         $lastNumberNumericPart = $latestDocument
193 194
             ? (int) substr($latestDocument->bill_number, strlen($numberPrefix))
194
-            : 0;
195
+            : DocumentDefault::getBaseNumber();
195 196
 
196 197
         $numberNext = $lastNumberNumericPart + 1;
197 198
 
198 199
         return $defaultBillSettings->getNumberNext(
199
-            padded: true,
200
-            format: true,
201 200
             prefix: $numberPrefix,
202
-            digits: $numberDigits,
203 201
             next: $numberNext
204 202
         );
205 203
     }

+ 1
- 1
app/Models/Accounting/Document.php Visa fil

@@ -32,7 +32,7 @@ abstract class Document extends Model
32 32
         return $this->lineItems()->exists();
33 33
     }
34 34
 
35
-    abstract public function documentType(): DocumentType;
35
+    abstract public static function documentType(): DocumentType;
36 36
 
37 37
     abstract public function documentNumber(): ?string;
38 38
 

+ 8
- 10
app/Models/Accounting/Estimate.php Visa fil

@@ -13,6 +13,8 @@ use App\Enums\Accounting\InvoiceStatus;
13 13
 use App\Filament\Company\Resources\Sales\EstimateResource;
14 14
 use App\Filament\Company\Resources\Sales\InvoiceResource;
15 15
 use App\Models\Common\Client;
16
+use App\Models\Company;
17
+use App\Models\Setting\DocumentDefault;
16 18
 use App\Observers\EstimateObserver;
17 19
 use Filament\Actions\Action;
18 20
 use Filament\Actions\MountableAction;
@@ -89,7 +91,7 @@ class Estimate extends Document
89 91
         return $this->hasOne(Invoice::class);
90 92
     }
91 93
 
92
-    public function documentType(): DocumentType
94
+    public static function documentType(): DocumentType
93 95
     {
94 96
         return DocumentType::Estimate;
95 97
     }
@@ -212,18 +214,17 @@ class Estimate extends Document
212 214
         ]);
213 215
     }
214 216
 
215
-    public static function getNextDocumentNumber(): string
217
+    public static function getNextDocumentNumber(?Company $company = null): string
216 218
     {
217
-        $company = auth()->user()->currentCompany;
219
+        $company ??= auth()->user()?->currentCompany;
218 220
 
219 221
         if (! $company) {
220 222
             throw new \RuntimeException('No current company is set for the user.');
221 223
         }
222 224
 
223
-        $defaultEstimateSettings = $company->defaultInvoice;
225
+        $defaultEstimateSettings = $company->defaultEstimate;
224 226
 
225
-        $numberPrefix = 'EST-';
226
-        $numberDigits = $defaultEstimateSettings->number_digits;
227
+        $numberPrefix = $defaultEstimateSettings->number_prefix ?? '';
227 228
 
228 229
         $latestDocument = static::query()
229 230
             ->whereNotNull('estimate_number')
@@ -232,15 +233,12 @@ class Estimate extends Document
232 233
 
233 234
         $lastNumberNumericPart = $latestDocument
234 235
             ? (int) substr($latestDocument->estimate_number, strlen($numberPrefix))
235
-            : 0;
236
+            : DocumentDefault::getBaseNumber();
236 237
 
237 238
         $numberNext = $lastNumberNumericPart + 1;
238 239
 
239 240
         return $defaultEstimateSettings->getNumberNext(
240
-            padded: true,
241
-            format: true,
242 241
             prefix: $numberPrefix,
243
-            digits: $numberDigits,
244 242
             next: $numberNext
245 243
         );
246 244
     }

+ 4
- 7
app/Models/Accounting/Invoice.php Visa fil

@@ -15,6 +15,7 @@ use App\Filament\Company\Resources\Sales\InvoiceResource;
15 15
 use App\Models\Banking\BankAccount;
16 16
 use App\Models\Common\Client;
17 17
 use App\Models\Company;
18
+use App\Models\Setting\DocumentDefault;
18 19
 use App\Observers\InvoiceObserver;
19 20
 use App\Utilities\Currency\CurrencyAccessor;
20 21
 use App\Utilities\Currency\CurrencyConverter;
@@ -140,7 +141,7 @@ class Invoice extends Document
140 141
         });
141 142
     }
142 143
 
143
-    public function documentType(): DocumentType
144
+    public static function documentType(): DocumentType
144 145
     {
145 146
         return DocumentType::Invoice;
146 147
     }
@@ -268,8 +269,7 @@ class Invoice extends Document
268 269
 
269 270
         $defaultInvoiceSettings = $company->defaultInvoice;
270 271
 
271
-        $numberPrefix = $defaultInvoiceSettings->number_prefix;
272
-        $numberDigits = $defaultInvoiceSettings->number_digits;
272
+        $numberPrefix = $defaultInvoiceSettings->number_prefix ?? '';
273 273
 
274 274
         $latestDocument = static::query()
275 275
             ->whereNotNull('invoice_number')
@@ -278,15 +278,12 @@ class Invoice extends Document
278 278
 
279 279
         $lastNumberNumericPart = $latestDocument
280 280
             ? (int) substr($latestDocument->invoice_number, strlen($numberPrefix))
281
-            : 0;
281
+            : DocumentDefault::getBaseNumber();
282 282
 
283 283
         $numberNext = $lastNumberNumericPart + 1;
284 284
 
285 285
         return $defaultInvoiceSettings->getNumberNext(
286
-            padded: true,
287
-            format: true,
288 286
             prefix: $numberPrefix,
289
-            digits: $numberDigits,
290 287
             next: $numberNext
291 288
         );
292 289
     }

+ 1
- 1
app/Models/Accounting/RecurringInvoice.php Visa fil

@@ -120,7 +120,7 @@ class RecurringInvoice extends Document
120 120
         return $this->hasMany(Invoice::class, 'recurring_invoice_id');
121 121
     }
122 122
 
123
-    public function documentType(): DocumentType
123
+    public static function documentType(): DocumentType
124 124
     {
125 125
         return DocumentType::RecurringInvoice;
126 126
     }

+ 18
- 7
app/Models/Company.php Visa fil

@@ -2,7 +2,7 @@
2 2
 
3 3
 namespace App\Models;
4 4
 
5
-use App\Enums\Setting\DocumentType;
5
+use App\Enums\Accounting\DocumentType;
6 6
 use App\Models\Accounting\AccountSubtype;
7 7
 use App\Models\Banking\BankAccount;
8 8
 use App\Models\Banking\ConnectedBankAccount;
@@ -10,7 +10,6 @@ use App\Models\Common\Client;
10 10
 use App\Models\Common\Contact;
11 11
 use App\Models\Common\Offering;
12 12
 use App\Models\Core\Department;
13
-use App\Models\Setting\Appearance;
14 13
 use App\Models\Setting\CompanyDefault;
15 14
 use App\Models\Setting\CompanyProfile;
16 15
 use App\Models\Setting\Currency;
@@ -94,11 +93,6 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
94 93
         return $this->hasMany(Accounting\Bill::class, 'company_id');
95 94
     }
96 95
 
97
-    public function appearance(): HasOne
98
-    {
99
-        return $this->hasOne(Appearance::class, 'company_id');
100
-    }
101
-
102 96
     public function accountSubtypes(): HasMany
103 97
     {
104 98
         return $this->hasMany(AccountSubtype::class, 'company_id');
@@ -125,18 +119,35 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
125 119
         return $this->hasOne(CompanyDefault::class, 'company_id');
126 120
     }
127 121
 
122
+    public function documentDefaults(): HasMany
123
+    {
124
+        return $this->hasMany(DocumentDefault::class, 'company_id');
125
+    }
126
+
128 127
     public function defaultBill(): HasOne
129 128
     {
130 129
         return $this->hasOne(DocumentDefault::class, 'company_id')
131 130
             ->where('type', DocumentType::Bill);
132 131
     }
133 132
 
133
+    public function defaultEstimate(): HasOne
134
+    {
135
+        return $this->hasOne(DocumentDefault::class, 'company_id')
136
+            ->where('type', DocumentType::Estimate);
137
+    }
138
+
134 139
     public function defaultInvoice(): HasOne
135 140
     {
136 141
         return $this->hasOne(DocumentDefault::class, 'company_id')
137 142
             ->where('type', DocumentType::Invoice);
138 143
     }
139 144
 
145
+    public function defaultRecurringInvoice(): HasOne
146
+    {
147
+        return $this->hasOne(DocumentDefault::class, 'company_id')
148
+            ->where('type', DocumentType::RecurringInvoice);
149
+    }
150
+
140 151
     public function departments(): HasMany
141 152
     {
142 153
         return $this->hasMany(Department::class, 'company_id');

+ 0
- 39
app/Models/Setting/Appearance.php Visa fil

@@ -1,39 +0,0 @@
1
-<?php
2
-
3
-namespace App\Models\Setting;
4
-
5
-use App\Concerns\Blamable;
6
-use App\Concerns\CompanyOwned;
7
-use App\Enums\Setting\Font;
8
-use App\Enums\Setting\PrimaryColor;
9
-use Database\Factories\Setting\AppearanceFactory;
10
-use Illuminate\Database\Eloquent\Factories\Factory;
11
-use Illuminate\Database\Eloquent\Factories\HasFactory;
12
-use Illuminate\Database\Eloquent\Model;
13
-
14
-class Appearance extends Model
15
-{
16
-    use Blamable;
17
-    use CompanyOwned;
18
-    use HasFactory;
19
-
20
-    protected $table = 'appearances';
21
-
22
-    protected $fillable = [
23
-        'company_id',
24
-        'primary_color',
25
-        'font',
26
-        'created_by',
27
-        'updated_by',
28
-    ];
29
-
30
-    protected $casts = [
31
-        'primary_color' => PrimaryColor::class,
32
-        'font' => Font::class,
33
-    ];
34
-
35
-    protected static function newFactory(): Factory
36
-    {
37
-        return AppearanceFactory::new();
38
-    }
39
-}

+ 15
- 30
app/Models/Setting/DocumentDefault.php Visa fil

@@ -2,10 +2,9 @@
2 2
 
3 3
 namespace App\Models\Setting;
4 4
 
5
-use App\Casts\TrimLeadingZeroCast;
6 5
 use App\Concerns\Blamable;
7 6
 use App\Concerns\CompanyOwned;
8
-use App\Enums\Setting\DocumentType;
7
+use App\Enums\Accounting\DocumentType;
9 8
 use App\Enums\Setting\Font;
10 9
 use App\Enums\Setting\PaymentTerms;
11 10
 use App\Enums\Setting\Template;
@@ -32,8 +31,6 @@ class DocumentDefault extends Model
32 31
         'logo',
33 32
         'show_logo',
34 33
         'number_prefix',
35
-        'number_digits',
36
-        'number_next',
37 34
         'payment_terms',
38 35
         'header',
39 36
         'subheader',
@@ -51,8 +48,8 @@ class DocumentDefault extends Model
51 48
     ];
52 49
 
53 50
     protected $casts = [
51
+        'type' => DocumentType::class,
54 52
         'show_logo' => 'boolean',
55
-        'number_next' => TrimLeadingZeroCast::class,
56 53
         'payment_terms' => PaymentTerms::class,
57 54
         'font' => Font::class,
58 55
         'template' => Template::class,
@@ -80,47 +77,35 @@ class DocumentDefault extends Model
80 77
 
81 78
     public function scopeInvoice(Builder $query): Builder
82 79
     {
83
-        return $query->scopes(['type' => [DocumentType::Invoice]]);
80
+        return $query->type(DocumentType::Invoice);
84 81
     }
85 82
 
86
-    public function scopeBill(Builder $query): Builder
83
+    public function scopeRecurringInvoice(Builder $query): Builder
87 84
     {
88
-        return $query->scopes(['type' => [DocumentType::Bill]]);
85
+        return $query->type(DocumentType::RecurringInvoice);
89 86
     }
90 87
 
91
-    public static function availableNumberDigits(): array
88
+    public function scopeBill(Builder $query): Builder
92 89
     {
93
-        return array_combine(range(1, 20), range(1, 20));
90
+        return $query->type(DocumentType::Bill);
94 91
     }
95 92
 
96
-    public function getNumberNext(?bool $padded = null, ?bool $format = null, ?string $prefix = null, int | string | null $digits = null, int | string | null $next = null): string
93
+    public function scopeEstimate(Builder $query): Builder
97 94
     {
98
-        [$number_prefix, $number_digits, $number_next] = $this->initializeAttributes($prefix, $digits, $next);
99
-
100
-        return match (true) {
101
-            $format && $padded => $number_prefix . $this->getPaddedNumberNext($number_next, $number_digits),
102
-            $format => $number_prefix . $number_next,
103
-            $padded => $this->getPaddedNumberNext($number_next, $number_digits),
104
-            default => $number_next,
105
-        };
95
+        return $query->type(DocumentType::Estimate);
106 96
     }
107 97
 
108
-    public function initializeAttributes(?string $prefix, int | string | null $digits, int | string | null $next): array
98
+    public function getNumberNext(?string $prefix = null, int | string | null $next = null): string
109 99
     {
110
-        $number_prefix = $prefix ?? $this->number_prefix;
111
-        $number_digits = $digits ?? $this->number_digits;
112
-        $number_next = $next ?? $this->number_next;
100
+        $numberPrefix = $prefix ?? $this->number_prefix ?? '';
101
+        $numberNext = (string) ($next ?? (static::getBaseNumber() + 1));
113 102
 
114
-        return [$number_prefix, $number_digits, $number_next];
103
+        return $numberPrefix . $numberNext;
115 104
     }
116 105
 
117
-    /**
118
-     * Get the next number with padding for dynamic display purposes.
119
-     * Even if number_next is a string, it will be cast to an integer.
120
-     */
121
-    public function getPaddedNumberNext(int | string | null $number_next, int | string | null $number_digits): string
106
+    public static function getBaseNumber(): int
122 107
     {
123
-        return str_pad($number_next, $number_digits, '0', STR_PAD_LEFT);
108
+        return 1000;
124 109
     }
125 110
 
126 111
     public static function getAvailableItemNameOptions(): array

+ 7
- 0
app/Models/User.php Visa fil

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Models;
4 4
 
5
+use App\Models\Common\Contact;
5 6
 use App\Models\Core\Department;
6 7
 use Filament\Models\Contracts\FilamentUser;
7 8
 use Filament\Models\Contracts\HasAvatar;
@@ -11,6 +12,7 @@ use Filament\Panel;
11 12
 use Illuminate\Database\Eloquent\Factories\HasFactory;
12 13
 use Illuminate\Database\Eloquent\Model;
13 14
 use Illuminate\Database\Eloquent\Relations\HasMany;
15
+use Illuminate\Database\Eloquent\Relations\MorphMany;
14 16
 use Illuminate\Foundation\Auth\User as Authenticatable;
15 17
 use Illuminate\Notifications\Notifiable;
16 18
 use Illuminate\Support\Collection;
@@ -92,6 +94,11 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasDefaul
92 94
         return $this->profile_photo_url;
93 95
     }
94 96
 
97
+    public function contacts(): MorphMany
98
+    {
99
+        return $this->morphMany(Contact::class, 'contactable');
100
+    }
101
+
95 102
     public function managerOf(): HasMany
96 103
     {
97 104
         return $this->hasMany(Department::class, 'manager_id');

+ 4
- 2
app/Providers/AppServiceProvider.php Visa fil

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Providers;
4 4
 
5
+use App\Http\Responses\LoginRedirectResponse;
5 6
 use App\Services\DateRangeService;
6 7
 use Filament\Http\Responses\Auth\Contracts\LoginResponse;
7 8
 use Filament\Notifications\Livewire\Notifications;
@@ -18,7 +19,7 @@ class AppServiceProvider extends ServiceProvider
18 19
     public function register(): void
19 20
     {
20 21
         $this->app->singleton(DateRangeService::class);
21
-        $this->app->singleton(LoginResponse::class, \App\Http\Responses\LoginResponse::class);
22
+        $this->app->singleton(LoginResponse::class, LoginRedirectResponse::class);
22 23
     }
23 24
 
24 25
     /**
@@ -29,7 +30,8 @@ class AppServiceProvider extends ServiceProvider
29 30
         Notifications::alignment(Alignment::Center);
30 31
 
31 32
         FilamentAsset::register([
32
-            Js::make('TopNavigation', __DIR__ . '/../../resources/js/TopNavigation.js'),
33
+            Js::make('top-navigation', __DIR__ . '/../../resources/js/top-navigation.js'),
34
+            Js::make('history-fix', __DIR__ . '/../../resources/js/history-fix.js'),
33 35
         ]);
34 36
     }
35 37
 }

+ 1
- 0
app/Providers/Filament/CompanyPanelProvider.php Visa fil

@@ -99,6 +99,7 @@ class CompanyPanelProvider extends PanelProvider
99 99
                     ->profilePhotos()
100 100
                     ->api()
101 101
                     ->companies(invitations: true)
102
+                    ->autoAcceptInvitations()
102 103
                     ->termsAndPrivacyPolicy()
103 104
                     ->notifications()
104 105
                     ->modals()

+ 12
- 1
app/Providers/Filament/UserPanelProvider.php Visa fil

@@ -6,6 +6,7 @@ use App\Filament\Components\PanelShiftDropdown;
6 6
 use App\Filament\User\Clusters\Account;
7 7
 use App\Http\Middleware\Authenticate;
8 8
 use Exception;
9
+use Filament\Facades\Filament;
9 10
 use Filament\Http\Middleware\DisableBladeIconComponents;
10 11
 use Filament\Http\Middleware\DispatchServingFilamentEvent;
11 12
 use Filament\Navigation\NavigationBuilder;
@@ -20,7 +21,9 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
20 21
 use Illuminate\Routing\Middleware\SubstituteBindings;
21 22
 use Illuminate\Session\Middleware\AuthenticateSession;
22 23
 use Illuminate\Session\Middleware\StartSession;
24
+use Illuminate\Support\Facades\Auth;
23 25
 use Illuminate\View\Middleware\ShareErrorsFromSession;
26
+use Wallo\FilamentCompanies\FilamentCompanies;
24 27
 use Wallo\FilamentCompanies\Pages\User\PersonalAccessTokens;
25 28
 use Wallo\FilamentCompanies\Pages\User\Profile;
26 29
 
@@ -45,7 +48,15 @@ class UserPanelProvider extends PanelProvider
45 48
                                 NavigationItem::make('company')
46 49
                                     ->label('Company Dashboard')
47 50
                                     ->icon('heroicon-s-building-office-2')
48
-                                    ->url(static fn (): string => Pages\Dashboard::getUrl(panel: 'company', tenant: auth()->user()->personalCompany())),
51
+                                    ->url(static function (): ?string {
52
+                                        $user = Auth::user();
53
+
54
+                                        if ($company = $user?->primaryCompany()) {
55
+                                            return Pages\Dashboard::getUrl(panel: FilamentCompanies::getCompanyPanel(), tenant: $company);
56
+                                        }
57
+
58
+                                        return Filament::getPanel(FilamentCompanies::getCompanyPanel())->getTenantRegistrationUrl();
59
+                                    }),
49 60
                             ]);
50 61
                     }),
51 62
             )

+ 1
- 17
app/Services/CompanySettingsService.php Visa fil

@@ -3,8 +3,6 @@
3 3
 namespace App\Services;
4 4
 
5 5
 use App\Enums\Setting\DateFormat;
6
-use App\Enums\Setting\Font;
7
-use App\Enums\Setting\PrimaryColor;
8 6
 use App\Enums\Setting\WeekStart;
9 7
 use App\Models\Company;
10 8
 use Illuminate\Support\Facades\Cache;
@@ -16,7 +14,7 @@ class CompanySettingsService
16 14
         $cacheKey = "company_settings_{$companyId}";
17 15
 
18 16
         return Cache::rememberForever($cacheKey, function () use ($companyId) {
19
-            $company = Company::with(['locale', 'appearance'])->find($companyId);
17
+            $company = Company::with(['locale'])->find($companyId);
20 18
 
21 19
             if (! $company) {
22 20
                 return self::getDefaultSettings();
@@ -26,8 +24,6 @@ class CompanySettingsService
26 24
                 'default_language' => $company->locale->language ?? config('transmatic.source_locale'),
27 25
                 'default_timezone' => $company->locale->timezone ?? config('app.timezone'),
28 26
                 'default_currency' => $company->currency_code ?? 'USD',
29
-                'default_primary_color' => $company->appearance->primary_color->value ?? PrimaryColor::DEFAULT,
30
-                'default_font' => $company->appearance->font->value ?? Font::DEFAULT,
31 27
                 'default_date_format' => $company->locale->date_format->value ?? DateFormat::DEFAULT,
32 28
                 'default_week_start' => $company->locale->week_start->value ?? WeekStart::DEFAULT,
33 29
             ];
@@ -46,8 +42,6 @@ class CompanySettingsService
46 42
             'default_language' => config('transmatic.source_locale'),
47 43
             'default_timezone' => config('app.timezone'),
48 44
             'default_currency' => 'USD',
49
-            'default_primary_color' => PrimaryColor::DEFAULT,
50
-            'default_font' => Font::DEFAULT,
51 45
             'default_date_format' => DateFormat::DEFAULT,
52 46
             'default_week_start' => WeekStart::DEFAULT,
53 47
         ];
@@ -75,16 +69,6 @@ class CompanySettingsService
75 69
         return self::getSpecificSetting($companyId, 'default_currency', 'USD');
76 70
     }
77 71
 
78
-    public static function getDefaultPrimaryColor(int $companyId): string
79
-    {
80
-        return self::getSpecificSetting($companyId, 'default_primary_color', PrimaryColor::DEFAULT);
81
-    }
82
-
83
-    public static function getDefaultFont(int $companyId): string
84
-    {
85
-        return self::getSpecificSetting($companyId, 'default_font', Font::DEFAULT);
86
-    }
87
-
88 72
     public static function getDefaultDateFormat(int $companyId): string
89 73
     {
90 74
         return self::getSpecificSetting($companyId, 'default_date_format', DateFormat::DEFAULT);

+ 0
- 1
bootstrap/providers.php Visa fil

@@ -3,7 +3,6 @@
3 3
 return [
4 4
     App\Providers\AppServiceProvider::class,
5 5
     App\Providers\AuthServiceProvider::class,
6
-    App\Providers\Filament\AdminPanelProvider::class,
7 6
     App\Providers\Filament\CompanyPanelProvider::class,
8 7
     App\Providers\Filament\UserPanelProvider::class,
9 8
     App\Providers\Faker\FakerServiceProvider::class,

+ 423
- 422
composer.lock
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 6
- 5
database/factories/Accounting/BillFactory.php Visa fil

@@ -8,6 +8,7 @@ use App\Models\Accounting\Bill;
8 8
 use App\Models\Accounting\DocumentLineItem;
9 9
 use App\Models\Banking\BankAccount;
10 10
 use App\Models\Common\Vendor;
11
+use App\Models\Setting\DocumentDefault;
11 12
 use App\Utilities\Currency\CurrencyConverter;
12 13
 use Illuminate\Database\Eloquent\Factories\Factory;
13 14
 use Illuminate\Support\Carbon;
@@ -42,8 +43,8 @@ class BillFactory extends Factory
42 43
         return [
43 44
             'company_id' => 1,
44 45
             'vendor_id' => Vendor::inRandomOrder()->value('id'),
45
-            'bill_number' => $this->faker->unique()->numerify('BILL-#####'),
46
-            'order_number' => $this->faker->unique()->numerify('PO-#####'),
46
+            'bill_number' => $this->faker->unique()->numerify('BILL-####'),
47
+            'order_number' => $this->faker->unique()->numerify('PO-####'),
47 48
             'date' => $billDate,
48 49
             'due_date' => Carbon::parse($billDate)->addDays($dueDays),
49 50
             'status' => BillStatus::Open,
@@ -179,11 +180,11 @@ class BillFactory extends Factory
179 180
         return $this->afterCreating(function (Bill $bill) {
180 181
             $this->ensureInitialized($bill);
181 182
 
182
-            $paddedId = str_pad((string) $bill->id, 5, '0', STR_PAD_LEFT);
183
+            $number = DocumentDefault::getBaseNumber() + $bill->id;
183 184
 
184 185
             $bill->updateQuietly([
185
-                'bill_number' => "BILL-{$paddedId}",
186
-                'order_number' => "PO-{$paddedId}",
186
+                'bill_number' => "BILL-{$number}",
187
+                'order_number' => "PO-{$number}",
187 188
             ]);
188 189
 
189 190
             if ($bill->wasInitialized() && $bill->is_currently_overdue) {

+ 6
- 5
database/factories/Accounting/EstimateFactory.php Visa fil

@@ -6,6 +6,7 @@ use App\Enums\Accounting\EstimateStatus;
6 6
 use App\Models\Accounting\DocumentLineItem;
7 7
 use App\Models\Accounting\Estimate;
8 8
 use App\Models\Common\Client;
9
+use App\Models\Setting\DocumentDefault;
9 10
 use Illuminate\Database\Eloquent\Factories\Factory;
10 11
 use Illuminate\Support\Carbon;
11 12
 
@@ -33,8 +34,8 @@ class EstimateFactory extends Factory
33 34
             'client_id' => Client::inRandomOrder()->value('id'),
34 35
             'header' => 'Estimate',
35 36
             'subheader' => 'Estimate',
36
-            'estimate_number' => $this->faker->unique()->numerify('EST-#####'),
37
-            'reference_number' => $this->faker->unique()->numerify('REF-#####'),
37
+            'estimate_number' => $this->faker->unique()->numerify('EST-####'),
38
+            'reference_number' => $this->faker->unique()->numerify('REF-####'),
38 39
             'date' => $estimateDate,
39 40
             'expiration_date' => Carbon::parse($estimateDate)->addDays($this->faker->numberBetween(14, 30)),
40 41
             'status' => EstimateStatus::Draft,
@@ -152,11 +153,11 @@ class EstimateFactory extends Factory
152 153
         return $this->afterCreating(function (Estimate $estimate) {
153 154
             $this->ensureLineItems($estimate);
154 155
 
155
-            $paddedId = str_pad((string) $estimate->id, 5, '0', STR_PAD_LEFT);
156
+            $number = DocumentDefault::getBaseNumber() + $estimate->id;
156 157
 
157 158
             $estimate->updateQuietly([
158
-                'estimate_number' => "EST-{$paddedId}",
159
-                'reference_number' => "REF-{$paddedId}",
159
+                'estimate_number' => "EST-{$number}",
160
+                'reference_number' => "REF-{$number}",
160 161
             ]);
161 162
 
162 163
             if ($estimate->wasApproved() && $estimate->is_currently_expired) {

+ 6
- 5
database/factories/Accounting/InvoiceFactory.php Visa fil

@@ -8,6 +8,7 @@ use App\Models\Accounting\DocumentLineItem;
8 8
 use App\Models\Accounting\Invoice;
9 9
 use App\Models\Banking\BankAccount;
10 10
 use App\Models\Common\Client;
11
+use App\Models\Setting\DocumentDefault;
11 12
 use App\Utilities\Currency\CurrencyConverter;
12 13
 use Illuminate\Database\Eloquent\Factories\Factory;
13 14
 use Illuminate\Support\Carbon;
@@ -36,8 +37,8 @@ class InvoiceFactory extends Factory
36 37
             'client_id' => Client::inRandomOrder()->value('id'),
37 38
             'header' => 'Invoice',
38 39
             'subheader' => 'Invoice',
39
-            'invoice_number' => $this->faker->unique()->numerify('INV-#####'),
40
-            'order_number' => $this->faker->unique()->numerify('ORD-#####'),
40
+            'invoice_number' => $this->faker->unique()->numerify('INV-####'),
41
+            'order_number' => $this->faker->unique()->numerify('ORD-####'),
41 42
             'date' => $invoiceDate,
42 43
             'due_date' => Carbon::parse($invoiceDate)->addDays($this->faker->numberBetween(14, 60)),
43 44
             'status' => InvoiceStatus::Draft,
@@ -197,11 +198,11 @@ class InvoiceFactory extends Factory
197 198
         return $this->afterCreating(function (Invoice $invoice) {
198 199
             $this->ensureLineItems($invoice);
199 200
 
200
-            $paddedId = str_pad((string) $invoice->id, 5, '0', STR_PAD_LEFT);
201
+            $number = DocumentDefault::getBaseNumber() + $invoice->id;
201 202
 
202 203
             $invoice->updateQuietly([
203
-                'invoice_number' => "INV-{$paddedId}",
204
-                'order_number' => "ORD-{$paddedId}",
204
+                'invoice_number' => "INV-{$number}",
205
+                'order_number' => "ORD-{$number}",
205 206
             ]);
206 207
 
207 208
             if ($invoice->wasApproved() && $invoice->is_currently_overdue) {

+ 1
- 1
database/factories/Accounting/RecurringInvoiceFactory.php Visa fil

@@ -38,7 +38,7 @@ class RecurringInvoiceFactory extends Factory
38 38
             'client_id' => Client::inRandomOrder()->value('id'),
39 39
             'header' => 'Invoice',
40 40
             'subheader' => 'Invoice',
41
-            'order_number' => $this->faker->unique()->numerify('ORD-#####'),
41
+            'order_number' => $this->faker->unique()->numerify('ORD-####'),
42 42
             'payment_terms' => PaymentTerms::Net30,
43 43
             'status' => RecurringInvoiceStatus::Draft,
44 44
             'currency_code' => 'USD',

+ 0
- 24
database/factories/Setting/AppearanceFactory.php Visa fil

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

+ 4
- 9
database/factories/Setting/CompanyDefaultFactory.php Visa fil

@@ -4,7 +4,6 @@ namespace Database\Factories\Setting;
4 4
 
5 5
 use App\Faker\CurrencyCode;
6 6
 use App\Models\Company;
7
-use App\Models\Setting\Appearance;
8 7
 use App\Models\Setting\CompanyDefault;
9 8
 use App\Models\Setting\Currency;
10 9
 use App\Models\Setting\DocumentDefault;
@@ -44,7 +43,6 @@ class CompanyDefaultFactory extends Factory
44 43
         }
45 44
 
46 45
         $currency = $this->createCurrency($company, $user, $currencyCode);
47
-        $this->createAppearance($company, $user);
48 46
         $this->createDocumentDefaults($company, $user);
49 47
         $this->createLocalization($company, $user, $countryCode, $language);
50 48
 
@@ -68,24 +66,21 @@ class CompanyDefaultFactory extends Factory
68 66
         ]);
69 67
     }
70 68
 
71
-    private function createAppearance(Company $company, User $user): void
69
+    private function createDocumentDefaults(Company $company, User $user): void
72 70
     {
73
-        Appearance::factory()->createQuietly([
71
+        DocumentDefault::factory()->invoice()->createQuietly([
74 72
             'company_id' => $company->id,
75 73
             'created_by' => $user->id,
76 74
             'updated_by' => $user->id,
77 75
         ]);
78
-    }
79 76
 
80
-    private function createDocumentDefaults(Company $company, User $user): void
81
-    {
82
-        DocumentDefault::factory()->invoice()->createQuietly([
77
+        DocumentDefault::factory()->bill()->createQuietly([
83 78
             'company_id' => $company->id,
84 79
             'created_by' => $user->id,
85 80
             'updated_by' => $user->id,
86 81
         ]);
87 82
 
88
-        DocumentDefault::factory()->bill()->createQuietly([
83
+        DocumentDefault::factory()->estimate()->createQuietly([
89 84
             'company_id' => $company->id,
90 85
             'created_by' => $user->id,
91 86
             'updated_by' => $user->id,

+ 31
- 9
database/factories/Setting/DocumentDefaultFactory.php Visa fil

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace Database\Factories\Setting;
4 4
 
5
-use App\Enums\Setting\DocumentType;
5
+use App\Enums\Accounting\DocumentType;
6
+use App\Enums\Setting\Font;
7
+use App\Enums\Setting\Template;
6 8
 use App\Models\Setting\DocumentDefault;
7 9
 use Illuminate\Database\Eloquent\Factories\Factory;
8 10
 
@@ -26,24 +28,36 @@ class DocumentDefaultFactory extends Factory
26 28
     public function definition(): array
27 29
     {
28 30
         return [
29
-            //
31
+            'company_id' => 1,
32
+            'payment_terms' => 'due_upon_receipt',
30 33
         ];
31 34
     }
32 35
 
33 36
     /**
34 37
      * The model's common default state.
35 38
      */
36
-    private function baseState(DocumentType $type, string $prefix, string $header): array
39
+    private function baseState(DocumentType $type): array
37 40
     {
38
-        return [
39
-            'type' => $type->value,
40
-            'number_prefix' => $prefix,
41
-            'header' => $header,
41
+        $state = [
42
+            'type' => $type,
43
+            'number_prefix' => $type->getDefaultPrefix(),
42 44
             'item_name' => ['option' => 'items', 'custom' => null],
43 45
             'unit_name' => ['option' => 'quantity', 'custom' => null],
44 46
             'price_name' => ['option' => 'price', 'custom' => null],
45 47
             'amount_name' => ['option' => 'amount', 'custom' => null],
46 48
         ];
49
+
50
+        if ($type !== DocumentType::Bill) {
51
+            $state = [...$state,
52
+                'header' => $type->getLabel(),
53
+                'show_logo' => false,
54
+                'accent_color' => '#4F46E5',
55
+                'font' => Font::Inter,
56
+                'template' => Template::Default,
57
+            ];
58
+        }
59
+
60
+        return $state;
47 61
     }
48 62
 
49 63
     /**
@@ -51,7 +65,7 @@ class DocumentDefaultFactory extends Factory
51 65
      */
52 66
     public function invoice(): self
53 67
     {
54
-        return $this->state($this->baseState(DocumentType::Invoice, 'INV-', 'Invoice'));
68
+        return $this->state($this->baseState(DocumentType::Invoice));
55 69
     }
56 70
 
57 71
     /**
@@ -59,6 +73,14 @@ class DocumentDefaultFactory extends Factory
59 73
      */
60 74
     public function bill(): self
61 75
     {
62
-        return $this->state($this->baseState(DocumentType::Bill, 'BILL-', 'Bill'));
76
+        return $this->state($this->baseState(DocumentType::Bill));
77
+    }
78
+
79
+    /**
80
+     * Indicate that the model's type is estimate.
81
+     */
82
+    public function estimate(): self
83
+    {
84
+        return $this->state($this->baseState(DocumentType::Estimate));
63 85
     }
64 86
 }

+ 0
- 32
database/migrations/2023_09_12_014413_create_appearances_table.php Visa fil

@@ -1,32 +0,0 @@
1
-<?php
2
-
3
-use Illuminate\Database\Migrations\Migration;
4
-use Illuminate\Database\Schema\Blueprint;
5
-use Illuminate\Support\Facades\Schema;
6
-
7
-return new class extends Migration
8
-{
9
-    /**
10
-     * Run the migrations.
11
-     */
12
-    public function up(): void
13
-    {
14
-        Schema::create('appearances', function (Blueprint $table) {
15
-            $table->id();
16
-            $table->foreignId('company_id')->constrained()->onDelete('cascade');
17
-            $table->string('primary_color')->default('indigo');
18
-            $table->string('font')->default('inter');
19
-            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
20
-            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
21
-            $table->timestamps();
22
-        });
23
-    }
24
-
25
-    /**
26
-     * Reverse the migrations.
27
-     */
28
-    public function down(): void
29
-    {
30
-        Schema::dropIfExists('appearances');
31
-    }
32
-};

+ 3
- 5
database/migrations/2023_09_12_032057_create_document_defaults_table.php Visa fil

@@ -18,16 +18,14 @@ return new class extends Migration
18 18
             $table->string('logo')->nullable();
19 19
             $table->boolean('show_logo')->default(false);
20 20
             $table->string('number_prefix')->nullable();
21
-            $table->unsignedTinyInteger('number_digits')->default(5);
22
-            $table->unsignedBigInteger('number_next')->default(1);
23 21
             $table->string('payment_terms')->default('due_upon_receipt');
24 22
             $table->string('header')->nullable();
25 23
             $table->string('subheader')->nullable();
26 24
             $table->text('terms')->nullable();
27 25
             $table->text('footer')->nullable();
28
-            $table->string('accent_color')->default('#4F46E5');
29
-            $table->string('font')->default('inter');
30
-            $table->string('template')->default('default');
26
+            $table->string('accent_color')->nullable();
27
+            $table->string('font')->nullable();
28
+            $table->string('template')->nullable();
31 29
             $table->json('item_name')->nullable();
32 30
             $table->json('unit_name')->nullable();
33 31
             $table->json('price_name')->nullable();

+ 389
- 219
package-lock.json
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 4
- 2
resources/data/lang/en.json Visa fil

@@ -195,5 +195,7 @@
195 195
     "Frequency": "Frequency",
196 196
     "Dates & Time": "Dates & Time",
197 197
     "account": "account",
198
-    "currency": "currency"
199
-}
198
+    "currency": "currency",
199
+    "Estimate": "Estimate",
200
+    "Column Labels": "Column Labels"
201
+}

+ 10
- 0
resources/js/history-fix.js Visa fil

@@ -0,0 +1,10 @@
1
+const original = window.history.replaceState;
2
+let previousState = null;
3
+window.history.replaceState = function (state, unused, url) {
4
+    state.url = url instanceof URL ? url.toString() : url;
5
+    if (JSON.stringify(state) === JSON.stringify(previousState)) {
6
+        return;
7
+    }
8
+    original.apply(this, [state, unused, url]);
9
+    previousState = state;
10
+};

resources/js/TopNavigation.js → resources/js/top-navigation.js Visa fil


+ 4
- 1
resources/views/components/company/invoice/container.blade.php Visa fil

@@ -6,9 +6,12 @@
6 6
     <div
7 7
         @class([
8 8
             'inv-paper bg-[#ffffff] dark:bg-gray-800 rounded-sm shadow-xl',
9
-            'w-full max-w-[820px] min-h-[1024px]' => $preview === false,
9
+            'w-full max-w-[820px] max-h-[1024px] overflow-y-auto' => $preview === false,
10 10
             'w-[38.25rem] h-[49.5rem] overflow-hidden' => $preview === true,
11 11
         ])
12
+        @style([
13
+            'scrollbar-width: thin;' => $preview === false,
14
+        ])
12 15
     >
13 16
         {{ $slot }}
14 17
     </div>

+ 19
- 0
resources/views/components/icons/document-header-decoration.blade.php Visa fil

@@ -0,0 +1,19 @@
1
+@props([
2
+   'color' => 'currentColor',
3
+   'text' => '',
4
+])
5
+
6
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 72" aria-hidden="true" fill="none" {{ $attributes }}>
7
+   <g stroke="{{ $color }}" stroke-width="2">
8
+       <path d="M20 57.038v-42.076c.33.025.664.038 1 .038 7.18 0 13-5.82 13-13 0-.336-.013-.67-.038-1h172.076c-.025.33-.038.664-.038 1 0 7.18 5.82 13 13 13 .336 0 .67-.013 1-.038v42.076c-.33-.025-.664-.038-1-.038-7.18 0-13 5.82-13 13 0 .336.013.67.038 1h-172.076c.025-.33.038-.664.038-1 0-7.18-5.82-13-13-13-.336 0-.67.013-1 .038z" />
9
+       <path d="M26 51.503v-31.007c.33.024.664.037 1 .037 7.18 0 13-5.626 13-12.567 0-.325.013-.648-.038-.967h160.076c-.025.319-.038.641-.038.967 0 6.94 5.82 12.567 13 12.567.336 0 .67-.012 1-.037v31.007c-.33-.024-.664-.037-1-.037-7.18 0-13 5.626-13 12.567 0 .325.013.648.038.967h-160.076c.025-.319.038-.641.038-.967 0-6.94-5.82-12.567-13-12.567-.336 0-.67.012-1 .037z" />
10
+   </g>
11
+   <text
12
+       x="50%"
13
+       y="50%"
14
+       text-anchor="middle"
15
+       dominant-baseline="middle"
16
+       class="text-3xl font-light tracking-tight"
17
+       fill="{{ $color }}"
18
+   >{{ $text }}</text>
19
+</svg>

+ 5
- 10
resources/views/filament/company/components/invoice-layouts/classic.blade.php Visa fil

@@ -33,16 +33,11 @@
33 33
     <x-company.invoice.metadata class="classic-template-metadata">
34 34
         <div class="items-center flex">
35 35
             <hr class="grow-[2] py-0.5 border-solid border-y-2" style="border-color: {{ $document->accentColor }};">
36
-            <div class="items-center flex mx-5">
37
-                <x-icons.decor-border-left color="{{ $document->accentColor }}"/>
38
-                <div class="px-2.5 border-solid border-y-2 py-1 -mx-3" style="border-color: {{ $document->accentColor }};">
39
-                    <div class="px-2.5 border-solid border-y-2 py-3" style="border-color: {{ $document->accentColor }};">
40
-                        <div class="inline text-2xl font-semibold"
41
-                             style="color: {{ $document->accentColor }};">{{ $document->header }}</div>
42
-                    </div>
43
-                </div>
44
-                <x-icons.decor-border-right color="{{ $document->accentColor }}"/>
45
-            </div>
36
+            <x-icons.document-header-decoration
37
+                color="{{ $document->accentColor }}"
38
+                text="{{ $document->header }}"
39
+                class="w-48"
40
+            />
46 41
             <hr class="grow-[2] py-0.5 border-solid border-y-2" style="border-color: {{ $document->accentColor }};">
47 42
         </div>
48 43
         <div class="mt-2 text-sm text-center text-gray-600 dark:text-gray-400">{{ $document->subheader }}</div>

+ 0
- 17
resources/views/filament/company/pages/setting/appearance.blade.php Visa fil

@@ -1,17 +0,0 @@
1
-<x-filament-panels::page style="margin-bottom: 500px">
2
-    <x-filament-panels::form wire:submit="save">
3
-        {{ $this->form }}
4
-
5
-        <x-filament-panels::form.actions
6
-            :actions="$this->getCachedFormActions()"
7
-            :full-width="$this->hasFullWidthFormActions()"
8
-        />
9
-    </x-filament-panels::form>
10
-</x-filament-panels::page>
11
-<script>
12
-    document.addEventListener('livewire:init', function () {
13
-        Livewire.on('appearanceUpdated', function () {
14
-            window.location.reload();
15
-        });
16
-    });
17
-</script>

+ 0
- 10
resources/views/filament/company/pages/setting/invoice.blade.php Visa fil

@@ -1,10 +0,0 @@
1
-<x-filament-panels::page style="margin-bottom: 500px">
2
-    <x-filament-panels::form wire:submit="save">
3
-        {{ $this->form }}
4
-
5
-        <x-filament-panels::form.actions
6
-            :actions="$this->getCachedFormActions()"
7
-            :full-width="$this->hasFullWidthFormActions()"
8
-        />
9
-    </x-filament-panels::form>
10
-</x-filament-panels::page>

+ 5
- 145
resources/views/filament/infolists/components/document-preview.blade.php Visa fil

@@ -1,5 +1,6 @@
1 1
 @php
2 2
     $document = \App\DTO\DocumentDTO::fromModel($getRecord());
3
+    $template = $getTemplate();
3 4
 @endphp
4 5
 
5 6
 {!! $document->getFontHtml() !!}
@@ -11,149 +12,8 @@
11 12
 </style>
12 13
 
13 14
 <div {{ $attributes }}>
14
-    <x-company.invoice.container class="modern-template-container">
15
-        <!-- Colored Header with Logo -->
16
-        <x-company.invoice.header class="bg-gray-800 h-24">
17
-            <!-- Logo -->
18
-            <div class="w-2/3">
19
-                @if($document->logo && $document->showLogo)
20
-                    <x-company.invoice.logo class="ml-8" :src="$document->logo"/>
21
-                @endif
22
-            </div>
23
-
24
-            <!-- Ribbon Container -->
25
-            <div class="w-1/3 absolute right-0 top-0 p-3 h-32 flex flex-col justify-end rounded-bl-sm"
26
-                 style="background: {{ $document->accentColor }};">
27
-                @if($document->header)
28
-                    <h1 class="text-4xl font-bold text-white text-center uppercase">{{ $document->header }}</h1>
29
-                @endif
30
-            </div>
31
-        </x-company.invoice.header>
32
-
33
-        <!-- Company Details -->
34
-        <x-company.invoice.metadata class="modern-template-metadata space-y-8">
35
-            <div class="text-sm">
36
-                <h2 class="text-lg font-semibold">{{ $document->company->name }}</h2>
37
-                @if($formattedAddress = $document->company->getFormattedAddressHtml())
38
-                    {!! $formattedAddress !!}
39
-                @endif
40
-            </div>
41
-
42
-            <div class="flex justify-between items-end">
43
-                <!-- Billing Details -->
44
-                <div class="text-sm tracking-tight">
45
-                    <h3 class="text-gray-600 dark:text-gray-400 font-medium tracking-tight mb-1">BILL TO</h3>
46
-                    <p class="text-base font-bold"
47
-                       style="color: {{ $document->accentColor }}">{{ $document->client->name }}</p>
48
-
49
-                    @if($formattedAddress = $document->client->getFormattedAddressHtml())
50
-                        {!! $formattedAddress !!}
51
-                    @endif
52
-                </div>
53
-
54
-                <div class="text-sm tracking-tight">
55
-                    <table class="min-w-full">
56
-                        <tbody>
57
-                        <tr>
58
-                            <td class="font-semibold text-right pr-2">{{ $document->label->number }}:</td>
59
-                            <td class="text-left pl-2">{{ $document->number }}</td>
60
-                        </tr>
61
-                        @if($document->referenceNumber)
62
-                            <tr>
63
-                                <td class="font-semibold text-right pr-2">{{ $document->label->referenceNumber }}:</td>
64
-                                <td class="text-left pl-2">{{ $document->referenceNumber }}</td>
65
-                            </tr>
66
-                        @endif
67
-                        <tr>
68
-                            <td class="font-semibold text-right pr-2">{{ $document->label->date }}:</td>
69
-                            <td class="text-left pl-2">{{ $document->date }}</td>
70
-                        </tr>
71
-                        <tr>
72
-                            <td class="font-semibold text-right pr-2">{{ $document->label->dueDate }}:</td>
73
-                            <td class="text-left pl-2">{{ $document->dueDate }}</td>
74
-                        </tr>
75
-                        </tbody>
76
-                    </table>
77
-                </div>
78
-            </div>
79
-        </x-company.invoice.metadata>
80
-
81
-        <!-- Line Items Table -->
82
-        <x-company.invoice.line-items class="modern-template-line-items">
83
-            <table class="w-full text-left table-fixed">
84
-                <thead class="text-sm leading-relaxed">
85
-                <tr class="text-gray-600 dark:text-gray-400">
86
-                    <th class="text-left pl-6 w-[50%] py-4">{{ $document->columnLabel->items }}</th>
87
-                    <th class="text-center w-[10%] py-4">{{ $document->columnLabel->units }}</th>
88
-                    <th class="text-right w-[20%] py-4">{{ $document->columnLabel->price }}</th>
89
-                    <th class="text-right pr-6 w-[20%] py-4">{{ $document->columnLabel->amount }}</th>
90
-                </tr>
91
-                </thead>
92
-                <tbody class="text-sm tracking-tight border-y-2">
93
-                @foreach($document->lineItems as $index => $item)
94
-                    <tr @class(['bg-gray-100 dark:bg-gray-800' => $index % 2 === 0])>
95
-                        <td class="text-left pl-6 font-semibold py-3">
96
-                            {{ $item->name }}
97
-                            @if($item->description)
98
-                                <div class="text-gray-600 font-normal line-clamp-2 mt-1">{{ $item->description }}</div>
99
-                            @endif
100
-                        </td>
101
-                        <td class="text-center py-3">{{ $item->quantity }}</td>
102
-                        <td class="text-right py-3">{{ $item->unitPrice }}</td>
103
-                        <td class="text-right pr-6 py-3">{{ $item->subtotal }}</td>
104
-                    </tr>
105
-                @endforeach
106
-                </tbody>
107
-                <tfoot class="text-sm tracking-tight">
108
-                <tr>
109
-                    <td class="pl-6 py-2" colspan="2"></td>
110
-                    <td class="text-right font-semibold py-2">Subtotal:</td>
111
-                    <td class="text-right pr-6 py-2">{{ $document->subtotal }}</td>
112
-                </tr>
113
-                @if($document->discount)
114
-                    <tr class="text-success-800 dark:text-success-600">
115
-                        <td class="pl-6 py-2" colspan="2"></td>
116
-                        <td class="text-right py-2">Discount:</td>
117
-                        <td class="text-right pr-6 py-2">
118
-                            ({{ $document->discount }})
119
-                        </td>
120
-                    </tr>
121
-                @endif
122
-                @if($document->tax)
123
-                    <tr>
124
-                        <td class="pl-6 py-2" colspan="2"></td>
125
-                        <td class="text-right py-2">Tax:</td>
126
-                        <td class="text-right pr-6 py-2">{{ $document->tax }}</td>
127
-                    </tr>
128
-                @endif
129
-                <tr>
130
-                    <td class="pl-6 py-2" colspan="2"></td>
131
-                    <td class="text-right font-semibold border-t py-2">Total:</td>
132
-                    <td class="text-right border-t pr-6 py-2">{{ $document->total }}</td>
133
-                </tr>
134
-                @if($document->amountDue)
135
-                    <tr>
136
-                        <td class="pl-6 py-2" colspan="2"></td>
137
-                        <td class="text-right font-semibold border-t-4 border-double py-2">{{ $document->label->amountDue }}
138
-                            ({{ $document->currencyCode }}):
139
-                        </td>
140
-                        <td class="text-right border-t-4 border-double pr-6 py-2">{{ $document->amountDue }}</td>
141
-                    </tr>
142
-                @endif
143
-                </tfoot>
144
-            </table>
145
-        </x-company.invoice.line-items>
146
-
147
-        <!-- Footer Notes -->
148
-        <x-company.invoice.footer class="modern-template-footer tracking-tight">
149
-            <h4 class="font-semibold px-6 text-sm" style="color: {{ $document->accentColor }}">
150
-                Terms & Conditions
151
-            </h4>
152
-            <span class="border-t-2 my-2 border-gray-300 block w-full"></span>
153
-            <div class="flex justify-between space-x-4 px-6 text-sm">
154
-                <p class="w-1/2 break-words line-clamp-4">{{ $document->terms }}</p>
155
-                <p class="w-1/2 break-words line-clamp-4">{{ $document->footer }}</p>
156
-            </div>
157
-        </x-company.invoice.footer>
158
-    </x-company.invoice.container>
15
+    @include("filament.infolists.components.document-templates.{$template->value}", [
16
+        'document' => $document,
17
+        'preview' => false,
18
+    ])
159 19
 </div>

+ 148
- 0
resources/views/filament/infolists/components/document-templates/classic.blade.php Visa fil

@@ -0,0 +1,148 @@
1
+<x-company.invoice.container class="classic-template-container">
2
+    <!-- Header Section -->
3
+    <x-company.invoice.header class="default-template-header">
4
+        <div class="w-2/3 text-left ml-6">
5
+            <div class="text-sm tracking-tight">
6
+                <h2 class="text-lg font-semibold">{{ $document->company->name }}</h2>
7
+                @if($formattedAddress = $document->company->getFormattedAddressHtml())
8
+                    {!! $formattedAddress !!}
9
+                @endif
10
+            </div>
11
+        </div>
12
+
13
+        <div class="w-1/3 flex justify-end mr-6">
14
+            @if($document->logo && $document->showLogo)
15
+                <x-company.invoice.logo :src="$document->logo"/>
16
+            @endif
17
+        </div>
18
+    </x-company.invoice.header>
19
+
20
+    <x-company.invoice.metadata class="classic-template-metadata space-y-8">
21
+        <div class="items-center flex">
22
+            <hr class="grow-[2] py-0.5 border-solid border-y-2" style="border-color: {{ $document->accentColor }};">
23
+            <x-icons.document-header-decoration
24
+                color="{{ $document->accentColor }}"
25
+                text="{{ $document->header }}"
26
+                class="w-60"
27
+            />
28
+            <hr class="grow-[2] py-0.5 border-solid border-y-2" style="border-color: {{ $document->accentColor }};">
29
+        </div>
30
+        <div class="mt-2 text-base text-center text-gray-600 dark:text-gray-400">{{ $document->subheader }}</div>
31
+
32
+        <div class="flex justify-between items-end">
33
+            <!-- Billing Details -->
34
+            <div class="text-sm tracking-tight">
35
+                <h3 class="text-gray-600 dark:text-gray-400 font-medium tracking-tight mb-1">BILL TO</h3>
36
+                <p class="text-base font-bold">{{ $document->client->name }}</p>
37
+                @if($formattedAddress = $document->client->getFormattedAddressHtml())
38
+                    {!! $formattedAddress !!}
39
+                @endif
40
+            </div>
41
+
42
+            <div class="text-sm tracking-tight">
43
+                <table class="min-w-full">
44
+                    <tbody>
45
+                    <tr>
46
+                        <td class="font-semibold text-right pr-2">{{ $document->label->number }}:</td>
47
+                        <td class="text-left pl-2">{{ $document->number }}</td>
48
+                    </tr>
49
+                    <tr>
50
+                        <td class="font-semibold text-right pr-2">{{ $document->label->referenceNumber }}:</td>
51
+                        <td class="text-left pl-2">{{ $document->referenceNumber }}</td>
52
+                    </tr>
53
+                    <tr>
54
+                        <td class="font-semibold text-right pr-2">{{ $document->label->date }}:</td>
55
+                        <td class="text-left pl-2">{{ $document->date }}</td>
56
+                    </tr>
57
+                    <tr>
58
+                        <td class="font-semibold text-right pr-2">{{ $document->label->dueDate }}:</td>
59
+                        <td class="text-left pl-2">{{ $document->dueDate }}</td>
60
+                    </tr>
61
+                    </tbody>
62
+                </table>
63
+            </div>
64
+        </div>
65
+    </x-company.invoice.metadata>
66
+
67
+    <!-- Line Items -->
68
+    <x-company.invoice.line-items class="classic-template-line-items">
69
+        <table class="w-full text-left table-fixed">
70
+            <thead class="text-sm leading-relaxed">
71
+            <tr>
72
+                <th class="text-left pl-6 w-[50%] py-4">{{ $document->columnLabel->items }}</th>
73
+                <th class="text-center w-[10%] py-4">{{ $document->columnLabel->units }}</th>
74
+                <th class="text-right w-[20%] py-4">{{ $document->columnLabel->price }}</th>
75
+                <th class="text-right pr-6 w-[20%] py-4">{{ $document->columnLabel->amount }}</th>
76
+            </tr>
77
+            </thead>
78
+            <tbody class="text-sm tracking-tight border-y-2 border-dotted border-gray-300">
79
+            @foreach($document->lineItems as $item)
80
+                <tr>
81
+                    <td class="text-left pl-6 font-semibold py-3">
82
+                        {{ $item->name }}
83
+                        @if($item->description)
84
+                            <div class="text-gray-600 font-normal line-clamp-2 mt-1">{{ $item->description }}</div>
85
+                        @endif
86
+                    </td>
87
+                    <td class="text-center py-3">{{ $item->quantity }}</td>
88
+                    <td class="text-right py-3">{{ $item->unitPrice }}</td>
89
+                    <td class="text-right pr-6 py-3">{{ $item->subtotal }}</td>
90
+                </tr>
91
+            @endforeach
92
+            </tbody>
93
+        </table>
94
+
95
+        <!-- Financial Details and Notes -->
96
+        <div class="flex justify-between text-sm tracking-tight space-x-1">
97
+            <!-- Notes Section -->
98
+            <div class="w-[60%] border border-dashed border-gray-300 p-2 mt-4">
99
+                <h4 class="font-semibold mb-2">Notes</h4>
100
+                <p>{{ $document->footer }}</p>
101
+            </div>
102
+
103
+            <!-- Financial Summary -->
104
+            <div class="w-[40%] mt-2">
105
+                <table class="w-full table-fixed">
106
+                    <tbody class="text-sm tracking-tight">
107
+                    <tr>
108
+                        <td class="text-right font-semibold py-2">Subtotal:</td>
109
+                        <td class="text-right pr-6 py-2">{{ $document->subtotal }}</td>
110
+                    </tr>
111
+                    @if($document->discount)
112
+                        <tr class="text-success-800 dark:text-success-600">
113
+                            <td class="text-right py-2">Discount:</td>
114
+                            <td class="text-right pr-6 py-2">
115
+                                ({{ $document->discount }})
116
+                            </td>
117
+                        </tr>
118
+                    @endif
119
+                    @if($document->tax)
120
+                        <tr>
121
+                            <td class="text-right py-2">Tax:</td>
122
+                            <td class="text-right pr-6 py-2">{{ $document->tax }}</td>
123
+                        </tr>
124
+                    @endif
125
+                    <tr>
126
+                        <td class="text-right font-semibold border-t py-2">Total:</td>
127
+                        <td class="text-right border-t pr-6 py-2">{{ $document->total }}</td>
128
+                    </tr>
129
+                    @if($document->amountDue)
130
+                        <tr>
131
+                            <td class="text-right font-semibold border-t-4 border-double py-2">{{ $document->label->amountDue }}
132
+                                ({{ $document->currencyCode }}):
133
+                            </td>
134
+                            <td class="text-right border-t-4 border-double pr-6 py-2">{{ $document->amountDue }}</td>
135
+                        </tr>
136
+                    @endif
137
+                    </tbody>
138
+                </table>
139
+            </div>
140
+        </div>
141
+    </x-company.invoice.line-items>
142
+
143
+    <!-- Footer -->
144
+    <x-company.invoice.footer class="classic-template-footer tracking-tight min-h-48">
145
+        <h4 class="font-semibold px-6 mb-2 text-sm">Terms & Conditions</h4>
146
+        <p class="px-6 break-words line-clamp-4 text-sm">{{ $document->terms }}</p>
147
+    </x-company.invoice.footer>
148
+</x-company.invoice.container>

+ 138
- 0
resources/views/filament/infolists/components/document-templates/default.blade.php Visa fil

@@ -0,0 +1,138 @@
1
+<x-company.invoice.container class="default-template-container">
2
+
3
+    <x-company.invoice.header class="default-template-header border-b-2 p-6 pb-4">
4
+        <div class="w-2/3">
5
+            @if($document->logo && $document->showLogo)
6
+                <x-company.invoice.logo :src="$document->logo"/>
7
+            @endif
8
+        </div>
9
+
10
+        <div class="w-1/3 text-right">
11
+            <div class="text-sm tracking-tight">
12
+                <h2 class="text-lg font-semibold">{{ $document->company->name }}</h2>
13
+                @if($formattedAddress = $document->company->getFormattedAddressHtml())
14
+                    {!! $formattedAddress !!}
15
+                @endif
16
+            </div>
17
+        </div>
18
+    </x-company.invoice.header>
19
+
20
+    <x-company.invoice.metadata class="default-template-metadata space-y-8">
21
+        <div>
22
+            <h1 class="text-4xl font-light uppercase">{{ $document->header }}</h1>
23
+            @if ($document->subheader)
24
+                <h2 class="text-base text-gray-600 dark:text-gray-400">{{ $document->subheader }}</h2>
25
+            @endif
26
+        </div>
27
+
28
+        <div class="flex justify-between items-end">
29
+            <!-- Billing Details -->
30
+            <div class="text-sm tracking-tight">
31
+                <h3 class="text-gray-600 dark:text-gray-400 font-medium tracking-tight mb-1">BILL TO</h3>
32
+                <p class="text-base font-bold">{{ $document->client->name }}</p>
33
+                @if($formattedAddress = $document->client->getFormattedAddressHtml())
34
+                    {!! $formattedAddress !!}
35
+                @endif
36
+            </div>
37
+
38
+            <div class="text-sm tracking-tight">
39
+                <table class="min-w-full">
40
+                    <tbody>
41
+                    <tr>
42
+                        <td class="font-semibold text-right pr-2">{{ $document->label->number }}:</td>
43
+                        <td class="text-left pl-2">{{ $document->number }}</td>
44
+                    </tr>
45
+                    @if($document->referenceNumber)
46
+                        <tr>
47
+                            <td class="font-semibold text-right pr-2">{{ $document->label->referenceNumber }}:</td>
48
+                            <td class="text-left pl-2">{{ $document->referenceNumber }}</td>
49
+                        </tr>
50
+                    @endif
51
+                    <tr>
52
+                        <td class="font-semibold text-right pr-2">{{ $document->label->date }}:</td>
53
+                        <td class="text-left pl-2">{{ $document->date }}</td>
54
+                    </tr>
55
+                    <tr>
56
+                        <td class="font-semibold text-right pr-2">{{ $document->label->dueDate }}:</td>
57
+                        <td class="text-left pl-2">{{ $document->dueDate }}</td>
58
+                    </tr>
59
+                    </tbody>
60
+                </table>
61
+            </div>
62
+        </div>
63
+    </x-company.invoice.metadata>
64
+
65
+    <!-- Line Items Table -->
66
+    <x-company.invoice.line-items class="default-template-line-items">
67
+        <table class="w-full text-left table-fixed">
68
+            <thead class="text-sm leading-relaxed" style="background: {{ $document->accentColor }}">
69
+            <tr class="text-white">
70
+                <th class="text-left pl-6 w-[50%] py-2">{{ $document->columnLabel->items }}</th>
71
+                <th class="text-center w-[10%] py-2">{{ $document->columnLabel->units }}</th>
72
+                <th class="text-right w-[20%] py-2">{{ $document->columnLabel->price }}</th>
73
+                <th class="text-right pr-6 w-[20%] py-2">{{ $document->columnLabel->amount }}</th>
74
+            </tr>
75
+            </thead>
76
+            <tbody class="text-sm tracking-tight border-b-2 border-gray-300">
77
+            @foreach($document->lineItems as $item)
78
+                <tr>
79
+                    <td class="text-left pl-6 font-semibold py-3">
80
+                        {{ $item->name }}
81
+                        @if($item->description)
82
+                            <div class="text-gray-600 font-normal line-clamp-2 mt-1">{{ $item->description }}</div>
83
+                        @endif
84
+                    </td>
85
+                    <td class="text-center py-3">{{ $item->quantity }}</td>
86
+                    <td class="text-right py-3">{{ $item->unitPrice }}</td>
87
+                    <td class="text-right pr-6 py-3">{{ $item->subtotal }}</td>
88
+                </tr>
89
+            @endforeach
90
+            </tbody>
91
+            <tfoot class="text-sm tracking-tight">
92
+            <tr>
93
+                <td class="pl-6 py-2" colspan="2"></td>
94
+                <td class="text-right font-semibold py-2">Subtotal:</td>
95
+                <td class="text-right pr-6 py-2">{{ $document->subtotal }}</td>
96
+            </tr>
97
+            @if($document->discount)
98
+                <tr class="text-success-800 dark:text-success-600">
99
+                    <td class="pl-6 py-2" colspan="2"></td>
100
+                    <td class="text-right py-2">Discount:</td>
101
+                    <td class="text-right pr-6 py-2">
102
+                        ({{ $document->discount }})
103
+                    </td>
104
+                </tr>
105
+            @endif
106
+            @if($document->tax)
107
+                <tr>
108
+                    <td class="pl-6 py-2" colspan="2"></td>
109
+                    <td class="text-right py-2">Tax:</td>
110
+                    <td class="text-right pr-6 py-2">{{ $document->tax }}</td>
111
+                </tr>
112
+            @endif
113
+            <tr>
114
+                <td class="pl-6 py-2" colspan="2"></td>
115
+                <td class="text-right font-semibold border-t py-2">Total:</td>
116
+                <td class="text-right border-t pr-6 py-2">{{ $document->total }}</td>
117
+            </tr>
118
+            @if($document->amountDue)
119
+                <tr>
120
+                    <td class="pl-6 py-2" colspan="2"></td>
121
+                    <td class="text-right font-semibold border-t-4 border-double py-2">{{ $document->label->amountDue }}
122
+                        ({{ $document->currencyCode }}):
123
+                    </td>
124
+                    <td class="text-right border-t-4 border-double pr-6 py-2">{{ $document->amountDue }}</td>
125
+                </tr>
126
+            @endif
127
+            </tfoot>
128
+        </table>
129
+    </x-company.invoice.line-items>
130
+
131
+    <!-- Footer Notes -->
132
+    <x-company.invoice.footer class="default-template-footer tracking-tight">
133
+        <p class="px-6 text-sm">{{ $document->footer }}</p>
134
+        <span class="border-t-2 my-2 border-gray-300 block w-full"></span>
135
+        <h4 class="font-semibold px-6 mb-2 text-sm">Terms & Conditions</h4>
136
+        <p class="px-6 break-words line-clamp-4 text-sm">{{ $document->terms }}</p>
137
+    </x-company.invoice.footer>
138
+</x-company.invoice.container>

+ 145
- 0
resources/views/filament/infolists/components/document-templates/modern.blade.php Visa fil

@@ -0,0 +1,145 @@
1
+<x-company.invoice.container class="modern-template-container">
2
+    <!-- Colored Header with Logo -->
3
+    <x-company.invoice.header class="bg-gray-800 h-24">
4
+        <!-- Logo -->
5
+        <div class="w-2/3">
6
+            @if($document->logo && $document->showLogo)
7
+                <x-company.invoice.logo class="ml-8" :src="$document->logo"/>
8
+            @endif
9
+        </div>
10
+
11
+        <!-- Ribbon Container -->
12
+        <div class="w-1/3 absolute right-0 top-0 p-3 h-32 flex flex-col justify-end rounded-bl-sm"
13
+             style="background: {{ $document->accentColor }};">
14
+            @if($document->header)
15
+                <h1 class="text-4xl font-bold text-white text-center uppercase">{{ $document->header }}</h1>
16
+            @endif
17
+        </div>
18
+    </x-company.invoice.header>
19
+
20
+    <!-- Company Details -->
21
+    <x-company.invoice.metadata class="modern-template-metadata space-y-8">
22
+        <div class="text-sm">
23
+            <h2 class="text-lg font-semibold">{{ $document->company->name }}</h2>
24
+            @if($formattedAddress = $document->company->getFormattedAddressHtml())
25
+                {!! $formattedAddress !!}
26
+            @endif
27
+        </div>
28
+
29
+        <div class="flex justify-between items-end">
30
+            <!-- Billing Details -->
31
+            <div class="text-sm tracking-tight">
32
+                <h3 class="text-gray-600 dark:text-gray-400 font-medium tracking-tight mb-1">BILL TO</h3>
33
+                <p class="text-base font-bold"
34
+                   style="color: {{ $document->accentColor }}">{{ $document->client->name }}</p>
35
+
36
+                @if($formattedAddress = $document->client->getFormattedAddressHtml())
37
+                    {!! $formattedAddress !!}
38
+                @endif
39
+            </div>
40
+
41
+            <div class="text-sm tracking-tight">
42
+                <table class="min-w-full">
43
+                    <tbody>
44
+                    <tr>
45
+                        <td class="font-semibold text-right pr-2">{{ $document->label->number }}:</td>
46
+                        <td class="text-left pl-2">{{ $document->number }}</td>
47
+                    </tr>
48
+                    @if($document->referenceNumber)
49
+                        <tr>
50
+                            <td class="font-semibold text-right pr-2">{{ $document->label->referenceNumber }}:</td>
51
+                            <td class="text-left pl-2">{{ $document->referenceNumber }}</td>
52
+                        </tr>
53
+                    @endif
54
+                    <tr>
55
+                        <td class="font-semibold text-right pr-2">{{ $document->label->date }}:</td>
56
+                        <td class="text-left pl-2">{{ $document->date }}</td>
57
+                    </tr>
58
+                    <tr>
59
+                        <td class="font-semibold text-right pr-2">{{ $document->label->dueDate }}:</td>
60
+                        <td class="text-left pl-2">{{ $document->dueDate }}</td>
61
+                    </tr>
62
+                    </tbody>
63
+                </table>
64
+            </div>
65
+        </div>
66
+    </x-company.invoice.metadata>
67
+
68
+    <!-- Line Items Table -->
69
+    <x-company.invoice.line-items class="modern-template-line-items">
70
+        <table class="w-full text-left table-fixed">
71
+            <thead class="text-sm leading-relaxed">
72
+            <tr class="text-gray-600 dark:text-gray-400">
73
+                <th class="text-left pl-6 w-[50%] py-4">{{ $document->columnLabel->items }}</th>
74
+                <th class="text-center w-[10%] py-4">{{ $document->columnLabel->units }}</th>
75
+                <th class="text-right w-[20%] py-4">{{ $document->columnLabel->price }}</th>
76
+                <th class="text-right pr-6 w-[20%] py-4">{{ $document->columnLabel->amount }}</th>
77
+            </tr>
78
+            </thead>
79
+            <tbody class="text-sm tracking-tight border-y-2">
80
+            @foreach($document->lineItems as $index => $item)
81
+                <tr @class(['bg-gray-100 dark:bg-gray-800' => $index % 2 === 0])>
82
+                    <td class="text-left pl-6 font-semibold py-3">
83
+                        {{ $item->name }}
84
+                        @if($item->description)
85
+                            <div class="text-gray-600 font-normal line-clamp-2 mt-1">{{ $item->description }}</div>
86
+                        @endif
87
+                    </td>
88
+                    <td class="text-center py-3">{{ $item->quantity }}</td>
89
+                    <td class="text-right py-3">{{ $item->unitPrice }}</td>
90
+                    <td class="text-right pr-6 py-3">{{ $item->subtotal }}</td>
91
+                </tr>
92
+            @endforeach
93
+            </tbody>
94
+            <tfoot class="text-sm tracking-tight">
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>
100
+            @if($document->discount)
101
+                <tr class="text-success-800 dark:text-success-600">
102
+                    <td class="pl-6 py-2" colspan="2"></td>
103
+                    <td class="text-right py-2">Discount:</td>
104
+                    <td class="text-right pr-6 py-2">
105
+                        ({{ $document->discount }})
106
+                    </td>
107
+                </tr>
108
+            @endif
109
+            @if($document->tax)
110
+                <tr>
111
+                    <td class="pl-6 py-2" colspan="2"></td>
112
+                    <td class="text-right py-2">Tax:</td>
113
+                    <td class="text-right pr-6 py-2">{{ $document->tax }}</td>
114
+                </tr>
115
+            @endif
116
+            <tr>
117
+                <td class="pl-6 py-2" colspan="2"></td>
118
+                <td class="text-right font-semibold border-t py-2">Total:</td>
119
+                <td class="text-right border-t pr-6 py-2">{{ $document->total }}</td>
120
+            </tr>
121
+            @if($document->amountDue)
122
+                <tr>
123
+                    <td class="pl-6 py-2" colspan="2"></td>
124
+                    <td class="text-right font-semibold border-t-4 border-double py-2">{{ $document->label->amountDue }}
125
+                        ({{ $document->currencyCode }}):
126
+                    </td>
127
+                    <td class="text-right border-t-4 border-double pr-6 py-2">{{ $document->amountDue }}</td>
128
+                </tr>
129
+            @endif
130
+            </tfoot>
131
+        </table>
132
+    </x-company.invoice.line-items>
133
+
134
+    <!-- Footer Notes -->
135
+    <x-company.invoice.footer class="modern-template-footer tracking-tight">
136
+        <h4 class="font-semibold px-6 text-sm" style="color: {{ $document->accentColor }}">
137
+            Terms & Conditions
138
+        </h4>
139
+        <span class="border-t-2 my-2 border-gray-300 block w-full"></span>
140
+        <div class="flex justify-between space-x-4 px-6 text-sm">
141
+            <p class="w-1/2 break-words line-clamp-4">{{ $document->terms }}</p>
142
+            <p class="w-1/2 break-words line-clamp-4">{{ $document->footer }}</p>
143
+        </div>
144
+    </x-company.invoice.footer>
145
+</x-company.invoice.container>

+ 3
- 3
resources/views/welcome.blade.php
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 66
- 0
tests/Feature/Setting/LocalizationTest.php Visa fil

@@ -0,0 +1,66 @@
1
+<?php
2
+
3
+use App\Models\Setting\Localization;
4
+use App\Utilities\RateCalculator;
5
+
6
+// RateCalculator Basic Operations
7
+it('calculates percentage correctly', function () {
8
+    $valueInCents = 100000; // 1000 dollars in cents
9
+    $scaledRate = RateCalculator::decimalToScaledRate(0.025); // 2.5%
10
+
11
+    expect(RateCalculator::calculatePercentage($valueInCents, $scaledRate))
12
+        ->toBe(2500); // Should be 25 dollars in cents
13
+});
14
+
15
+it('converts between scaled rates and decimals correctly', function (float $decimal, int $scaled) {
16
+    // Test decimal to scaled
17
+    expect(RateCalculator::decimalToScaledRate($decimal))->toBe($scaled)
18
+        ->and(RateCalculator::scaledRateToDecimal($scaled))->toBe($decimal);
19
+})->with([
20
+    [0.25, 250000],     // 0.25 * 1000000 = 250000
21
+    [0.1, 100000],      // 0.1 * 1000000 = 100000
22
+    [0.01, 10000],      // 0.01 * 1000000 = 10000
23
+    [0.001, 1000],      // 0.001 * 1000000 = 1000
24
+    [0.0001, 100],      // 0.0001 * 1000000 = 100
25
+]);
26
+
27
+it('handles rate formatting correctly for different computations', function () {
28
+    $localization = Localization::firstOrFail();
29
+    $localization->update(['language' => 'en']);
30
+
31
+    // Test fixed amount formatting
32
+    expect(rateFormat(100000, 'fixed', 'USD'))->toBe('$100,000.00 USD');
33
+
34
+    // Test percentage formatting
35
+    $scaledRate = RateCalculator::decimalToScaledRate(0.000025); // 0.25%
36
+    expect(rateFormat($scaledRate, 'percentage'))->toBe('25%');
37
+});
38
+
39
+// Edge Cases and Error Handling
40
+it('handles edge cases correctly', function () {
41
+    $localization = Localization::firstOrFail();
42
+    $localization->update(['language' => 'en']);
43
+
44
+    expect(RateCalculator::formatScaledRate(0))->toBe('0')
45
+        ->and(RateCalculator::formatScaledRate(1))->toBe('0.0001')
46
+        ->and(RateCalculator::formatScaledRate(10000000))->toBe('1,000')
47
+        ->and(RateCalculator::formatScaledRate(-250000))->toBe('-25');
48
+});
49
+
50
+// Precision Tests
51
+it('maintains precision correctly', function () {
52
+    $localization = Localization::firstOrFail();
53
+    $localization->update(['language' => 'en']);
54
+
55
+    $testCases = [
56
+        '1.0000' => '1',
57
+        '1.2300' => '1.23',
58
+        '1.2340' => '1.234',
59
+        '1.2345' => '1.2345',
60
+    ];
61
+
62
+    foreach ($testCases as $input => $expected) {
63
+        $scaled = RateCalculator::parseLocalizedRate($input);
64
+        expect(RateCalculator::formatScaledRate($scaled))->toBe($expected);
65
+    }
66
+});

Laddar…
Avbryt
Spara