Skip to content

Commit 3ef785c

Browse files
Create DuplicateMatchResource.php
1 parent 188b5fa commit 3ef785c

File tree

1 file changed

+140
-0
lines changed

1 file changed

+140
-0
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
namespace App\Modules\Person\Filament\Resources;
4+
5+
use App\Models\DuplicateMatch;
6+
use App\Services\PersonMergeService;
7+
use App\Models\Person;
8+
use Filament\Resources\Resource;
9+
use Filament\Tables;
10+
use Filament\Resources\Table;
11+
use Filament\Tables\Columns\TextColumn;
12+
use Filament\Tables\Actions\Action;
13+
use Filament\Tables\Actions\BulkAction;
14+
use Filament\Tables\Filters\SelectFilter;
15+
use Illuminate\Database\Eloquent\Builder;
16+
17+
class DuplicateMatchResource extends Resource
18+
{
19+
protected static ?string $model = DuplicateMatch::class;
20+
21+
protected static ?string $navigationLabel = 'Duplicate Suggestions';
22+
protected static ?string $navigationGroup = 'Genealogy';
23+
protected static ?string $navigationIcon = 'heroicon-o-identification';
24+
25+
public static function table(Table $table): Table
26+
{
27+
return $table
28+
->columns([
29+
TextColumn::make('primaryPerson.name')
30+
->label('Primary')
31+
->searchable()
32+
->wrap(),
33+
TextColumn::make('duplicatePerson.name')
34+
->label('Candidate')
35+
->searchable()
36+
->wrap(),
37+
TextColumn::make('confidence_score')
38+
->label('Confidence')
39+
->formatStateUsing(fn($state) => sprintf('%.1f%%', $state * 100))
40+
->sortable(),
41+
TextColumn::make('status')
42+
->label('Status')
43+
->sortable(),
44+
TextColumn::make('match_data')
45+
->label('Match data')
46+
->toggleable()
47+
->wrap()
48+
->formatStateUsing(fn($state) => json_encode($state)),
49+
TextColumn::make('created_at')
50+
->dateTime()
51+
->label('Detected'),
52+
])
53+
->filters([
54+
SelectFilter::make('status')->options([
55+
'pending' => 'Pending',
56+
'reviewed' => 'Reviewed',
57+
'accepted' => 'Accepted',
58+
'rejected' => 'Rejected',
59+
'merged' => 'Merged',
60+
]),
61+
])
62+
->actions([
63+
Action::make('review')
64+
->label('Review')
65+
->icon('heroicon-o-eye')
66+
->modalHeading('Review duplicate suggestion')
67+
->modalSubheading(fn (DuplicateMatch $record) => 'Confidence: ' . sprintf('%.1f%%', $record->confidence_score * 100))
68+
->modalContent(function (DuplicateMatch $record): string {
69+
$p = $record->primaryPerson;
70+
$d = $record->duplicatePerson;
71+
$data = $record->match_data ?? [];
72+
$md = htmlspecialchars(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
73+
return "<div>
74+
<p><strong>Primary</strong>: {$p->name} (ID: {$p->id})</p>
75+
<p><strong>Candidate</strong>: {$d->name} (ID: {$d->id})</p>
76+
<pre style=\"max-height:250px;overflow:auto;background:#f6f8fa;padding:8px;border-radius:4px;\">{$md}</pre>
77+
</div>";
78+
})
79+
->modalActions([
80+
Action::make('merge')
81+
->label('Merge (candidate → primary)')
82+
->color('success')
83+
->requiresConfirmation()
84+
->action(function (DuplicateMatch $record, array $data) {
85+
$mergeService = app(PersonMergeService::class);
86+
$primary = $record->primaryPerson;
87+
$candidate = $record->duplicatePerson;
88+
if (!$primary || !$candidate) {
89+
$record->status = 'rejected';
90+
$record->save();
91+
return $record->fresh();
92+
}
93+
$mergeService->merge($primary, $candidate);
94+
$record->status = 'merged';
95+
$record->save();
96+
return $record->fresh();
97+
}),
98+
Action::make('reject')
99+
->label('Reject')
100+
->color('danger')
101+
->requiresConfirmation()
102+
->action(function (DuplicateMatch $record) {
103+
$record->status = 'rejected';
104+
$record->reviewed_at = now();
105+
$record->save();
106+
}),
107+
]),
108+
Action::make('open')
109+
->label('Open persons')
110+
->url(fn (DuplicateMatch $record) => route('filament.resources.persons.edit', ['record' => $record->primary_person_id]))
111+
->openUrlInNewTab(),
112+
])
113+
->bulkActions([
114+
BulkAction::make('merge_selected')
115+
->label('Merge selected (pairwise)')
116+
->action(function (Collection $records) {
117+
$service = app(PersonMergeService::class);
118+
foreach ($records as $record) {
119+
if ($record->status === 'merged') {
120+
continue;
121+
}
122+
$primary = $record->primaryPerson;
123+
$candidate = $record->duplicatePerson;
124+
if ($primary && $candidate) {
125+
$service->merge($primary, $candidate);
126+
$record->status = 'merged';
127+
$record->save();
128+
}
129+
}
130+
}),
131+
]);
132+
}
133+
134+
public static function getPages(): array
135+
{
136+
return [
137+
'index' => \App\Modules\Person\Filament\Resources\DuplicateMatchResource\Pages\ListDuplicateMatches::route('/'),
138+
];
139+
}
140+
}

0 commit comments

Comments
 (0)