Skip to content

Commit 43f94c4

Browse files
authored
Merge pull request #577 from underdogg-forks/feature/221-client-detail-view
[IP-221]: client detail view
2 parents 3a61722 + 80247a8 commit 43f94c4

19 files changed

Lines changed: 608 additions & 18 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Modules\Clients\Exceptions;
4+
5+
use RuntimeException;
6+
7+
class RelationHasLinkedRecordsException extends RuntimeException
8+
{
9+
public function __construct(string $message = '')
10+
{
11+
parent::__construct($message ?: trans('ip.cannot_delete_client_has_linked_records'));
12+
}
13+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Modules\Clients\Filament\Company\Resources\Relations\Pages;
4+
5+
use Filament\Actions\Action;
6+
use Filament\Actions\DeleteAction;
7+
use Filament\Actions\EditAction;
8+
use Filament\Notifications\Notification;
9+
use Filament\Resources\Pages\ViewRecord;
10+
use Modules\Clients\Exceptions\RelationHasLinkedRecordsException;
11+
use Modules\Clients\Filament\Company\Resources\Relations\RelationResource;
12+
use Modules\Clients\Services\RelationService;
13+
use Modules\Invoices\Filament\Company\Resources\Invoices\InvoiceResource;
14+
use Modules\Quotes\Filament\Company\Resources\Quotes\QuoteResource;
15+
16+
class ViewRelation extends ViewRecord
17+
{
18+
protected static string $resource = RelationResource::class;
19+
20+
protected function getHeaderActions(): array
21+
{
22+
return [
23+
Action::make('create_invoice')
24+
->label(trans('ip.create_invoice'))
25+
->icon('heroicon-o-document-plus')
26+
->url(fn (): string => InvoiceResource::getUrl('create', [
27+
'customer_id' => $this->getRecord()->id,
28+
])),
29+
30+
Action::make('create_quote')
31+
->label(trans('ip.create_quote'))
32+
->icon('heroicon-o-document-text')
33+
->url(fn (): string => QuoteResource::getUrl('create', [
34+
'customer_id' => $this->getRecord()->id,
35+
])),
36+
37+
EditAction::make(),
38+
39+
DeleteAction::make()
40+
->hidden(fn () => $this->getRecord()->hasLinkedRecords())
41+
->action(function (): void {
42+
try {
43+
app(RelationService::class)->deleteRelation($this->getRecord());
44+
$this->redirect(RelationResource::getUrl('index'));
45+
} catch (RelationHasLinkedRecordsException $e) {
46+
Notification::make()
47+
->title(trans('ip.cannot_delete_client_has_linked_records'))
48+
->danger()
49+
->send();
50+
}
51+
}),
52+
];
53+
}
54+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Modules\Clients\Filament\Company\Resources\Relations\RelationManagers;
4+
5+
use Filament\Resources\RelationManagers\RelationManager;
6+
use Filament\Tables\Columns\TextColumn;
7+
use Filament\Tables\Table;
8+
9+
class ExpensesRelationManager extends RelationManager
10+
{
11+
protected static string $relationship = 'expenses';
12+
13+
public function table(Table $table): Table
14+
{
15+
return $table
16+
->columns([
17+
TextColumn::make('expense_number')->sortable()->searchable(),
18+
TextColumn::make('expensed_at')->date()->sortable(),
19+
TextColumn::make('description')->limit(40)->searchable(),
20+
TextColumn::make('expense_amount')->money()->sortable(),
21+
]);
22+
}
23+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Modules\Clients\Filament\Company\Resources\Relations\RelationManagers;
4+
5+
use Filament\Actions\Action;
6+
use Filament\Resources\RelationManagers\RelationManager;
7+
use Filament\Tables\Columns\TextColumn;
8+
use Filament\Tables\Table;
9+
use Modules\Invoices\Enums\InvoiceStatus;
10+
use Modules\Invoices\Filament\Company\Resources\Invoices\InvoiceResource;
11+
12+
class InvoicesRelationManager extends RelationManager
13+
{
14+
protected static string $relationship = 'invoices';
15+
16+
public function table(Table $table): Table
17+
{
18+
return $table
19+
->columns([
20+
TextColumn::make('invoice_number')->sortable()->searchable(),
21+
TextColumn::make('invoice_status')
22+
->badge()
23+
->formatStateUsing(fn ($state) => ($state instanceof InvoiceStatus ? $state : InvoiceStatus::tryFrom($state))?->label())
24+
->color(fn ($state) => ($state instanceof InvoiceStatus ? $state : InvoiceStatus::tryFrom($state))?->color() ?? 'secondary')
25+
->sortable(),
26+
TextColumn::make('invoiced_at')->date()->sortable(),
27+
TextColumn::make('invoice_due_at')->date()->sortable(),
28+
TextColumn::make('invoice_total')->money()->sortable(),
29+
])
30+
->headerActions([
31+
Action::make('create_invoice')
32+
->label(trans('ip.create_invoice'))
33+
->url(fn (): string => InvoiceResource::getUrl('create', [
34+
'customer_id' => $this->getOwnerRecord()->id,
35+
])),
36+
]);
37+
}
38+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Modules\Clients\Filament\Company\Resources\Relations\RelationManagers;
4+
5+
use Filament\Resources\RelationManagers\RelationManager;
6+
use Filament\Tables\Columns\TextColumn;
7+
use Filament\Tables\Table;
8+
9+
class ProjectsRelationManager extends RelationManager
10+
{
11+
protected static string $relationship = 'projects';
12+
13+
public function table(Table $table): Table
14+
{
15+
return $table
16+
->columns([
17+
TextColumn::make('project_number')->sortable()->searchable(),
18+
TextColumn::make('project_name')->limit(40)->sortable()->searchable(),
19+
TextColumn::make('project_status')->sortable()->badge(),
20+
TextColumn::make('start_at')->date()->sortable(),
21+
TextColumn::make('end_at')->date()->sortable(),
22+
]);
23+
}
24+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Modules\Clients\Filament\Company\Resources\Relations\RelationManagers;
4+
5+
use Filament\Actions\Action;
6+
use Filament\Resources\RelationManagers\RelationManager;
7+
use Filament\Tables\Columns\TextColumn;
8+
use Filament\Tables\Table;
9+
use Modules\Quotes\Enums\QuoteStatus;
10+
use Modules\Quotes\Filament\Company\Resources\Quotes\QuoteResource;
11+
12+
class QuotesRelationManager extends RelationManager
13+
{
14+
protected static string $relationship = 'quotes';
15+
16+
public function table(Table $table): Table
17+
{
18+
return $table
19+
->columns([
20+
TextColumn::make('quote_number')->sortable()->searchable(),
21+
TextColumn::make('quote_status')
22+
->badge()
23+
->formatStateUsing(fn ($state) => ($state instanceof QuoteStatus ? $state : QuoteStatus::tryFrom($state))?->label())
24+
->color(fn ($state) => ($state instanceof QuoteStatus ? $state : QuoteStatus::tryFrom($state))?->color() ?? 'secondary')
25+
->sortable(),
26+
TextColumn::make('quoted_at')->date()->sortable(),
27+
TextColumn::make('quote_expires_at')->date()->sortable(),
28+
TextColumn::make('quote_total')->money()->sortable(),
29+
])
30+
->headerActions([
31+
Action::make('create_quote')
32+
->label(trans('ip.create_quote'))
33+
->url(fn (): string => QuoteResource::getUrl('create', [
34+
'customer_id' => $this->getOwnerRecord()->id,
35+
])),
36+
]);
37+
}
38+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Modules\Clients\Filament\Company\Resources\Relations\RelationManagers;
4+
5+
use Filament\Resources\RelationManagers\RelationManager;
6+
use Filament\Tables\Columns\TextColumn;
7+
use Filament\Tables\Table;
8+
9+
class TasksRelationManager extends RelationManager
10+
{
11+
protected static string $relationship = 'tasks';
12+
13+
public function table(Table $table): Table
14+
{
15+
return $table
16+
->columns([
17+
TextColumn::make('task_number')->sortable()->searchable(),
18+
TextColumn::make('task_name')->limit(40)->sortable()->searchable(),
19+
TextColumn::make('task_status')->sortable()->badge(),
20+
TextColumn::make('due_at')->date()->sortable(),
21+
TextColumn::make('task_price')->money()->sortable(),
22+
]);
23+
}
24+
}

Modules/Clients/Filament/Company/Resources/Relations/RelationResource.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,28 @@
33
namespace Modules\Clients\Filament\Company\Resources\Relations;
44

55
use BackedEnum;
6+
use Filament\Infolists\Components\TextEntry;
7+
use Filament\Schemas\Components\Grid;
8+
use Filament\Schemas\Components\Section;
69
use Filament\Schemas\Schema;
710
use Filament\Support\Icons\Heroicon;
811
use Filament\Tables\Table;
912
use Illuminate\Database\Eloquent\Model;
13+
use Modules\Clients\Enums\RelationStatus;
14+
use Modules\Clients\Enums\RelationType;
1015
use Modules\Clients\Filament\Company\Resources\Relations\Pages\ListRelations;
16+
use Modules\Clients\Filament\Company\Resources\Relations\Pages\ViewRelation;
17+
use Modules\Clients\Filament\Company\Resources\Relations\RelationManagers\ExpensesRelationManager;
18+
use Modules\Clients\Filament\Company\Resources\Relations\RelationManagers\InvoicesRelationManager;
19+
use Modules\Clients\Filament\Company\Resources\Relations\RelationManagers\ProjectsRelationManager;
20+
use Modules\Clients\Filament\Company\Resources\Relations\RelationManagers\QuotesRelationManager;
21+
use Modules\Clients\Filament\Company\Resources\Relations\RelationManagers\TasksRelationManager;
1122
use Modules\Clients\Filament\Company\Resources\Relations\Schemas\RelationForm;
1223
use Modules\Clients\Filament\Company\Resources\Relations\Tables\RelationsTable;
1324
use Modules\Clients\Models\Relation;
1425
use Modules\Core\Enums\Permission;
1526
use Modules\Core\Filament\Company\Resources\BaseResource;
27+
use Modules\Core\Helpers\EnumHelper;
1628

1729
class RelationResource extends BaseResource
1830
{
@@ -30,15 +42,48 @@ public static function table(Table $table): Table
3042
return RelationsTable::configure($table);
3143
}
3244

45+
public static function infolist(Schema $schema): Schema
46+
{
47+
return $schema->components([
48+
Section::make(trans('ip.client_information'))
49+
->schema([
50+
Grid::make(3)->schema([
51+
TextEntry::make('company_name')->label(trans('ip.company_name')),
52+
TextEntry::make('trading_name')->label(trans('ip.trading_name')),
53+
TextEntry::make('relation_number')->label(trans('ip.relation_number')),
54+
TextEntry::make('relation_type')
55+
->label(trans('ip.type'))
56+
->formatStateUsing(fn ($state) => EnumHelper::safeEnum(RelationType::class, $state)?->label() ?? '-')
57+
->badge(),
58+
TextEntry::make('relation_status')
59+
->label(trans('ip.status'))
60+
->formatStateUsing(fn ($state) => EnumHelper::safeEnum(RelationStatus::class, $state)?->label() ?? '-')
61+
->badge(),
62+
TextEntry::make('registered_at')->label(trans('ip.date'))->date(),
63+
TextEntry::make('coc_number')->label(trans('ip.coc_number')),
64+
TextEntry::make('vat_number')->label(trans('ip.vat_id')),
65+
TextEntry::make('currency_code')->label(trans('ip.currency')),
66+
]),
67+
]),
68+
]);
69+
}
70+
3371
public static function getRelations(): array
3472
{
35-
return [];
73+
return [
74+
InvoicesRelationManager::class,
75+
QuotesRelationManager::class,
76+
ExpensesRelationManager::class,
77+
TasksRelationManager::class,
78+
ProjectsRelationManager::class,
79+
];
3680
}
3781

3882
public static function getPages(): array
3983
{
4084
return [
4185
'index' => ListRelations::route('/'),
86+
'view' => ViewRelation::route('/{record}'),
4287
];
4388
}
4489

Modules/Clients/Filament/Company/Resources/Relations/Tables/RelationsTable.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@
22

33
namespace Modules\Clients\Filament\Company\Resources\Relations\Tables;
44

5+
use Filament\Actions\Action;
56
use Filament\Actions\ActionGroup;
67
use Filament\Actions\BulkActionGroup;
78
use Filament\Actions\DeleteAction;
89
use Filament\Actions\DeleteBulkAction;
910
use Filament\Actions\EditAction;
11+
use Filament\Actions\ViewAction;
12+
use Filament\Notifications\Notification;
1013
use Filament\Tables\Columns\TextColumn;
1114
use Filament\Tables\Table;
1215
use Modules\Clients\Enums\RelationStatus;
1316
use Modules\Clients\Enums\RelationType;
17+
use Modules\Clients\Exceptions\RelationHasLinkedRecordsException;
1418
use Modules\Clients\Models\Relation;
1519
use Modules\Clients\Services\RelationService;
1620
use Modules\Core\Enums\Permission;
1721
use Modules\Core\Helpers\EnumHelper;
22+
use Modules\Invoices\Filament\Company\Resources\Invoices\InvoiceResource;
23+
use Modules\Quotes\Filament\Company\Resources\Quotes\QuoteResource;
1824

1925
class RelationsTable
2026
{
@@ -88,16 +94,40 @@ public static function configure(Table $table): Table
8894
->filters([])
8995
->recordActions([
9096
ActionGroup::make([
97+
ViewAction::make('view'),
98+
Action::make('create_invoice')
99+
->label(trans('ip.create_invoice'))
100+
->icon('heroicon-o-document-plus')
101+
->visible(fn (Relation $record) => $record->relation_type === RelationType::CUSTOMER
102+
&& auth()->user()?->can(Permission::CREATE_INVOICES->value))
103+
->url(fn (Relation $record): string => InvoiceResource::getUrl('create', [
104+
'customer_id' => $record->id,
105+
])),
106+
Action::make('create_quote')
107+
->label(trans('ip.create_quote'))
108+
->icon('heroicon-o-document-text')
109+
->visible(fn () => auth()->user()?->can(Permission::CREATE_QUOTES->value))
110+
->url(fn (Relation $record): string => QuoteResource::getUrl('create', [
111+
'customer_id' => $record->id,
112+
])),
91113
EditAction::make('edit')
92114
->visible(fn () => auth()->user()?->can(Permission::EDIT_RELATIONS->value))
93115
->action(function (Relation $record, array $data) {
94116
app(RelationService::class)->updateRelation($record, $data);
95117
})
96118
->modalWidth('full'),
97119
DeleteAction::make('delete')
98-
->visible(fn () => auth()->user()?->can(Permission::DELETE_RELATIONS->value))
120+
->visible(fn (Relation $record) => ! $record->hasLinkedRecords()
121+
&& auth()->user()?->can(Permission::DELETE_RELATIONS->value))
99122
->action(function (Relation $record, array $data) {
100-
app(\Modules\Clients\Services\RelationService::class)->deleteRelation($record);
123+
try {
124+
app(RelationService::class)->deleteRelation($record);
125+
} catch (RelationHasLinkedRecordsException) {
126+
Notification::make()
127+
->title(trans('ip.cannot_delete_client_has_linked_records'))
128+
->danger()
129+
->send();
130+
}
101131
}),
102132
]),
103133
])

Modules/Clients/Models/Relation.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@ public function tasks(): HasMany
168168
return $this->hasMany(Task::class, 'customer_id');
169169
}
170170

171+
/*
172+
* Deleting a relation with linked records would orphan financial
173+
* history, so delete actions and the service guard both check this.
174+
*/
175+
public function hasLinkedRecords(): bool
176+
{
177+
return $this->invoices()->withoutGlobalScopes()->exists()
178+
|| $this->quotes()->withoutGlobalScopes()->exists()
179+
|| $this->payments()->withoutGlobalScopes()->exists()
180+
|| $this->expenses()->withoutGlobalScopes()->exists()
181+
|| $this->tasks()->withoutGlobalScopes()->exists()
182+
|| $this->projects()->withoutGlobalScopes()->exists();
183+
}
184+
171185
/**
172186
* Define a one-to-many relationship to User models.
173187
*

0 commit comments

Comments
 (0)