Skip to content

Commit 89f6ebe

Browse files
Merge branch 'main' into patch-18
2 parents 4b83811 + 3118f8b commit 89f6ebe

File tree

42 files changed

+1800
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1800
-7
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Jobs\RunRecordMatchingJob;
6+
use Illuminate\Console\Command;
7+
8+
class RunRecordMatcher extends Command
9+
{
10+
protected $signature = 'ai:run-matcher {--queue=default}';
11+
protected $description = 'Run the AI record matching job (dispatch to queue).';
12+
13+
public function handle()
14+
{
15+
RunRecordMatchingJob::dispatch()->onQueue($this->option('queue'));
16+
$this->info('Record matching job dispatched.');
17+
}
18+
}

app/Console/Kernel.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,24 @@
22

33
namespace App\Console;
44

5-
use Override;
65
use Illuminate\Console\Scheduling\Schedule;
76
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
7+
use App\Jobs\ScanForDuplicatePersons;
88

99
class Kernel extends ConsoleKernel
1010
{
1111
/**
1212
* Define the application's command schedule.
1313
*/
14-
#[Override]
1514
protected function schedule(Schedule $schedule): void
1615
{
17-
// $schedule->command('inspire')->hourly();
16+
// Run duplicate scanning once a day (adjust frequency as needed)
17+
$schedule->job(new ScanForDuplicatePersons(0.7, 10))->daily();
1818
}
1919

2020
/**
2121
* Register the commands for the application.
2222
*/
23-
#[Override]
2423
protected function commands(): void
2524
{
2625
$this->load(__DIR__.'/Commands');
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Events;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
7+
use Illuminate\Foundation\Events\Dispatchable;
8+
use Illuminate\Queue\SerializesModels;
9+
10+
class ResearchSpaceUpdated implements ShouldBroadcastNow
11+
{
12+
use Dispatchable, InteractsWithSockets, SerializesModels;
13+
14+
public int $researchSpaceId;
15+
public array $payload;
16+
17+
public function __construct(int $researchSpaceId, array $payload = [])
18+
{
19+
$this->researchSpaceId = $researchSpaceId;
20+
$this->payload = $payload;
21+
}
22+
23+
public function broadcastAs(): string
24+
{
25+
return 'ResearchSpaceUpdated';
26+
}
27+
28+
public function broadcastWith(): array
29+
{
30+
return $this->payload;
31+
}
32+
33+
public function broadcastOn()
34+
{
35+
return ['research-space.' . $this->researchSpaceId];
36+
}
37+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace App\Filament\Resources;
4+
5+
use App\Filament\Resources\AIRecordMatchResource\Pages\ReviewMatches;
6+
use App\Models\AISuggestedMatch;
7+
use Filament\Resources\Resource;
8+
use Filament\Resources\Table;
9+
use Filament\Tables;
10+
use Filament\Tables\Columns\TextColumn;
11+
12+
class AIRecordMatchResource extends Resource
13+
{
14+
protected static ?string $model = AISuggestedMatch::class;
15+
16+
protected static ?string $navigationIcon = 'heroicon-o-switch-horizontal';
17+
18+
public static function table(Table $table): Table
19+
{
20+
return $table
21+
->columns([
22+
TextColumn::make('id')->label('ID'),
23+
TextColumn::make('local_person_id')->label('Local Person ID')->sortable(),
24+
TextColumn::make('provider')->label('Provider')->sortable(),
25+
TextColumn::make('external_record_id')->label('External ID'),
26+
TextColumn::make('confidence')->label('Confidence')->formatStateUsing(fn($state) => round($state * 100, 1) . '%'),
27+
TextColumn::make('status')->label('Status')->sortable(),
28+
TextColumn::make('created_at')->label('Created'),
29+
])
30+
->filters([
31+
//
32+
])
33+
->actions([
34+
Tables\Actions\Action::make('confirm')
35+
->label('Confirm')
36+
->action(fn (AISuggestedMatch $record) => redirect()->route('filament.resources.ai-record-matches.review.confirm', ['record' => $record->id])),
37+
Tables\Actions\Action::make('reject')
38+
->label('Reject')
39+
->action(fn (AISuggestedMatch $record) => redirect()->route('filament.resources.ai-record-matches.review.reject', ['record' => $record->id])),
40+
]);
41+
}
42+
43+
public static function getPages(): array
44+
{
45+
return [
46+
'index' => ReviewMatches::route('/'),
47+
];
48+
}
49+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\AIRecordMatchResource\Pages;
4+
5+
use App\Filament\Resources\AIRecordMatchResource;
6+
use App\Models\AISuggestedMatch;
7+
use Filament\Pages\Actions;
8+
use Filament\Resources\Pages\ListRecords;
9+
use Illuminate\Support\Facades\Http;
10+
11+
class ReviewMatches extends ListRecords
12+
{
13+
protected static string $resource = AIRecordMatchResource::class;
14+
15+
protected function getTableActions(): array
16+
{
17+
return [
18+
Actions\Action::make('confirm')
19+
->label('Confirm')
20+
->action(function (AISuggestedMatch $record) {
21+
// call controller endpoint
22+
Http::post(route('ai.matches.confirm', ['suggestion' => $record->id]));
23+
$this->notify('success', 'Confirmed');
24+
}),
25+
Actions\Action::make('reject')
26+
->label('Reject')
27+
->action(function (AISuggestedMatch $record) {
28+
Http::post(route('ai.matches.reject', ['suggestion' => $record->id]));
29+
$this->notify('success', 'Rejected');
30+
}),
31+
];
32+
}
33+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace App\Filament\Resources;
4+
5+
use App\Filament\Resources\ResearchSpaceResource\Pages;
6+
use App\Models\ResearchSpace;
7+
use Filament\Schemas\Schema;
8+
use Filament\Schemas\Components\Section;
9+
use Filament\Schemas\Components\Grid;
10+
use Filament\Forms\Components\TextInput;
11+
use Filament\Forms\Components\Textarea;
12+
use Filament\Forms\Components\Toggle;
13+
use Filament\Tables\Table;
14+
use Filament\Tables\Columns\TextColumn;
15+
use Filament\Resources\Resource;
16+
17+
class ResearchSpaceResource extends Resource
18+
{
19+
protected static ?string $model = ResearchSpace::class;
20+
21+
protected static ?string $navigationIcon = 'heroicon-o-users';
22+
23+
protected static ?string $navigationGroup = 'Collaboration';
24+
25+
protected static ?string $navigationLabel = 'Research Spaces';
26+
27+
public static function form(Schema $schema): Schema
28+
{
29+
return $schema
30+
->components([
31+
Section::make('Details')
32+
->schema([
33+
Grid::make(1)
34+
->schema([
35+
TextInput::make('name')
36+
->required()
37+
->maxLength(255),
38+
TextInput::make('slug')
39+
->required()
40+
->maxLength(255),
41+
Textarea::make('description')
42+
->rows(4),
43+
Toggle::make('is_private')
44+
->label('Private')
45+
->default(true),
46+
]),
47+
]),
48+
]);
49+
}
50+
51+
public static function table(Table $table): Table
52+
{
53+
return $table
54+
->columns([
55+
TextColumn::make('id')->label('ID')->sortable(),
56+
TextColumn::make('name')->searchable()->sortable(),
57+
TextColumn::make('owner.name')->label('Owner')->sortable(),
58+
TextColumn::make('is_private')->boolean()->label('Private'),
59+
TextColumn::make('created_at')->label('Created')->dateTime(),
60+
]);
61+
}
62+
63+
public static function getPages(): array
64+
{
65+
return [
66+
'index' => Pages\ListResearchSpaces::route('/'),
67+
'create' => Pages\CreateResearchSpace::route('/create'),
68+
'edit' => Pages\EditResearchSpace::route('/{record}/edit'),
69+
];
70+
}
71+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\ResearchSpaceResource\Pages;
4+
5+
use Filament\Resources\Pages\CreateRecord;
6+
use App\Filament\Resources\ResearchSpaceResource;
7+
8+
class CreateResearchSpace extends CreateRecord
9+
{
10+
protected static string $resource = ResearchSpaceResource::class;
11+
12+
protected function beforeCreate(): void
13+
{
14+
// Automatically set owner/created_by to currently authenticated user
15+
$user = auth()->user();
16+
$this->data['owner_id'] = $user->id;
17+
$this->data['created_by'] = $user->id;
18+
}
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\ResearchSpaceResource\Pages;
4+
5+
use Filament\Resources\Pages\EditRecord;
6+
use App\Filament\Resources\ResearchSpaceResource;
7+
8+
class EditResearchSpace extends EditRecord
9+
{
10+
protected static string $resource = ResearchSpaceResource::class;
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\ResearchSpaceResource\Pages;
4+
5+
use Filament\Actions\CreateAction;
6+
use Filament\Resources\Pages\ListRecords;
7+
use App\Filament\Resources\ResearchSpaceResource;
8+
9+
class ListResearchSpaces extends ListRecords
10+
{
11+
protected static string $resource = ResearchSpaceResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
CreateAction::make(),
17+
];
18+
}
19+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace App\Http\Livewire\ResearchSpace;
4+
5+
use Livewire\Component;
6+
use App\Models\ResearchSpace;
7+
use App\Events\ResearchSpaceUpdated;
8+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
9+
10+
class CollaboratorBoard extends Component
11+
{
12+
use AuthorizesRequests;
13+
14+
public ResearchSpace $space;
15+
public $content = '';
16+
public $userPermissions = [];
17+
18+
protected $listeners = [
19+
'echo:research-space.{spaceId},ResearchSpaceUpdated' => 'onExternalUpdate',
20+
];
21+
22+
public function mount($spaceId)
23+
{
24+
$this->space = ResearchSpace::with('collaborators.user')->findOrFail($spaceId);
25+
26+
$this->authorize('view', $this->space);
27+
28+
// Example initial content stored in settings (could be moved to separate table)
29+
$this->content = data_get($this->space->settings, 'board.content', '');
30+
$this->userPermissions = []; // can be populated from collaborators
31+
}
32+
33+
public function saveContent(string $updated)
34+
{
35+
$this->authorize('update', $this->space);
36+
37+
$this->content = $updated;
38+
39+
$settings = $this->space->settings ?? [];
40+
$settings['board'] = array_merge($settings['board'] ?? [], ['content' => $this->content, 'updated_at' => now()->toDateTimeString()]);
41+
$this->space->settings = $settings;
42+
$this->space->save();
43+
44+
// Broadcast to other collaborators immediately
45+
event(new ResearchSpaceUpdated($this->space->id, ['content' => $this->content, 'user_id' => auth()->id()]));
46+
}
47+
48+
public function onExternalUpdate($payload)
49+
{
50+
// When we get an external broadcast, update local content
51+
$this->content = data_get($payload, 'content', $this->content);
52+
$this->emitSelf('contentUpdated');
53+
}
54+
55+
public function render()
56+
{
57+
return view('livewire.research-space.collaborator-board', [
58+
'space' => $this->space,
59+
]);
60+
}
61+
}

0 commit comments

Comments
 (0)