Andrew Wallo 9 月之前
父節點
當前提交
a0ecf7dd4f

+ 33
- 0
app/DTO/ClientDTO.php 查看文件

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+use App\Models\Common\Client;
6
+
7
+readonly class ClientDTO
8
+{
9
+    public function __construct(
10
+        public string $name,
11
+        public string $addressLine1,
12
+        public string $addressLine2,
13
+        public string $city,
14
+        public string $state,
15
+        public string $postalCode,
16
+        public string $country,
17
+    ) {}
18
+
19
+    public static function fromModel(Client $client): self
20
+    {
21
+        $address = $client->billingAddress ?? null;
22
+
23
+        return new self(
24
+            name: $client->name,
25
+            addressLine1: $address?->address_line_1 ?? '',
26
+            addressLine2: $address?->address_line_2 ?? '',
27
+            city: $address?->city ?? '',
28
+            state: $address?->state ?? '',
29
+            postalCode: $address?->postal_code ?? '',
30
+            country: $address?->country ?? '',
31
+        );
32
+    }
33
+}

+ 31
- 0
app/DTO/CompanyDTO.php 查看文件

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+use App\Models\Company;
6
+
7
+readonly class CompanyDTO
8
+{
9
+    public function __construct(
10
+        public string $name,
11
+        public string $address,
12
+        public string $city,
13
+        public string $state,
14
+        public string $zipCode,
15
+        public string $country,
16
+    ) {}
17
+
18
+    public static function fromModel(Company $company): self
19
+    {
20
+        $profile = $company->profile;
21
+
22
+        return new self(
23
+            name: $company->name,
24
+            address: $profile->address ?? '',
25
+            city: $profile->city?->name ?? '',
26
+            state: $profile->state?->name ?? '',
27
+            zipCode: $profile->zip_code ?? '',
28
+            country: $profile->state?->country->name ?? '',
29
+        );
30
+    }
31
+}

+ 71
- 0
app/DTO/DocumentDTO.php 查看文件

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+use App\Models\Accounting\Document;
6
+use App\Models\Setting\DocumentDefault;
7
+use App\Utilities\Currency\CurrencyAccessor;
8
+use App\Utilities\Currency\CurrencyConverter;
9
+
10
+readonly class DocumentDTO
11
+{
12
+    /**
13
+     * @param  LineItemDTO[]  $lineItems
14
+     */
15
+    public function __construct(
16
+        public ?string $header,
17
+        public ?string $footer,
18
+        public ?string $terms,
19
+        public ?string $logo,
20
+        public string $number,
21
+        public ?string $referenceNumber,
22
+        public string $date,
23
+        public string $dueDate,
24
+        public string $currencyCode,
25
+        public string $subtotal,
26
+        public string $discount,
27
+        public string $tax,
28
+        public string $total,
29
+        public string $amountDue,
30
+        public CompanyDTO $company,
31
+        public ClientDTO $client,
32
+        public iterable $lineItems,
33
+        public DocumentLabelDTO $label,
34
+        public string $accentColor = '#000000',
35
+        public bool $showLogo = true,
36
+    ) {}
37
+
38
+    public static function fromModel(Document $document): self
39
+    {
40
+        /** @var DocumentDefault $settings */
41
+        $settings = $document->company->defaultInvoice;
42
+
43
+        return new self(
44
+            header: $document->header,
45
+            footer: $document->footer,
46
+            terms: $document->terms,
47
+            logo: $document->logo,
48
+            number: $document->documentNumber(),
49
+            referenceNumber: $document->referenceNumber(),
50
+            date: $document->documentDate(),
51
+            dueDate: $document->dueDate(),
52
+            currencyCode: $document->currency_code ?? CurrencyAccessor::getDefaultCurrency(),
53
+            subtotal: self::formatToMoney($document->subtotal, $document->currency_code),
54
+            discount: self::formatToMoney($document->discount_total, $document->currency_code),
55
+            tax: self::formatToMoney($document->tax_total, $document->currency_code),
56
+            total: self::formatToMoney($document->total, $document->currency_code),
57
+            amountDue: self::formatToMoney($document->amountDue(), $document->currency_code),
58
+            company: CompanyDTO::fromModel($document->company),
59
+            client: ClientDTO::fromModel($document->client),
60
+            lineItems: $document->lineItems->map(fn ($item) => LineItemDTO::fromModel($item)),
61
+            label: $document->documentType()->getLabels(),
62
+            accentColor: $settings->accent_color ?? '#000000',
63
+            showLogo: $settings->show_logo ?? false,
64
+        );
65
+    }
66
+
67
+    private static function formatToMoney(float | string $value, ?string $currencyCode): string
68
+    {
69
+        return CurrencyConverter::formatToMoney($value, $currencyCode);
70
+    }
71
+}

+ 27
- 0
app/DTO/DocumentLabelDTO.php 查看文件

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+readonly class DocumentLabelDTO
6
+{
7
+    public function __construct(
8
+        public string $title,
9
+        public string $number,
10
+        public string $referenceNumber,
11
+        public string $date,
12
+        public string $dueDate,
13
+        public string $amountDue,
14
+    ) {}
15
+
16
+    public function toArray(): array
17
+    {
18
+        return [
19
+            'title' => $this->title,
20
+            'number' => $this->number,
21
+            'reference_number' => $this->referenceNumber,
22
+            'date' => $this->date,
23
+            'due_date' => $this->dueDate,
24
+            'amount_due' => $this->amountDue,
25
+        ];
26
+    }
27
+}

+ 33
- 0
app/DTO/LineItemDTO.php 查看文件

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+use App\Models\Accounting\DocumentLineItem;
6
+use App\Utilities\Currency\CurrencyConverter;
7
+
8
+readonly class LineItemDTO
9
+{
10
+    public function __construct(
11
+        public string $name,
12
+        public string $description,
13
+        public int $quantity,
14
+        public string $unitPrice,
15
+        public string $subtotal,
16
+    ) {}
17
+
18
+    public static function fromModel(DocumentLineItem $lineItem): self
19
+    {
20
+        return new self(
21
+            name: $lineItem->offering->name ?? '',
22
+            description: $lineItem->description ?? '',
23
+            quantity: $lineItem->quantity,
24
+            unitPrice: self::formatToMoney($lineItem->unit_price, $lineItem->documentable->currency_code),
25
+            subtotal: self::formatToMoney($lineItem->subtotal, $lineItem->documentable->currency_code),
26
+        );
27
+    }
28
+
29
+    private static function formatToMoney(float | string $value, ?string $currencyCode): string
30
+    {
31
+        return CurrencyConverter::formatToMoney($value, $currencyCode);
32
+    }
33
+}

+ 34
- 33
app/Enums/Accounting/DocumentType.php 查看文件

2
 
2
 
3
 namespace App\Enums\Accounting;
3
 namespace App\Enums\Accounting;
4
 
4
 
5
+use App\DTO\DocumentLabelDTO;
5
 use Filament\Support\Contracts\HasIcon;
6
 use Filament\Support\Contracts\HasIcon;
6
 use Filament\Support\Contracts\HasLabel;
7
 use Filament\Support\Contracts\HasLabel;
7
 
8
 
47
         };
48
         };
48
     }
49
     }
49
 
50
 
50
-    public function getLabels(): array
51
+    public function getLabels(): DocumentLabelDTO
51
     {
52
     {
52
         return match ($this) {
53
         return match ($this) {
53
-            self::Invoice => [
54
-                'title' => 'Invoice',
55
-                'number' => 'Invoice Number',
56
-                'reference_number' => 'P.O/S.O Number',
57
-                'date' => 'Invoice Date',
58
-                'due_date' => 'Payment Due',
59
-                'amount_due' => 'Amount Due',
60
-            ],
61
-            self::RecurringInvoice => [
62
-                'title' => 'Recurring Invoice',
63
-                'number' => 'Invoice Number',
64
-                'reference_number' => 'P.O/S.O Number',
65
-                'date' => 'Invoice Date',
66
-                'due_date' => 'Payment Due',
67
-                'amount_due' => 'Amount Due',
68
-            ],
69
-            self::Estimate => [
70
-                'title' => 'Estimate',
71
-                'number' => 'Estimate Number',
72
-                'reference_number' => 'Reference Number',
73
-                'date' => 'Estimate Date',
74
-                'due_date' => 'Expiration Date',
75
-                'amount_due' => 'Grand Total',
76
-            ],
77
-            self::Bill => [
78
-                'title' => 'Bill',
79
-                'number' => 'Bill Number',
80
-                'reference_number' => 'P.O/S.O Number',
81
-                'date' => 'Bill Date',
82
-                'due_date' => 'Payment Due',
83
-                'amount_due' => 'Amount Due',
84
-            ],
54
+            self::Invoice => new DocumentLabelDTO(
55
+                title: 'Invoice',
56
+                number: 'Invoice Number',
57
+                referenceNumber: 'P.O/S.O Number',
58
+                date: 'Invoice Date',
59
+                dueDate: 'Payment Due',
60
+                amountDue: 'Amount Due',
61
+            ),
62
+            self::RecurringInvoice => new DocumentLabelDTO(
63
+                title: 'Recurring Invoice',
64
+                number: 'Invoice Number',
65
+                referenceNumber: 'P.O/S.O Number',
66
+                date: 'Invoice Date',
67
+                dueDate: 'Payment Due',
68
+                amountDue: 'Amount Due',
69
+            ),
70
+            self::Estimate => new DocumentLabelDTO(
71
+                title: 'Estimate',
72
+                number: 'Estimate Number',
73
+                referenceNumber: 'Reference Number',
74
+                date: 'Estimate Date',
75
+                dueDate: 'Expiration Date',
76
+                amountDue: 'Grand Total',
77
+            ),
78
+            self::Bill => new DocumentLabelDTO(
79
+                title: 'Bill',
80
+                number: 'Bill Number',
81
+                referenceNumber: 'P.O/S.O Number',
82
+                date: 'Bill Date',
83
+                dueDate: 'Payment Due',
84
+                amountDue: 'Amount Due',
85
+            ),
85
         };
86
         };
86
     }
87
     }
87
 }
88
 }

+ 0
- 9
app/Filament/Company/Resources/Sales/RecurringInvoiceResource/Pages/ViewRecurringInvoice.php 查看文件

2
 
2
 
3
 namespace App\Filament\Company\Resources\Sales\RecurringInvoiceResource\Pages;
3
 namespace App\Filament\Company\Resources\Sales\RecurringInvoiceResource\Pages;
4
 
4
 
5
-use App\Enums\Accounting\DayOfMonth;
6
 use App\Enums\Accounting\DocumentType;
5
 use App\Enums\Accounting\DocumentType;
7
 use App\Filament\Company\Resources\Sales\ClientResource;
6
 use App\Filament\Company\Resources\Sales\ClientResource;
8
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource;
7
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource;
25
 {
24
 {
26
     protected static string $resource = RecurringInvoiceResource::class;
25
     protected static string $resource = RecurringInvoiceResource::class;
27
 
26
 
28
-    protected function mutateFormDataBeforeFill(array $data): array
29
-    {
30
-        $data['day_of_month'] ??= DayOfMonth::First;
31
-        $data['start_date'] ??= now()->addMonth()->startOfMonth();
32
-
33
-        return $data;
34
-    }
35
-
36
     public function getMaxContentWidth(): MaxWidth | string | null
27
     public function getMaxContentWidth(): MaxWidth | string | null
37
     {
28
     {
38
         return MaxWidth::SixExtraLarge;
29
         return MaxWidth::SixExtraLarge;

+ 32
- 23
app/Models/Accounting/Bill.php 查看文件

5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
 use App\Casts\RateCast;
6
 use App\Casts\RateCast;
7
 use App\Collections\Accounting\DocumentCollection;
7
 use App\Collections\Accounting\DocumentCollection;
8
-use App\Concerns\Blamable;
9
-use App\Concerns\CompanyOwned;
10
 use App\Enums\Accounting\AdjustmentComputation;
8
 use App\Enums\Accounting\AdjustmentComputation;
11
 use App\Enums\Accounting\BillStatus;
9
 use App\Enums\Accounting\BillStatus;
12
 use App\Enums\Accounting\DocumentDiscountMethod;
10
 use App\Enums\Accounting\DocumentDiscountMethod;
11
+use App\Enums\Accounting\DocumentType;
13
 use App\Enums\Accounting\JournalEntryType;
12
 use App\Enums\Accounting\JournalEntryType;
14
 use App\Enums\Accounting\TransactionType;
13
 use App\Enums\Accounting\TransactionType;
15
 use App\Filament\Company\Resources\Purchases\BillResource;
14
 use App\Filament\Company\Resources\Purchases\BillResource;
25
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
24
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
26
 use Illuminate\Database\Eloquent\Builder;
25
 use Illuminate\Database\Eloquent\Builder;
27
 use Illuminate\Database\Eloquent\Casts\Attribute;
26
 use Illuminate\Database\Eloquent\Casts\Attribute;
28
-use Illuminate\Database\Eloquent\Factories\HasFactory;
29
 use Illuminate\Database\Eloquent\Model;
27
 use Illuminate\Database\Eloquent\Model;
30
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
28
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
31
 use Illuminate\Database\Eloquent\Relations\MorphMany;
29
 use Illuminate\Database\Eloquent\Relations\MorphMany;
34
 
32
 
35
 #[CollectedBy(DocumentCollection::class)]
33
 #[CollectedBy(DocumentCollection::class)]
36
 #[ObservedBy(BillObserver::class)]
34
 #[ObservedBy(BillObserver::class)]
37
-class Bill extends Model
35
+class Bill extends Document
38
 {
36
 {
39
-    use Blamable;
40
-    use CompanyOwned;
41
-    use HasFactory;
42
-
43
     protected $table = 'bills';
37
     protected $table = 'bills';
44
 
38
 
45
     protected $fillable = [
39
     protected $fillable = [
81
         'amount_due' => MoneyCast::class,
75
         'amount_due' => MoneyCast::class,
82
     ];
76
     ];
83
 
77
 
84
-    public function currency(): BelongsTo
85
-    {
86
-        return $this->belongsTo(Currency::class, 'currency_code', 'code');
87
-    }
88
-
89
     public function vendor(): BelongsTo
78
     public function vendor(): BelongsTo
90
     {
79
     {
91
         return $this->belongsTo(Vendor::class);
80
         return $this->belongsTo(Vendor::class);
92
     }
81
     }
93
 
82
 
94
-    public function lineItems(): MorphMany
95
-    {
96
-        return $this->morphMany(DocumentLineItem::class, 'documentable');
97
-    }
98
-
99
     public function transactions(): MorphMany
83
     public function transactions(): MorphMany
100
     {
84
     {
101
         return $this->morphMany(Transaction::class, 'transactionable');
85
         return $this->morphMany(Transaction::class, 'transactionable');
122
             ->where('type', TransactionType::Journal);
106
             ->where('type', TransactionType::Journal);
123
     }
107
     }
124
 
108
 
109
+    public function documentType(): DocumentType
110
+    {
111
+        return DocumentType::Bill;
112
+    }
113
+
114
+    public function documentNumber(): ?string
115
+    {
116
+        return $this->bill_number;
117
+    }
118
+
119
+    public function documentDate(): ?string
120
+    {
121
+        return $this->date?->toDefaultDateFormat();
122
+    }
123
+
124
+    public function dueDate(): ?string
125
+    {
126
+        return $this->due_date?->toDefaultDateFormat();
127
+    }
128
+
129
+    public function referenceNumber(): ?string
130
+    {
131
+        return $this->order_number;
132
+    }
133
+
134
+    public function amountDue(): ?string
135
+    {
136
+        return $this->amount_due;
137
+    }
138
+
125
     protected function isCurrentlyOverdue(): Attribute
139
     protected function isCurrentlyOverdue(): Attribute
126
     {
140
     {
127
         return Attribute::get(function () {
141
         return Attribute::get(function () {
152
         ]) && $this->currency_code === CurrencyAccessor::getDefaultCurrency();
166
         ]) && $this->currency_code === CurrencyAccessor::getDefaultCurrency();
153
     }
167
     }
154
 
168
 
155
-    public function hasLineItems(): bool
156
-    {
157
-        return $this->lineItems()->exists();
158
-    }
159
-
160
     public function hasPayments(): bool
169
     public function hasPayments(): bool
161
     {
170
     {
162
         return $this->payments->isNotEmpty();
171
         return $this->payments->isNotEmpty();

+ 46
- 0
app/Models/Accounting/Document.php 查看文件

1
+<?php
2
+
3
+namespace App\Models\Accounting;
4
+
5
+use App\Concerns\Blamable;
6
+use App\Concerns\CompanyOwned;
7
+use App\Enums\Accounting\DocumentType;
8
+use App\Models\Setting\Currency;
9
+use Illuminate\Database\Eloquent\Factories\HasFactory;
10
+use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
+use Illuminate\Database\Eloquent\Relations\MorphMany;
13
+
14
+abstract class Document extends Model
15
+{
16
+    use Blamable;
17
+    use CompanyOwned;
18
+    use HasFactory;
19
+
20
+    public function currency(): BelongsTo
21
+    {
22
+        return $this->belongsTo(Currency::class, 'currency_code', 'code');
23
+    }
24
+
25
+    public function lineItems(): MorphMany
26
+    {
27
+        return $this->morphMany(DocumentLineItem::class, 'documentable');
28
+    }
29
+
30
+    public function hasLineItems(): bool
31
+    {
32
+        return $this->lineItems()->exists();
33
+    }
34
+
35
+    abstract public function documentType(): DocumentType;
36
+
37
+    abstract public function documentNumber(): ?string;
38
+
39
+    abstract public function documentDate(): ?string;
40
+
41
+    abstract public function dueDate(): ?string;
42
+
43
+    abstract public function referenceNumber(): ?string;
44
+
45
+    abstract public function amountDue(): ?string;
46
+}

+ 28
- 21
app/Models/Accounting/Estimate.php 查看文件

5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
 use App\Casts\RateCast;
6
 use App\Casts\RateCast;
7
 use App\Collections\Accounting\DocumentCollection;
7
 use App\Collections\Accounting\DocumentCollection;
8
-use App\Concerns\Blamable;
9
-use App\Concerns\CompanyOwned;
10
 use App\Enums\Accounting\AdjustmentComputation;
8
 use App\Enums\Accounting\AdjustmentComputation;
11
 use App\Enums\Accounting\DocumentDiscountMethod;
9
 use App\Enums\Accounting\DocumentDiscountMethod;
10
+use App\Enums\Accounting\DocumentType;
12
 use App\Enums\Accounting\EstimateStatus;
11
 use App\Enums\Accounting\EstimateStatus;
13
 use App\Enums\Accounting\InvoiceStatus;
12
 use App\Enums\Accounting\InvoiceStatus;
14
 use App\Filament\Company\Resources\Sales\EstimateResource;
13
 use App\Filament\Company\Resources\Sales\EstimateResource;
15
 use App\Filament\Company\Resources\Sales\InvoiceResource;
14
 use App\Filament\Company\Resources\Sales\InvoiceResource;
16
 use App\Models\Common\Client;
15
 use App\Models\Common\Client;
17
-use App\Models\Setting\Currency;
18
 use App\Observers\EstimateObserver;
16
 use App\Observers\EstimateObserver;
19
 use Filament\Actions\Action;
17
 use Filament\Actions\Action;
20
 use Filament\Actions\MountableAction;
18
 use Filament\Actions\MountableAction;
23
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
21
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
24
 use Illuminate\Database\Eloquent\Builder;
22
 use Illuminate\Database\Eloquent\Builder;
25
 use Illuminate\Database\Eloquent\Casts\Attribute;
23
 use Illuminate\Database\Eloquent\Casts\Attribute;
26
-use Illuminate\Database\Eloquent\Factories\HasFactory;
27
 use Illuminate\Database\Eloquent\Model;
24
 use Illuminate\Database\Eloquent\Model;
28
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
25
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
29
 use Illuminate\Database\Eloquent\Relations\HasOne;
26
 use Illuminate\Database\Eloquent\Relations\HasOne;
30
-use Illuminate\Database\Eloquent\Relations\MorphMany;
31
 use Illuminate\Support\Carbon;
27
 use Illuminate\Support\Carbon;
32
 
28
 
33
 #[CollectedBy(DocumentCollection::class)]
29
 #[CollectedBy(DocumentCollection::class)]
34
 #[ObservedBy(EstimateObserver::class)]
30
 #[ObservedBy(EstimateObserver::class)]
35
-class Estimate extends Model
31
+class Estimate extends Document
36
 {
32
 {
37
-    use Blamable;
38
-    use CompanyOwned;
39
-    use HasFactory;
40
-
41
     protected $fillable = [
33
     protected $fillable = [
42
         'company_id',
34
         'company_id',
43
         'client_id',
35
         'client_id',
92
         return $this->belongsTo(Client::class);
84
         return $this->belongsTo(Client::class);
93
     }
85
     }
94
 
86
 
95
-    public function currency(): BelongsTo
87
+    public function invoice(): HasOne
96
     {
88
     {
97
-        return $this->belongsTo(Currency::class, 'currency_code', 'code');
89
+        return $this->hasOne(Invoice::class);
98
     }
90
     }
99
 
91
 
100
-    public function invoice(): HasOne
92
+    public function documentType(): DocumentType
101
     {
93
     {
102
-        return $this->hasOne(Invoice::class);
94
+        return DocumentType::Estimate;
95
+    }
96
+
97
+    public function documentNumber(): ?string
98
+    {
99
+        return $this->estimate_number;
100
+    }
101
+
102
+    public function documentDate(): ?string
103
+    {
104
+        return $this->date?->toDateString();
105
+    }
106
+
107
+    public function dueDate(): ?string
108
+    {
109
+        return $this->expiration_date?->toDateString();
110
+    }
111
+
112
+    public function referenceNumber(): ?string
113
+    {
114
+        return $this->reference_number;
103
     }
115
     }
104
 
116
 
105
-    public function lineItems(): MorphMany
117
+    public function amountDue(): ?string
106
     {
118
     {
107
-        return $this->morphMany(DocumentLineItem::class, 'documentable');
119
+        return $this->total;
108
     }
120
     }
109
 
121
 
110
     protected function isCurrentlyExpired(): Attribute
122
     protected function isCurrentlyExpired(): Attribute
190
             && ! $this->wasConverted();
202
             && ! $this->wasConverted();
191
     }
203
     }
192
 
204
 
193
-    public function hasLineItems(): bool
194
-    {
195
-        return $this->lineItems()->exists();
196
-    }
197
-
198
     public function scopeActive(Builder $query): Builder
205
     public function scopeActive(Builder $query): Builder
199
     {
206
     {
200
         return $query->whereIn('status', [
207
         return $query->whereIn('status', [

+ 32
- 24
app/Models/Accounting/Invoice.php 查看文件

5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
 use App\Casts\RateCast;
6
 use App\Casts\RateCast;
7
 use App\Collections\Accounting\DocumentCollection;
7
 use App\Collections\Accounting\DocumentCollection;
8
-use App\Concerns\Blamable;
9
-use App\Concerns\CompanyOwned;
10
 use App\Enums\Accounting\AdjustmentComputation;
8
 use App\Enums\Accounting\AdjustmentComputation;
11
 use App\Enums\Accounting\DocumentDiscountMethod;
9
 use App\Enums\Accounting\DocumentDiscountMethod;
10
+use App\Enums\Accounting\DocumentType;
12
 use App\Enums\Accounting\InvoiceStatus;
11
 use App\Enums\Accounting\InvoiceStatus;
13
 use App\Enums\Accounting\JournalEntryType;
12
 use App\Enums\Accounting\JournalEntryType;
14
 use App\Enums\Accounting\TransactionType;
13
 use App\Enums\Accounting\TransactionType;
16
 use App\Models\Banking\BankAccount;
15
 use App\Models\Banking\BankAccount;
17
 use App\Models\Common\Client;
16
 use App\Models\Common\Client;
18
 use App\Models\Company;
17
 use App\Models\Company;
19
-use App\Models\Setting\Currency;
20
 use App\Observers\InvoiceObserver;
18
 use App\Observers\InvoiceObserver;
21
 use App\Utilities\Currency\CurrencyAccessor;
19
 use App\Utilities\Currency\CurrencyAccessor;
22
 use App\Utilities\Currency\CurrencyConverter;
20
 use App\Utilities\Currency\CurrencyConverter;
27
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
25
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
28
 use Illuminate\Database\Eloquent\Builder;
26
 use Illuminate\Database\Eloquent\Builder;
29
 use Illuminate\Database\Eloquent\Casts\Attribute;
27
 use Illuminate\Database\Eloquent\Casts\Attribute;
30
-use Illuminate\Database\Eloquent\Factories\HasFactory;
31
 use Illuminate\Database\Eloquent\Model;
28
 use Illuminate\Database\Eloquent\Model;
32
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
29
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
33
 use Illuminate\Database\Eloquent\Relations\MorphMany;
30
 use Illuminate\Database\Eloquent\Relations\MorphMany;
36
 
33
 
37
 #[CollectedBy(DocumentCollection::class)]
34
 #[CollectedBy(DocumentCollection::class)]
38
 #[ObservedBy(InvoiceObserver::class)]
35
 #[ObservedBy(InvoiceObserver::class)]
39
-class Invoice extends Model
36
+class Invoice extends Document
40
 {
37
 {
41
-    use Blamable;
42
-    use CompanyOwned;
43
-    use HasFactory;
44
-
45
     protected $table = 'invoices';
38
     protected $table = 'invoices';
46
 
39
 
47
     protected $fillable = [
40
     protected $fillable = [
100
         return $this->belongsTo(Client::class);
93
         return $this->belongsTo(Client::class);
101
     }
94
     }
102
 
95
 
103
-    public function currency(): BelongsTo
104
-    {
105
-        return $this->belongsTo(Currency::class, 'currency_code', 'code');
106
-    }
107
-
108
     public function estimate(): BelongsTo
96
     public function estimate(): BelongsTo
109
     {
97
     {
110
         return $this->belongsTo(Estimate::class);
98
         return $this->belongsTo(Estimate::class);
115
         return $this->belongsTo(RecurringInvoice::class);
103
         return $this->belongsTo(RecurringInvoice::class);
116
     }
104
     }
117
 
105
 
118
-    public function lineItems(): MorphMany
119
-    {
120
-        return $this->morphMany(DocumentLineItem::class, 'documentable');
121
-    }
122
-
123
     public function transactions(): MorphMany
106
     public function transactions(): MorphMany
124
     {
107
     {
125
         return $this->morphMany(Transaction::class, 'transactionable');
108
         return $this->morphMany(Transaction::class, 'transactionable');
146
             ->where('type', TransactionType::Journal);
129
             ->where('type', TransactionType::Journal);
147
     }
130
     }
148
 
131
 
132
+    public function documentType(): DocumentType
133
+    {
134
+        return DocumentType::Invoice;
135
+    }
136
+
137
+    public function documentNumber(): ?string
138
+    {
139
+        return $this->invoice_number;
140
+    }
141
+
142
+    public function documentDate(): ?string
143
+    {
144
+        return $this->date?->toDefaultDateFormat();
145
+    }
146
+
147
+    public function dueDate(): ?string
148
+    {
149
+        return $this->due_date?->toDefaultDateFormat();
150
+    }
151
+
152
+    public function referenceNumber(): ?string
153
+    {
154
+        return $this->order_number;
155
+    }
156
+
157
+    public function amountDue(): ?string
158
+    {
159
+        return $this->amount_due;
160
+    }
161
+
149
     public function scopeUnpaid(Builder $query): Builder
162
     public function scopeUnpaid(Builder $query): Builder
150
     {
163
     {
151
         return $query->whereNotIn('status', [
164
         return $query->whereNotIn('status', [
222
         return ! $this->hasBeenSent();
235
         return ! $this->hasBeenSent();
223
     }
236
     }
224
 
237
 
225
-    public function hasLineItems(): bool
226
-    {
227
-        return $this->lineItems()->exists();
228
-    }
229
-
230
     public function hasPayments(): bool
238
     public function hasPayments(): bool
231
     {
239
     {
232
         return $this->payments()->exists();
240
         return $this->payments()->exists();

+ 35
- 37
app/Models/Accounting/RecurringInvoice.php 查看文件

5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
 use App\Casts\RateCast;
6
 use App\Casts\RateCast;
7
 use App\Collections\Accounting\DocumentCollection;
7
 use App\Collections\Accounting\DocumentCollection;
8
-use App\Concerns\Blamable;
9
-use App\Concerns\CompanyOwned;
10
 use App\Enums\Accounting\AdjustmentComputation;
8
 use App\Enums\Accounting\AdjustmentComputation;
11
 use App\Enums\Accounting\DayOfMonth;
9
 use App\Enums\Accounting\DayOfMonth;
12
 use App\Enums\Accounting\DayOfWeek;
10
 use App\Enums\Accounting\DayOfWeek;
13
 use App\Enums\Accounting\DocumentDiscountMethod;
11
 use App\Enums\Accounting\DocumentDiscountMethod;
12
+use App\Enums\Accounting\DocumentType;
14
 use App\Enums\Accounting\EndType;
13
 use App\Enums\Accounting\EndType;
15
 use App\Enums\Accounting\Frequency;
14
 use App\Enums\Accounting\Frequency;
16
 use App\Enums\Accounting\IntervalType;
15
 use App\Enums\Accounting\IntervalType;
20
 use App\Filament\Forms\Components\CustomSection;
19
 use App\Filament\Forms\Components\CustomSection;
21
 use App\Models\Common\Client;
20
 use App\Models\Common\Client;
22
 use App\Models\Setting\CompanyProfile;
21
 use App\Models\Setting\CompanyProfile;
23
-use App\Models\Setting\Currency;
24
 use App\Observers\RecurringInvoiceObserver;
22
 use App\Observers\RecurringInvoiceObserver;
25
 use App\Utilities\Localization\Timezone;
23
 use App\Utilities\Localization\Timezone;
26
 use Filament\Actions\Action;
24
 use Filament\Actions\Action;
27
 use Filament\Actions\MountableAction;
25
 use Filament\Actions\MountableAction;
28
 use Filament\Forms;
26
 use Filament\Forms;
29
 use Filament\Forms\Form;
27
 use Filament\Forms\Form;
30
-use Filament\Support\Enums\MaxWidth;
31
 use Guava\FilamentClusters\Forms\Cluster;
28
 use Guava\FilamentClusters\Forms\Cluster;
32
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
29
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
33
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
30
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
34
-use Illuminate\Database\Eloquent\Factories\HasFactory;
35
-use Illuminate\Database\Eloquent\Model;
36
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
31
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
37
 use Illuminate\Database\Eloquent\Relations\HasMany;
32
 use Illuminate\Database\Eloquent\Relations\HasMany;
38
-use Illuminate\Database\Eloquent\Relations\MorphMany;
39
 use Illuminate\Support\Carbon;
33
 use Illuminate\Support\Carbon;
40
 
34
 
41
 #[CollectedBy(DocumentCollection::class)]
35
 #[CollectedBy(DocumentCollection::class)]
42
 #[ObservedBy(RecurringInvoiceObserver::class)]
36
 #[ObservedBy(RecurringInvoiceObserver::class)]
43
-class RecurringInvoice extends Model
37
+class RecurringInvoice extends Document
44
 {
38
 {
45
-    use Blamable;
46
-    use CompanyOwned;
47
-    use HasFactory;
48
-
49
     protected $table = 'recurring_invoices';
39
     protected $table = 'recurring_invoices';
50
 
40
 
51
     protected $fillable = [
41
     protected $fillable = [
120
         return $this->belongsTo(Client::class);
110
         return $this->belongsTo(Client::class);
121
     }
111
     }
122
 
112
 
123
-    public function currency(): BelongsTo
113
+    public function invoices(): HasMany
124
     {
114
     {
125
-        return $this->belongsTo(Currency::class, 'currency_code', 'code');
115
+        return $this->hasMany(Invoice::class, 'recurring_invoice_id');
126
     }
116
     }
127
 
117
 
128
-    public function invoices(): HasMany
118
+    public function documentType(): DocumentType
129
     {
119
     {
130
-        return $this->hasMany(Invoice::class, 'recurring_invoice_id');
120
+        return DocumentType::RecurringInvoice;
121
+    }
122
+
123
+    public function documentNumber(): ?string
124
+    {
125
+        return 'Auto-generated';
126
+    }
127
+
128
+    public function documentDate(): ?string
129
+    {
130
+        return $this->calculateNextDate()?->toDefaultDateFormat() ?? 'Auto-generated';
131
+    }
132
+
133
+    public function dueDate(): ?string
134
+    {
135
+        return $this->calculateNextDueDate()?->toDefaultDateFormat() ?? 'Auto-generated';
136
+    }
137
+
138
+    public function referenceNumber(): ?string
139
+    {
140
+        return $this->order_number;
131
     }
141
     }
132
 
142
 
133
-    public function lineItems(): MorphMany
143
+    public function amountDue(): ?string
134
     {
144
     {
135
-        return $this->morphMany(DocumentLineItem::class, 'documentable');
145
+        return $this->total;
136
     }
146
     }
137
 
147
 
138
     public function isDraft(): bool
148
     public function isDraft(): bool
170
         return $this->isActive() && ! $this->wasEnded();
180
         return $this->isActive() && ! $this->wasEnded();
171
     }
181
     }
172
 
182
 
173
-    public function hasLineItems(): bool
174
-    {
175
-        return $this->lineItems()->exists();
176
-    }
177
-
178
     public function hasSchedule(): bool
183
     public function hasSchedule(): bool
179
     {
184
     {
180
         return $this->start_date !== null;
185
         return $this->start_date !== null;
345
             ->label(fn (self $record) => $record->hasSchedule() ? 'Update Schedule' : 'Set Schedule')
350
             ->label(fn (self $record) => $record->hasSchedule() ? 'Update Schedule' : 'Set Schedule')
346
             ->icon('heroicon-o-calendar-date-range')
351
             ->icon('heroicon-o-calendar-date-range')
347
             ->slideOver()
352
             ->slideOver()
348
-            ->modalWidth(MaxWidth::FiveExtraLarge)
349
             ->successNotificationTitle('Schedule Updated')
353
             ->successNotificationTitle('Schedule Updated')
350
             ->mountUsing(function (self $record, Form $form) {
354
             ->mountUsing(function (self $record, Form $form) {
351
                 $data = $record->attributesToArray();
355
                 $data = $record->attributesToArray();
412
                         // Custom frequency fields in a nested grid
416
                         // Custom frequency fields in a nested grid
413
                         Cluster::make([
417
                         Cluster::make([
414
                             Forms\Components\TextInput::make('interval_value')
418
                             Forms\Components\TextInput::make('interval_value')
415
-                                ->label('every')
416
                                 ->softRequired()
419
                                 ->softRequired()
417
                                 ->numeric()
420
                                 ->numeric()
418
                                 ->default(1),
421
                                 ->default(1),
419
                             Forms\Components\Select::make('interval_type')
422
                             Forms\Components\Select::make('interval_type')
420
-                                ->label('Interval Type')
421
                                 ->options(IntervalType::class)
423
                                 ->options(IntervalType::class)
422
                                 ->softRequired()
424
                                 ->softRequired()
423
                                 ->default(IntervalType::Month)
425
                                 ->default(IntervalType::Month)
449
                                 }),
451
                                 }),
450
                         ])
452
                         ])
451
                             ->live()
453
                             ->live()
452
-                            ->label('Interval')
454
+                            ->label('Every')
453
                             ->required()
455
                             ->required()
454
                             ->markAsRequired(false)
456
                             ->markAsRequired(false)
455
                             ->visible(fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isCustom()),
457
                             ->visible(fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isCustom()),
485
                             ),
487
                             ),
486
                     ])->columns(2),
488
                     ])->columns(2),
487
 
489
 
488
-                CustomSection::make('Dates')
490
+                CustomSection::make('Dates & Time')
489
                     ->contained(false)
491
                     ->contained(false)
490
                     ->schema([
492
                     ->schema([
491
                         Forms\Components\DatePicker::make('start_date')
493
                         Forms\Components\DatePicker::make('start_date')
492
-                            ->label('Create First Invoice')
494
+                            ->label('First Invoice Date')
493
                             ->softRequired(),
495
                             ->softRequired(),
494
 
496
 
495
                         Forms\Components\Group::make(function (Forms\Get $get) {
497
                         Forms\Components\Group::make(function (Forms\Get $get) {
524
                             if ($endType?->isAfter()) {
526
                             if ($endType?->isAfter()) {
525
                                 $components[] = Forms\Components\TextInput::make('max_occurrences')
527
                                 $components[] = Forms\Components\TextInput::make('max_occurrences')
526
                                     ->numeric()
528
                                     ->numeric()
529
+                                    ->suffix('invoices')
527
                                     ->live();
530
                                     ->live();
528
                             }
531
                             }
529
 
532
 
534
 
537
 
535
                             return [
538
                             return [
536
                                 Cluster::make($components)
539
                                 Cluster::make($components)
537
-                                    ->label('Ends')
540
+                                    ->label('Schedule Ends')
538
                                     ->required()
541
                                     ->required()
539
                                     ->markAsRequired(false),
542
                                     ->markAsRequired(false),
540
                             ];
543
                             ];
541
                         }),
544
                         }),
542
-                    ])
543
-                    ->columns(2),
544
 
545
 
545
-                CustomSection::make('Time Zone')
546
-                    ->contained(false)
547
-                    ->columns(2)
548
-                    ->schema([
549
                         Forms\Components\Select::make('timezone')
546
                         Forms\Components\Select::make('timezone')
550
                             ->options(Timezone::getTimezoneOptions(CompanyProfile::first()->country))
547
                             ->options(Timezone::getTimezoneOptions(CompanyProfile::first()->country))
551
                             ->searchable()
548
                             ->searchable()
552
                             ->softRequired(),
549
                             ->softRequired(),
553
-                    ]),
550
+                    ])
551
+                    ->columns(2),
554
             ])
552
             ])
555
             ->action(function (self $record, array $data, MountableAction $action) {
553
             ->action(function (self $record, array $data, MountableAction $action) {
556
                 $record->update($data);
554
                 $record->update($data);

+ 0
- 125
app/View/Models/DocumentPreviewViewModel.php 查看文件

1
-<?php
2
-
3
-namespace App\View\Models;
4
-
5
-use App\Enums\Accounting\DocumentType;
6
-use App\Models\Accounting\DocumentLineItem;
7
-use App\Models\Common\Client;
8
-use App\Models\Company;
9
-use App\Models\Setting\DocumentDefault;
10
-use App\Utilities\Currency\CurrencyAccessor;
11
-use App\Utilities\Currency\CurrencyConverter;
12
-use Illuminate\Database\Eloquent\Model;
13
-
14
-class DocumentPreviewViewModel
15
-{
16
-    public function __construct(
17
-        public Model $document,
18
-        public DocumentType $documentType = DocumentType::Invoice,
19
-    ) {}
20
-
21
-    public function buildViewData(): array
22
-    {
23
-        return [
24
-            'company' => $this->getCompanyDetails(),
25
-            'client' => $this->getClientDetails(),
26
-            'metadata' => $this->getDocumentMetadata(),
27
-            'lineItems' => $this->getLineItems(),
28
-            'totals' => $this->getTotals(),
29
-            'header' => $this->document->header,
30
-            'footer' => $this->document->footer,
31
-            'terms' => $this->document->terms,
32
-            'logo' => $this->document->logo,
33
-            'style' => $this->getStyle(),
34
-            'labels' => $this->documentType->getLabels(),
35
-        ];
36
-    }
37
-
38
-    private function getCompanyDetails(): array
39
-    {
40
-        /** @var Company $company */
41
-        $company = $this->document->company;
42
-        $profile = $company->profile;
43
-
44
-        return [
45
-            'name' => $company->name,
46
-            'address' => $profile->address ?? '',
47
-            'city' => $profile->city?->name ?? '',
48
-            'state' => $profile->state?->name ?? '',
49
-            'zip_code' => $profile->zip_code ?? '',
50
-            'country' => $profile->state?->country->name ?? '',
51
-        ];
52
-    }
53
-
54
-    private function getClientDetails(): array
55
-    {
56
-        /** @var Client $client */
57
-        $client = $this->document->client;
58
-        $address = $client->billingAddress ?? null;
59
-
60
-        return [
61
-            'name' => $client->name,
62
-            'address_line_1' => $address->address_line_1 ?? '',
63
-            'address_line_2' => $address->address_line_2 ?? '',
64
-            'city' => $address->city ?? '',
65
-            'state' => $address->state ?? '',
66
-            'postal_code' => $address->postal_code ?? '',
67
-            'country' => $address->country ?? '',
68
-        ];
69
-    }
70
-
71
-    private function getDocumentMetadata(): array
72
-    {
73
-        $number = match ($this->documentType) {
74
-            DocumentType::Invoice => $this->document->invoice_number,
75
-            DocumentType::RecurringInvoice => 'Auto-generated',
76
-            DocumentType::Bill => $this->document->bill_number,
77
-            DocumentType::Estimate => $this->document->estimate_number,
78
-        };
79
-
80
-        return [
81
-            'number' => $number,
82
-            'reference_number' => $this->document->order_number ?? $this->document->reference_number,
83
-            'date' => $this->document->date?->toDefaultDateFormat() ?? $this->document->calculateNextDate()?->toDefaultDateFormat(),
84
-            'due_date' => $this->document->due_date?->toDefaultDateFormat() ?? $this->document->expiration_date?->toDefaultDateFormat() ?? $this->document->calculateNextDueDate()?->toDefaultDateFormat(),
85
-            'currency_code' => $this->document->currency_code ?? CurrencyAccessor::getDefaultCurrency(),
86
-        ];
87
-    }
88
-
89
-    private function getLineItems(): array
90
-    {
91
-        $currencyCode = $this->document->currency_code ?? CurrencyAccessor::getDefaultCurrency();
92
-
93
-        return $this->document->lineItems->map(fn (DocumentLineItem $item) => [
94
-            'name' => $item->offering->name ?? '',
95
-            'description' => $item->description ?? '',
96
-            'quantity' => $item->quantity,
97
-            'unit_price' => CurrencyConverter::formatToMoney($item->unit_price, $currencyCode),
98
-            'subtotal' => CurrencyConverter::formatToMoney($item->subtotal, $currencyCode),
99
-        ])->toArray();
100
-    }
101
-
102
-    private function getTotals(): array
103
-    {
104
-        $currencyCode = $this->document->currency_code ?? CurrencyAccessor::getDefaultCurrency();
105
-
106
-        return [
107
-            'subtotal' => CurrencyConverter::formatToMoney($this->document->subtotal, $currencyCode),
108
-            'discount' => CurrencyConverter::formatToMoney($this->document->discount_total, $currencyCode),
109
-            'tax' => CurrencyConverter::formatToMoney($this->document->tax_total, $currencyCode),
110
-            'total' => CurrencyConverter::formatToMoney($this->document->total, $currencyCode),
111
-            'amount_due' => $this->document->amount_due ? CurrencyConverter::formatToMoney($this->document->amount_due, $currencyCode) : CurrencyConverter::formatToMoney($this->document->total, $currencyCode),
112
-        ];
113
-    }
114
-
115
-    private function getStyle(): array
116
-    {
117
-        /** @var DocumentDefault $settings */
118
-        $settings = $this->document->company->defaultInvoice;
119
-
120
-        return [
121
-            'accent_color' => $settings->accent_color ?? '#000000',
122
-            'show_logo' => $settings->show_logo ?? false,
123
-        ];
124
-    }
125
-}

+ 1
- 0
composer.json 查看文件

25
         "laravel/framework": "^11.0",
25
         "laravel/framework": "^11.0",
26
         "laravel/sanctum": "^4.0",
26
         "laravel/sanctum": "^4.0",
27
         "laravel/tinker": "^2.9",
27
         "laravel/tinker": "^2.9",
28
+        "spatie/laravel-view-models": "^1.6",
28
         "squirephp/model": "^3.4",
29
         "squirephp/model": "^3.4",
29
         "squirephp/repository": "^3.4",
30
         "squirephp/repository": "^3.4",
30
         "symfony/intl": "^6.3"
31
         "symfony/intl": "^6.3"

+ 72
- 3
composer.lock 查看文件

4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5
         "This file is @generated automatically"
5
         "This file is @generated automatically"
6
     ],
6
     ],
7
-    "content-hash": "095bb4040f9910ddd128bd53c0670a55",
7
+    "content-hash": "decc627f2a7bd0c6546114ebb1f500f9",
8
     "packages": [
8
     "packages": [
9
         {
9
         {
10
             "name": "akaunting/laravel-money",
10
             "name": "akaunting/laravel-money",
6551
             ],
6551
             ],
6552
             "time": "2024-12-30T13:13:39+00:00"
6552
             "time": "2024-12-30T13:13:39+00:00"
6553
         },
6553
         },
6554
+        {
6555
+            "name": "spatie/laravel-view-models",
6556
+            "version": "1.6.0",
6557
+            "source": {
6558
+                "type": "git",
6559
+                "url": "https://github.com/spatie/laravel-view-models.git",
6560
+                "reference": "c8c74e26e2cc78d04e581867ce74c8b772279015"
6561
+            },
6562
+            "dist": {
6563
+                "type": "zip",
6564
+                "url": "https://api.github.com/repos/spatie/laravel-view-models/zipball/c8c74e26e2cc78d04e581867ce74c8b772279015",
6565
+                "reference": "c8c74e26e2cc78d04e581867ce74c8b772279015",
6566
+                "shasum": ""
6567
+            },
6568
+            "require": {
6569
+                "illuminate/support": "^8.0|^9.0|^10.0|^11.0",
6570
+                "php": "^7.3|^8.0"
6571
+            },
6572
+            "require-dev": {
6573
+                "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0",
6574
+                "pestphp/pest": "^1.22|^2.34"
6575
+            },
6576
+            "type": "library",
6577
+            "extra": {
6578
+                "laravel": {
6579
+                    "providers": [
6580
+                        "Spatie\\ViewModels\\Providers\\ViewModelsServiceProvider"
6581
+                    ]
6582
+                }
6583
+            },
6584
+            "autoload": {
6585
+                "psr-4": {
6586
+                    "Spatie\\ViewModels\\": "src"
6587
+                }
6588
+            },
6589
+            "notification-url": "https://packagist.org/downloads/",
6590
+            "license": [
6591
+                "MIT"
6592
+            ],
6593
+            "authors": [
6594
+                {
6595
+                    "name": "Brent Roose",
6596
+                    "email": "brent@spatie.be",
6597
+                    "homepage": "https://spatie.be",
6598
+                    "role": "Developer"
6599
+                }
6600
+            ],
6601
+            "description": "View models in Laravel",
6602
+            "homepage": "https://github.com/spatie/laravel-view-models",
6603
+            "keywords": [
6604
+                "laravel-view-models",
6605
+                "spatie"
6606
+            ],
6607
+            "support": {
6608
+                "issues": "https://github.com/spatie/laravel-view-models/issues",
6609
+                "source": "https://github.com/spatie/laravel-view-models/tree/1.6.0"
6610
+            },
6611
+            "funding": [
6612
+                {
6613
+                    "url": "https://spatie.be/open-source/support-us",
6614
+                    "type": "custom"
6615
+                },
6616
+                {
6617
+                    "url": "https://github.com/spatie",
6618
+                    "type": "github"
6619
+                }
6620
+            ],
6621
+            "time": "2024-03-13T17:58:20+00:00"
6622
+        },
6554
         {
6623
         {
6555
             "name": "squirephp/model",
6624
             "name": "squirephp/model",
6556
             "version": "v3.7.0",
6625
             "version": "v3.7.0",
13398
     ],
13467
     ],
13399
     "aliases": [],
13468
     "aliases": [],
13400
     "minimum-stability": "stable",
13469
     "minimum-stability": "stable",
13401
-    "stability-flags": [],
13470
+    "stability-flags": {},
13402
     "prefer-stable": true,
13471
     "prefer-stable": true,
13403
     "prefer-lowest": false,
13472
     "prefer-lowest": false,
13404
     "platform": {
13473
     "platform": {
13406
         "ext-bcmath": "*",
13475
         "ext-bcmath": "*",
13407
         "ext-intl": "*"
13476
         "ext-intl": "*"
13408
     },
13477
     },
13409
-    "platform-dev": [],
13478
+    "platform-dev": {},
13410
     "plugin-api-version": "2.6.0"
13479
     "plugin-api-version": "2.6.0"
13411
 }
13480
 }

+ 27
- 0
resources/css/filament/company/custom-data-table.css 查看文件

1
+.es-table__header-ctn, .es-table__footer-ctn {
2
+    @apply divide-y divide-gray-200 dark:divide-white/10 min-h-12;
3
+}
4
+
5
+.es-table .es-table__rowgroup td:first-child {
6
+    padding-left: 3rem;
7
+}
8
+
9
+.es-table .es-table__rowgroup .es-table__row > td:nth-child(2) {
10
+    word-wrap: break-word;
11
+    word-break: break-word;
12
+    white-space: normal;
13
+}
14
+
15
+.es-table .es-table__rowgroup .es-table__row > td:nth-child(3) {
16
+    word-wrap: break-word;
17
+    word-break: break-word;
18
+    white-space: normal;
19
+}
20
+
21
+.es-table .es-table__rowgroup .es-table__row > td:nth-child(4) {
22
+    white-space: nowrap;
23
+}
24
+
25
+.es-table .es-table__rowgroup .es-table__row > td:last-child {
26
+    padding-right: 3rem;
27
+}

+ 125
- 0
resources/css/filament/company/custom-section.css 查看文件

1
+.fi-custom-section {
2
+    &:not(.fi-section-not-contained) {
3
+        & .fi-section-content {
4
+            @apply p-6;
5
+        }
6
+
7
+        & .fi-section-footer {
8
+            @apply border-t border-gray-200 px-6 py-4 dark:border-white/10;
9
+        }
10
+
11
+        &:not(.fi-aside) {
12
+            @apply rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10;
13
+
14
+            & .fi-section-header {
15
+                @apply px-6 py-4;
16
+            }
17
+
18
+            &.fi-section-has-header {
19
+                & .fi-section-content-ctn {
20
+                    @apply border-t border-gray-200 dark:border-white/10;
21
+                }
22
+            }
23
+        }
24
+
25
+        &.fi-aside {
26
+            & .fi-section-content-ctn {
27
+                @apply rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 md:col-span-2;
28
+            }
29
+        }
30
+
31
+        &.fi-compact {
32
+            &:not(.fi-aside) {
33
+                & .fi-section-header {
34
+                    @apply px-4 py-2.5;
35
+                }
36
+            }
37
+
38
+            & .fi-section-content {
39
+                @apply p-4;
40
+            }
41
+
42
+            & .fi-section-footer {
43
+                @apply px-4 py-2.5;
44
+            }
45
+        }
46
+    }
47
+
48
+    &.fi-section-not-contained:not(.fi-aside) {
49
+        @apply grid gap-y-4;
50
+
51
+        & .fi-section-header {
52
+            @apply py-2;
53
+        }
54
+
55
+        & .fi-section-content-ctn {
56
+            @apply grid gap-y-4;
57
+        }
58
+
59
+        &.fi-compact {
60
+            @apply gap-y-2.5;
61
+
62
+            & .fi-section-content-ctn {
63
+                @apply gap-y-2.5;
64
+            }
65
+        }
66
+    }
67
+
68
+    &.fi-aside {
69
+        @apply grid grid-cols-1 items-start gap-x-6 gap-y-4 md:grid-cols-3;
70
+    }
71
+
72
+    &.fi-collapsible {
73
+        & .fi-section-header {
74
+            @apply cursor-pointer;
75
+        }
76
+    }
77
+
78
+    &.fi-collapsed {
79
+        & .fi-section-collapse-btn {
80
+            @apply rotate-180;
81
+        }
82
+
83
+        & .fi-section-content-ctn {
84
+            @apply invisible absolute h-0 overflow-hidden border-none;
85
+        }
86
+    }
87
+
88
+    &.fi-section-has-content-before {
89
+        & .fi-section-content-ctn {
90
+            @apply md:order-first;
91
+        }
92
+    }
93
+
94
+    & .fi-section-header {
95
+        @apply flex items-center gap-3;
96
+    }
97
+
98
+    & .fi-section-header-icon {
99
+        @apply size-6 self-start text-gray-400 dark:text-gray-500;
100
+
101
+        &.fi-color-custom {
102
+            @apply text-custom-500 dark:text-custom-400;
103
+        }
104
+
105
+        &.fi-size-sm {
106
+            @apply mt-1 size-4;
107
+        }
108
+
109
+        &.fi-size-md {
110
+            @apply mt-0.5 size-5;
111
+        }
112
+    }
113
+
114
+    & .fi-section-header-text-ctn {
115
+        @apply grid flex-1 gap-y-1;
116
+    }
117
+
118
+    & .fi-section-header-heading {
119
+        @apply text-base font-semibold leading-6 text-gray-950 dark:text-white;
120
+    }
121
+
122
+    & .fi-section-header-description {
123
+        @apply overflow-hidden break-words text-sm text-gray-500 dark:text-gray-400;
124
+    }
125
+}

+ 92
- 0
resources/css/filament/company/form-fields.css 查看文件

1
+/* Filament Repeater Styles */
2
+.fi-fo-repeater.uncontained .fi-fo-repeater-item {
3
+    @apply divide-y-0 rounded-none bg-none shadow-none ring-0 ring-gray-950/5 dark:divide-white/10 dark:bg-white/5 dark:ring-white/10;
4
+
5
+    .fi-fo-repeater-item-header {
6
+        @apply px-0;
7
+
8
+        > h4 {
9
+            @apply text-base font-semibold leading-6 text-gray-950 dark:text-white;
10
+        }
11
+    }
12
+
13
+    .fi-fo-repeater-item-content {
14
+        @apply py-4 px-0;
15
+    }
16
+}
17
+
18
+.fi-fo-repeater-item {
19
+    @apply divide-y divide-gray-200 rounded-xl bg-white dark:bg-gray-900;
20
+}
21
+
22
+/* Report Field Styles */
23
+.fi-fo-field-wrp.report-hidden-label > div.grid.gap-y-2 > div.flex.items-center {
24
+    @apply hidden;
25
+}
26
+
27
+.fi-fo-field-wrp.report-hidden-label {
28
+    @apply lg:mt-8;
29
+}
30
+
31
+/* Choices.js select field overrides */
32
+.choices__list.choices__list--single {
33
+    @apply w-full;
34
+}
35
+
36
+.choices:focus-visible {
37
+    outline: none;
38
+}
39
+
40
+.choices__group {
41
+    @apply text-gray-900 dark:text-white font-semibold;
42
+}
43
+
44
+.choices[data-type="select-one"] .choices__inner {
45
+    line-height: 1.5;
46
+    display: flex;
47
+    align-items: center;
48
+    min-height: 2.25rem;
49
+    box-sizing: border-box;
50
+}
51
+
52
+.choices:not(.is-disabled) .choices__item {
53
+    cursor: pointer;
54
+}
55
+
56
+/* Table Repeater Styles */
57
+.table-repeater-container {
58
+    @apply rounded-none ring-0;
59
+}
60
+
61
+.table-repeater-component {
62
+    @apply space-y-10;
63
+}
64
+
65
+.table-repeater-component ul {
66
+    @apply justify-start;
67
+}
68
+
69
+.table-repeater-row {
70
+    @apply divide-x-0 !important;
71
+}
72
+
73
+.table-repeater-column {
74
+    @apply py-2 !important;
75
+}
76
+
77
+.table-repeater-header {
78
+    @apply rounded-t-none !important;
79
+}
80
+
81
+.table-repeater-rows-wrapper {
82
+    @apply divide-gray-300 last:border-b last:border-gray-300 dark:divide-white/20 dark:last:border-white/20;
83
+}
84
+
85
+.table-repeater-header tr {
86
+    @apply divide-x-0 text-base sm:text-sm sm:leading-6 !important;
87
+}
88
+
89
+.table-repeater-header-column {
90
+    @apply ps-3 pe-3 font-semibold bg-gray-200 dark:bg-gray-800 rounded-none !important;
91
+}
92
+

+ 18
- 0
resources/css/filament/company/modal.css 查看文件

1
+/* Journal Entry Modal Styles */
2
+.fi-modal.fi-width-screen {
3
+    .fi-modal-header {
4
+        @apply xl:px-80;
5
+
6
+        .absolute.end-4.top-4 {
7
+            @apply xl:end-80;
8
+        }
9
+    }
10
+
11
+    .fi-modal-content {
12
+        @apply xl:px-80;
13
+    }
14
+
15
+    .fi-modal-footer {
16
+        @apply xl:px-80;
17
+    }
18
+}

+ 15
- 0
resources/css/filament/company/report-card.css 查看文件

1
+.es-report-card {
2
+    @apply md:!grid-cols-2;
3
+
4
+    .fi-fo-component-ctn {
5
+        @apply divide-y divide-gray-200 dark:divide-white/10 !gap-0;
6
+    }
7
+
8
+    .fi-section-content-ctn {
9
+        @apply md:!col-span-1;
10
+    }
11
+
12
+    .fi-section-content {
13
+        @apply !p-0;
14
+    }
15
+}

+ 11
- 176
resources/css/filament/company/theme.css 查看文件

1
 @import '/vendor/filament/filament/resources/css/theme.css';
1
 @import '/vendor/filament/filament/resources/css/theme.css';
2
-@import 'tooltip.css';
3
 @import '/vendor/awcodes/filament-table-repeater/resources/css/plugin.css';
2
 @import '/vendor/awcodes/filament-table-repeater/resources/css/plugin.css';
3
+@import 'custom-data-table.css';
4
+@import 'custom-section.css';
5
+@import 'form-fields.css';
6
+@import 'modal.css';
7
+@import 'report-card.css';
8
+@import 'tooltip.css';
9
+@import 'top-navigation.css';
4
 
10
 
5
 @config 'tailwind.config.js';
11
 @config 'tailwind.config.js';
6
 
12
 
7
-.fi-fo-repeater.uncontained .fi-fo-repeater-item {
8
-    @apply divide-y-0 rounded-none bg-none shadow-none ring-0 ring-gray-950/5 dark:divide-white/10 dark:bg-white/5 dark:ring-white/10;
9
-
10
-    .fi-fo-repeater-item-header {
11
-        @apply px-0;
12
-
13
-        > h4 {
14
-            @apply text-base font-semibold leading-6 text-gray-950 dark:text-white;
15
-        }
16
-    }
17
-
18
-    .fi-fo-repeater-item-content {
19
-        @apply py-4 px-0;
20
-    }
21
-}
22
-
23
-.fi-fo-field-wrp.report-hidden-label > div.grid.gap-y-2 > div.flex.items-center {
24
-    @apply hidden;
25
-}
26
-
27
-.fi-fo-repeater-item {
28
-    @apply divide-y divide-gray-200 rounded-xl bg-white dark:bg-gray-900;
29
-}
30
-
31
-.fi-fo-field-wrp.report-hidden-label {
32
-    @apply lg:mt-8;
33
-}
34
-
35
-.choices__list.choices__list--single {
36
-    @apply w-full;
37
-}
38
-
39
-.choices:focus-visible {
40
-    outline: none;
41
-}
42
-
43
-.choices__group {
44
-    @apply text-gray-900 dark:text-white font-semibold;
45
-}
46
-
47
-.choices[data-type="select-one"] .choices__inner {
48
-    line-height: 1.5;
49
-    display: flex;
50
-    align-items: center;
51
-    min-height: 2.25rem;
52
-    box-sizing: border-box;
53
-}
54
-
55
-.choices:not(.is-disabled) .choices__item {
56
-    cursor: pointer;
57
-}
58
-
59
-.table-repeater-container {
60
-    @apply rounded-none ring-0;
61
-}
62
-
63
-.table-repeater-component {
64
-    @apply space-y-10;
65
-}
66
-
67
-.table-repeater-component ul {
68
-    @apply justify-start;
69
-}
70
-
71
-.table-repeater-row {
72
-    @apply divide-x-0 !important;
73
-}
74
-
75
-.table-repeater-column {
76
-    @apply py-2 !important;
77
-}
78
-
79
-.table-repeater-header {
80
-    @apply rounded-t-none !important;
81
-}
82
-
83
-.table-repeater-rows-wrapper {
84
-    @apply divide-gray-300 last:border-b last:border-gray-300 dark:divide-white/20 dark:last:border-white/20;
85
-}
86
-
87
-.table-repeater-header tr {
88
-    @apply divide-x-0 text-base sm:text-sm sm:leading-6 !important;
89
-}
90
-
91
-.table-repeater-header-column {
92
-    @apply ps-3 pe-3 font-semibold bg-gray-200 dark:bg-gray-800 rounded-none !important;
93
-}
94
-
95
-.es-report-card {
96
-    @apply md:!grid-cols-2;
97
-
98
-    .fi-fo-component-ctn {
99
-        @apply divide-y divide-gray-200 dark:divide-white/10 !gap-0;
100
-    }
101
-
102
-    .fi-section-content-ctn {
103
-        @apply md:!col-span-1;
104
-    }
105
-
106
-    .fi-section-content {
107
-        @apply !p-0;
108
-    }
109
-}
110
-
111
-.fi-modal.fi-width-screen {
112
-    .fi-modal-header {
113
-        @apply xl:px-80;
114
-
115
-        .absolute.end-4.top-4 {
116
-            @apply xl:end-80;
117
-        }
118
-    }
119
-
120
-    .fi-modal-content {
121
-        @apply xl:px-80;
122
-    }
123
-
124
-    .fi-modal-footer {
125
-        @apply xl:px-80;
126
-    }
127
-}
128
-
129
-.es-table__header-ctn, .es-table__footer-ctn {
130
-    @apply divide-y divide-gray-200 dark:divide-white/10 min-h-12;
131
-}
132
-
133
-.es-table .es-table__rowgroup td:first-child {
134
-    padding-left: 3rem;
135
-}
136
-
137
-.es-table .es-table__rowgroup .es-table__row > td:nth-child(2) {
138
-    word-wrap: break-word;
139
-    word-break: break-word;
140
-    white-space: normal;
141
-}
142
-
143
-.es-table .es-table__rowgroup .es-table__row > td:nth-child(3) {
144
-    word-wrap: break-word;
145
-    word-break: break-word;
146
-    white-space: normal;
147
-}
148
-
149
-.es-table .es-table__rowgroup .es-table__row > td:nth-child(4) {
150
-    white-space: nowrap;
151
-}
152
-
153
-.es-table .es-table__rowgroup .es-table__row > td:last-child {
154
-    padding-right: 3rem;
155
-}
156
-
157
 .fi-ta-empty-state-icon-ctn {
13
 .fi-ta-empty-state-icon-ctn {
158
     @apply bg-platinum;
14
     @apply bg-platinum;
159
 }
15
 }
160
 
16
 
17
+.fi-badge {
18
+    display: inline-flex;
19
+}
20
+
161
 :not(.dark) .fi-body {
21
 :not(.dark) .fi-body {
162
     position: relative;
22
     position: relative;
163
     background-color: #E8E9EB;
23
     background-color: #E8E9EB;
180
     pointer-events: none;
40
     pointer-events: none;
181
     z-index: -1;
41
     z-index: -1;
182
 }
42
 }
183
-
184
-.fi-topbar > nav, .fi-sidebar-header {
185
-    @apply bg-transparent ring-0 shadow-none !important;
186
-    transition: background-color 0.3s, top 0.3s;
187
-}
188
-
189
-.fi-topbar > nav.topbar-hovered, .fi-sidebar-header.topbar-hovered {
190
-    background-color: rgba(255, 255, 255, 0.75) !important;
191
-}
192
-
193
-:is(.dark .fi-topbar > nav.topbar-hovered, .dark .fi-sidebar-header.topbar-hovered) {
194
-    @apply bg-gray-900/75 !important;
195
-}
196
-
197
-.fi-topbar > nav.topbar-scrolled, .fi-sidebar-header.topbar-scrolled {
198
-    background-color: rgba(255, 255, 255, 0.5) !important;
199
-}
200
-
201
-:is(.dark .fi-topbar > nav.topbar-scrolled, .dark .fi-sidebar-header.topbar-scrolled) {
202
-    @apply bg-gray-900/50 !important;
203
-}
204
-
205
-.fi-badge {
206
-    display: inline-flex;
207
-}

+ 20
- 0
resources/css/filament/company/top-navigation.css 查看文件

1
+.fi-topbar > nav, .fi-sidebar-header {
2
+    @apply bg-transparent ring-0 shadow-none !important;
3
+    transition: background-color 0.3s, top 0.3s;
4
+}
5
+
6
+.fi-topbar > nav.topbar-hovered, .fi-sidebar-header.topbar-hovered {
7
+    background-color: rgba(255, 255, 255, 0.75) !important;
8
+}
9
+
10
+:is(.dark .fi-topbar > nav.topbar-hovered, .dark .fi-sidebar-header.topbar-hovered) {
11
+    @apply bg-gray-900/75 !important;
12
+}
13
+
14
+.fi-topbar > nav.topbar-scrolled, .fi-sidebar-header.topbar-scrolled {
15
+    background-color: rgba(255, 255, 255, 0.5) !important;
16
+}
17
+
18
+:is(.dark .fi-topbar > nav.topbar-scrolled, .dark .fi-sidebar-header.topbar-scrolled) {
19
+    @apply bg-gray-900/50 !important;
20
+}

+ 2
- 1
resources/data/lang/en.json 查看文件

222
     "Approve": "Approve",
222
     "Approve": "Approve",
223
     "Frequency": "Frequency",
223
     "Frequency": "Frequency",
224
     "Schedule Bounds": "Schedule Bounds",
224
     "Schedule Bounds": "Schedule Bounds",
225
-    "Time Zone": "Time Zone"
225
+    "Time Zone": "Time Zone",
226
+    "Dates & Time": "Dates & Time"
226
 }
227
 }

+ 16
- 46
resources/views/components/custom-section.blade.php 查看文件

66
     @endif
66
     @endif
67
     {{
67
     {{
68
         $attributes->class([
68
         $attributes->class([
69
-            'fi-section',
70
-            'fi-aside grid grid-cols-1 items-start gap-x-6 gap-y-4 md:grid-cols-3' => $aside && $contained,
71
-            'fi-aside grid grid-cols-1 items-start gap-x-6 gap-y-4 md:grid-cols-3 pt-4' => $aside && ! $contained,
72
-            'rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10' => $contained && ! $aside,
69
+            'fi-custom-section',
70
+            'fi-section-not-contained' => ! $contained,
71
+            'fi-section-has-content-before' => $contentBefore,
72
+            'fi-section-has-header' => $hasHeader,
73
+            'fi-aside' => $aside,
74
+            'fi-compact' => $compact,
75
+            'fi-collapsible' => $collapsible,
73
         ])
76
         ])
74
     }}
77
     }}
75
 >
78
 >
78
             @if ($collapsible)
81
             @if ($collapsible)
79
                 x-on:click="isCollapsed = ! isCollapsed"
82
                 x-on:click="isCollapsed = ! isCollapsed"
80
             @endif
83
             @endif
81
-            @class([
82
-                'fi-section-header flex flex-col gap-3',
83
-                'cursor-pointer' => $collapsible,
84
-                'px-6 py-4' => $contained && ! $aside,
85
-                'px-4 py-2.5' => $compact && ! $aside,
86
-                'py-4' => ! $compact && ! $aside,
87
-            ])
84
+            class="fi-section-header"
88
         >
85
         >
89
             <div class="flex items-center gap-3">
86
             <div class="flex items-center gap-3">
90
                 @if ($hasIcon)
87
                 @if ($hasIcon)
91
                     <x-filament::icon
88
                     <x-filament::icon
92
                         :icon="$icon"
89
                         :icon="$icon"
93
                         @class([
90
                         @class([
94
-                            'fi-section-header-icon self-start',
91
+                            'fi-section-header-icon',
95
                             match ($iconColor) {
92
                             match ($iconColor) {
96
-                                'gray' => 'text-gray-400 dark:text-gray-500',
97
-                                default => 'fi-color-custom text-custom-500 dark:text-custom-400',
93
+                                'gray' => null,
94
+                                default => 'fi-color-custom',
98
                             },
95
                             },
99
                             is_string($iconColor) ? "fi-color-{$iconColor}" : null,
96
                             is_string($iconColor) ? "fi-color-{$iconColor}" : null,
100
-                            match ($iconSize) {
101
-                                IconSize::Small, 'sm' => 'h-4 w-4 mt-1',
102
-                                IconSize::Medium, 'md' => 'h-5 w-5 mt-0.5',
103
-                                IconSize::Large, 'lg' => 'h-6 w-6',
104
-                                default => $iconSize,
105
-                            },
97
+                            ($iconSize instanceof IconSize) ? "fi-size-{$iconSize->value}" : (is_string($iconSize) ? $iconSize : null),
106
                         ])
98
                         ])
107
                         @style([
99
                         @style([
108
                             \Filament\Support\get_color_css_variables(
100
                             \Filament\Support\get_color_css_variables(
115
                 @endif
107
                 @endif
116
 
108
 
117
                 @if ($hasHeading || $hasDescription)
109
                 @if ($hasHeading || $hasDescription)
118
-                    <div class="grid flex-1 gap-y-1">
110
+                    <div class="fi-section-header-text-ctn">
119
                         @if ($hasHeading)
111
                         @if ($hasHeading)
120
                             <x-filament::section.heading>
112
                             <x-filament::section.heading>
121
                                 {{ $heading }}
113
                                 {{ $heading }}
171
             @if ($collapsed || $persistCollapsed)
163
             @if ($collapsed || $persistCollapsed)
172
                 x-cloak
164
                 x-cloak
173
             @endif
165
             @endif
174
-            x-bind:class="{ 'invisible h-0 overflow-y-hidden border-none': isCollapsed }"
175
         @endif
166
         @endif
176
-        @class([
177
-            'fi-section-content-ctn',
178
-            'border-t border-gray-200 dark:border-white/10' => $hasHeader && ! $aside && $contained,
179
-            'rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 md:col-span-2' => $aside && $contained,
180
-            'md:col-span-2' => $aside && ! $contained,
181
-            'md:order-first' => $contentBefore,
182
-        ])
167
+        class="fi-section-content-ctn"
183
     >
168
     >
184
-        <div
185
-            @class([
186
-                'fi-section-content',
187
-                'pt-4' => ! $contained && ! $aside,
188
-                'p-4' => $compact && $contained,
189
-                'p-6' => ! $compact && $contained,
190
-            ])
191
-        >
169
+        <div class="fi-section-content">
192
             {{ $slot }}
170
             {{ $slot }}
193
         </div>
171
         </div>
194
 
172
 
195
         @if ($hasFooterActions)
173
         @if ($hasFooterActions)
196
-            <footer
197
-                @class([
198
-                    'fi-section-footer',
199
-                    'border-t border-gray-200 dark:border-white/10' => $contained,
200
-                    'mt-6' => ! $contained,
201
-                    'px-6 py-4' => ! $compact && $contained,
202
-                    'px-4 py-2.5' => $compact && $contained,
203
-                ])
204
-            >
174
+            <footer class="fi-section-footer">
205
                 <x-filament::actions
175
                 <x-filament::actions
206
                     :actions="$footerActions"
176
                     :actions="$footerActions"
207
                     :alignment="$footerActionsAlignment"
177
                     :alignment="$footerActionsAlignment"

+ 49
- 55
resources/views/filament/infolists/components/document-preview.blade.php 查看文件

1
 @php
1
 @php
2
-    use App\View\Models\DocumentPreviewViewModel;
3
-    use App\Enums\Accounting\DocumentType;
4
-
5
-    $type = $getType();
6
-    $viewModel = new DocumentPreviewViewModel($getRecord(), $type);
7
-    extract($viewModel->buildViewData(), EXTR_SKIP);
2
+    $document = \App\DTO\DocumentDTO::fromModel($getRecord());
8
 @endphp
3
 @endphp
9
 
4
 
10
 <div {{ $attributes }}>
5
 <div {{ $attributes }}>
13
         <x-company.invoice.header class="bg-gray-800 h-24">
8
         <x-company.invoice.header class="bg-gray-800 h-24">
14
             <!-- Logo -->
9
             <!-- Logo -->
15
             <div class="w-2/3">
10
             <div class="w-2/3">
16
-                @if($logo && $style['show_logo'])
17
-                    <x-company.invoice.logo class="ml-8" :src="$logo"/>
11
+                @if($document->logo && $document->showLogo)
12
+                    <x-company.invoice.logo class="ml-8" :src="$document->logo"/>
18
                 @endif
13
                 @endif
19
             </div>
14
             </div>
20
 
15
 
21
             <!-- Ribbon Container -->
16
             <!-- Ribbon Container -->
22
             <div class="w-1/3 absolute right-0 top-0 p-3 h-32 flex flex-col justify-end rounded-bl-sm"
17
             <div class="w-1/3 absolute right-0 top-0 p-3 h-32 flex flex-col justify-end rounded-bl-sm"
23
-                 style="background: {{ $style['accent_color'] }};">
24
-                @if($header)
25
-                    <h1 class="text-4xl font-bold text-white text-center uppercase">{{ $header }}</h1>
18
+                 style="background: {{ $document->accentColor }};">
19
+                @if($document->header)
20
+                    <h1 class="text-4xl font-bold text-white text-center uppercase">{{ $document->header }}</h1>
26
                 @endif
21
                 @endif
27
             </div>
22
             </div>
28
         </x-company.invoice.header>
23
         </x-company.invoice.header>
30
         <!-- Company Details -->
25
         <!-- Company Details -->
31
         <x-company.invoice.metadata class="modern-template-metadata space-y-8">
26
         <x-company.invoice.metadata class="modern-template-metadata space-y-8">
32
             <div class="text-sm">
27
             <div class="text-sm">
33
-                <h2 class="text-lg font-semibold">{{ $company['name'] }}</h2>
34
-                @if($company['address'] && $company['city'] && $company['state'] && $company['zip_code'])
35
-                    <p>{{ $company['address'] }}</p>
36
-                    <p>{{ $company['city'] }}
37
-                        , {{ $company['state'] }} {{ $company['zip_code'] }}</p>
38
-                    <p>{{ $company['country'] }}</p>
28
+                <h2 class="text-lg font-semibold">{{ $document->company->name }}</h2>
29
+                @if($document->company->address && $document->company->city && $document->company->state && $document->company->zipCode)
30
+                    <p>{{ $document->company->address }}</p>
31
+                    <p>{{ $document->company->city }}, {{ $document->company->state }} {{ $document->company->zipCode }}</p>
32
+                    <p>{{ $document->company->country }}</p>
39
                 @endif
33
                 @endif
40
             </div>
34
             </div>
41
 
35
 
44
                 <div class="text-sm tracking-tight">
38
                 <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>
39
                     <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"
40
                     <p class="text-base font-bold"
47
-                       style="color: {{ $style['accent_color'] }}">{{ $client['name'] }}</p>
41
+                       style="color: {{ $document->accentColor }}">{{ $document->client->name }}</p>
48
 
42
 
49
-                    @if($client['address_line_1'])
50
-                        <p>{{ $client['address_line_1'] }}</p>
43
+                    @if($document->client->addressLine1)
44
+                        <p>{{ $document->client->addressLine1 }}</p>
51
 
45
 
52
-                        @if($client['address_line_2'])
53
-                            <p>{{ $client['address_line_2'] }}</p>
46
+                        @if($document->client->addressLine2)
47
+                            <p>{{ $document->client->addressLine2 }}</p>
54
                         @endif
48
                         @endif
55
                         <p>
49
                         <p>
56
-                            {{ $client['city'] }}{{ $client['state'] ? ', ' . $client['state'] : '' }}
57
-                            {{ $client['postal_code'] }}
50
+                            {{ $document->client->city }}{{ $document->client->state ? ', ' . $document->client->state: '' }}
51
+                            {{ $document->client->postalCode }}
58
                         </p>
52
                         </p>
59
-                        @if($client['country'])
60
-                            <p>{{ $client['country'] }}</p>
53
+                        @if($document->client->country)
54
+                            <p>{{ $document->client->country }}</p>
61
                         @endif
55
                         @endif
62
                     @endif
56
                     @endif
63
                 </div>
57
                 </div>
66
                     <table class="min-w-full">
60
                     <table class="min-w-full">
67
                         <tbody>
61
                         <tbody>
68
                         <tr>
62
                         <tr>
69
-                            <td class="font-semibold text-right pr-2">{{ $labels['number'] }}:</td>
70
-                            <td class="text-left pl-2">{{ $metadata['number'] }}</td>
63
+                            <td class="font-semibold text-right pr-2">{{ $document->label->number }}:</td>
64
+                            <td class="text-left pl-2">{{ $document->number }}</td>
71
                         </tr>
65
                         </tr>
72
-                        @if($metadata['reference_number'])
66
+                        @if($document->referenceNumber)
73
                             <tr>
67
                             <tr>
74
-                                <td class="font-semibold text-right pr-2">{{ $labels['reference_number'] }}:</td>
75
-                                <td class="text-left pl-2">{{ $metadata['reference_number'] }}</td>
68
+                                <td class="font-semibold text-right pr-2">{{ $document->label->referenceNumber }}:</td>
69
+                                <td class="text-left pl-2">{{ $document->referenceNumber }}</td>
76
                             </tr>
70
                             </tr>
77
                         @endif
71
                         @endif
78
                         <tr>
72
                         <tr>
79
-                            <td class="font-semibold text-right pr-2">{{ $labels['date'] }}:</td>
80
-                            <td class="text-left pl-2">{{ $metadata['date'] }}</td>
73
+                            <td class="font-semibold text-right pr-2">{{ $document->label->date }}:</td>
74
+                            <td class="text-left pl-2">{{ $document->date }}</td>
81
                         </tr>
75
                         </tr>
82
                         <tr>
76
                         <tr>
83
-                            <td class="font-semibold text-right pr-2">{{ $labels['due_date'] }}:</td>
84
-                            <td class="text-left pl-2">{{ $metadata['due_date'] }}</td>
77
+                            <td class="font-semibold text-right pr-2">{{ $document->label->dueDate }}:</td>
78
+                            <td class="text-left pl-2">{{ $document->dueDate }}</td>
85
                         </tr>
79
                         </tr>
86
                         </tbody>
80
                         </tbody>
87
                     </table>
81
                     </table>
101
                 </tr>
95
                 </tr>
102
                 </thead>
96
                 </thead>
103
                 <tbody class="text-sm tracking-tight border-y-2">
97
                 <tbody class="text-sm tracking-tight border-y-2">
104
-                @foreach($lineItems as $index => $item)
98
+                @foreach($document->lineItems as $index => $item)
105
                     <tr @class(['bg-gray-100 dark:bg-gray-800' => $index % 2 === 0])>
99
                     <tr @class(['bg-gray-100 dark:bg-gray-800' => $index % 2 === 0])>
106
                         <td class="text-left pl-6 font-semibold py-3">
100
                         <td class="text-left pl-6 font-semibold py-3">
107
-                            {{ $item['name'] }}
108
-                            @if($item['description'])
109
-                                <div class="text-gray-600 font-normal line-clamp-2 mt-1">{{ $item['description'] }}</div>
101
+                            {{ $item->name }}
102
+                            @if($item->description)
103
+                                <div class="text-gray-600 font-normal line-clamp-2 mt-1">{{ $item->description }}</div>
110
                             @endif
104
                             @endif
111
                         </td>
105
                         </td>
112
-                        <td class="text-center py-3">{{ $item['quantity'] }}</td>
113
-                        <td class="text-right py-3">{{ $item['unit_price'] }}</td>
114
-                        <td class="text-right pr-6 py-3">{{ $item['subtotal'] }}</td>
106
+                        <td class="text-center py-3">{{ $item->quantity }}</td>
107
+                        <td class="text-right py-3">{{ $item->unitPrice }}</td>
108
+                        <td class="text-right pr-6 py-3">{{ $item->subtotal }}</td>
115
                     </tr>
109
                     </tr>
116
                 @endforeach
110
                 @endforeach
117
                 </tbody>
111
                 </tbody>
119
                 <tr>
113
                 <tr>
120
                     <td class="pl-6 py-2" colspan="2"></td>
114
                     <td class="pl-6 py-2" colspan="2"></td>
121
                     <td class="text-right font-semibold py-2">Subtotal:</td>
115
                     <td class="text-right font-semibold py-2">Subtotal:</td>
122
-                    <td class="text-right pr-6 py-2">{{ $totals['subtotal'] }}</td>
116
+                    <td class="text-right pr-6 py-2">{{ $document->subtotal }}</td>
123
                 </tr>
117
                 </tr>
124
-                @if($totals['discount'])
118
+                @if($document->discount)
125
                     <tr class="text-success-800 dark:text-success-600">
119
                     <tr class="text-success-800 dark:text-success-600">
126
                         <td class="pl-6 py-2" colspan="2"></td>
120
                         <td class="pl-6 py-2" colspan="2"></td>
127
                         <td class="text-right py-2">Discount:</td>
121
                         <td class="text-right py-2">Discount:</td>
128
                         <td class="text-right pr-6 py-2">
122
                         <td class="text-right pr-6 py-2">
129
-                            ({{ $totals['discount'] }})
123
+                            ({{ $document->discount }})
130
                         </td>
124
                         </td>
131
                     </tr>
125
                     </tr>
132
                 @endif
126
                 @endif
133
-                @if($totals['tax'])
127
+                @if($document->tax)
134
                     <tr>
128
                     <tr>
135
                         <td class="pl-6 py-2" colspan="2"></td>
129
                         <td class="pl-6 py-2" colspan="2"></td>
136
                         <td class="text-right py-2">Tax:</td>
130
                         <td class="text-right py-2">Tax:</td>
137
-                        <td class="text-right pr-6 py-2">{{ $totals['tax'] }}</td>
131
+                        <td class="text-right pr-6 py-2">{{ $document->tax }}</td>
138
                     </tr>
132
                     </tr>
139
                 @endif
133
                 @endif
140
                 <tr>
134
                 <tr>
141
                     <td class="pl-6 py-2" colspan="2"></td>
135
                     <td class="pl-6 py-2" colspan="2"></td>
142
                     <td class="text-right font-semibold border-t py-2">Total:</td>
136
                     <td class="text-right font-semibold border-t py-2">Total:</td>
143
-                    <td class="text-right border-t pr-6 py-2">{{ $totals['total'] }}</td>
137
+                    <td class="text-right border-t pr-6 py-2">{{ $document->total }}</td>
144
                 </tr>
138
                 </tr>
145
-                @if($totals['amount_due'])
139
+                @if($document->amountDue)
146
                     <tr>
140
                     <tr>
147
                         <td class="pl-6 py-2" colspan="2"></td>
141
                         <td class="pl-6 py-2" colspan="2"></td>
148
-                        <td class="text-right font-semibold border-t-4 border-double py-2">{{ $labels['amount_due'] }}
149
-                            ({{ $metadata['currency_code'] }}):
142
+                        <td class="text-right font-semibold border-t-4 border-double py-2">{{ $document->label->amountDue }}
143
+                            ({{ $document->currencyCode }}):
150
                         </td>
144
                         </td>
151
-                        <td class="text-right border-t-4 border-double pr-6 py-2">{{ $totals['amount_due'] }}</td>
145
+                        <td class="text-right border-t-4 border-double pr-6 py-2">{{ $document->amountDue }}</td>
152
                     </tr>
146
                     </tr>
153
                 @endif
147
                 @endif
154
                 </tfoot>
148
                 </tfoot>
157
 
151
 
158
         <!-- Footer Notes -->
152
         <!-- Footer Notes -->
159
         <x-company.invoice.footer class="modern-template-footer tracking-tight">
153
         <x-company.invoice.footer class="modern-template-footer tracking-tight">
160
-            <h4 class="font-semibold px-6 text-sm" style="color: {{ $style['accent_color'] }}">
154
+            <h4 class="font-semibold px-6 text-sm" style="color: {{ $document->accentColor }}">
161
                 Terms & Conditions
155
                 Terms & Conditions
162
             </h4>
156
             </h4>
163
             <span class="border-t-2 my-2 border-gray-300 block w-full"></span>
157
             <span class="border-t-2 my-2 border-gray-300 block w-full"></span>
164
             <div class="flex justify-between space-x-4 px-6 text-sm">
158
             <div class="flex justify-between space-x-4 px-6 text-sm">
165
-                <p class="w-1/2 break-words line-clamp-4">{{ $terms }}</p>
166
-                <p class="w-1/2 break-words line-clamp-4">{{ $footer }}</p>
159
+                <p class="w-1/2 break-words line-clamp-4">{{ $document->terms }}</p>
160
+                <p class="w-1/2 break-words line-clamp-4">{{ $document->footer }}</p>
167
             </div>
161
             </div>
168
         </x-company.invoice.footer>
162
         </x-company.invoice.footer>
169
     </x-company.invoice.container>
163
     </x-company.invoice.container>

Loading…
取消
儲存