Skip to content

Commit 0a2e1d4

Browse files
Improve charts and add image support.
1 parent 3b58c1e commit 0a2e1d4

File tree

10 files changed

+223
-11
lines changed

10 files changed

+223
-11
lines changed

app/Filament/App/Resources/PersonResource.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
use Filament\Tables;
2525
use Filament\Actions;
2626
use Filament\Tables\Table;
27+
use Filament\Forms\Components\FileUpload;
28+
use Filament\Tables\Columns\ImageColumn;
2729

2830
class PersonResource extends Resource
2931
{
@@ -61,6 +63,11 @@ public static function form(Schema $schema): Schema
6163
TextInput::make('phone')->label('Phone'),
6264
DateTimePicker::make('birthday')->label('Birthday'),
6365
DateTimePicker::make('deathday')->label('Deathday'),
66+
FileUpload::make('photo_url')
67+
->image()
68+
->label('Profile Photo')
69+
->directory('persons')
70+
->disk('public'),
6471
DateTimePicker::make('burial_day')->label('Burial Day'),
6572
TextInput::make('bank')->label('Bank'),
6673
TextInput::make('bank_account')->label('Bank Account'),
@@ -89,6 +96,7 @@ public static function table(Table $table): Table
8996
TextColumn::make('phone')->label('Phone'),
9097
TextColumn::make('birthday')->label('Birthday'),
9198
TextColumn::make('deathday')->label('Deathday'),
99+
ImageColumn::make('photo_url')->label('Photo')->disk('public')->height(40)->width(40),
92100
TextColumn::make('burial_day')->label('Burial Day'),
93101
TextColumn::make('bank')->label('Bank'),
94102
TextColumn::make('bank_account')->label('Bank Account'),

app/Filament/App/Resources/PersonResource/Pages/EditPerson.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
use App\Filament\App\Resources\PersonResource;
77
use Filament\Actions;
88
use Filament\Resources\Pages\EditRecord;
9+
use Filament\Actions\Action;
10+
use Filament\Forms\Components\Select;
11+
use App\Models\MediaObject;
912

1013
class EditPerson extends EditRecord
1114
{
@@ -15,6 +18,70 @@ protected function getHeaderActions(): array
1518
{
1619
return [
1720
DeleteAction::make(),
21+
Action::make('selectMedia')
22+
->label('Select GEDCOM Media')
23+
->icon('heroicon-o-photograph')
24+
->modalHeading('Select GEDCOM Media to Use as Profile Photo')
25+
->modalWidth('lg')
26+
->form([
27+
Select::make('media_id')
28+
->label('GEDCOM Media')
29+
->options(fn () => MediaObject::orderBy('id', 'desc')->pluck('titl', 'id')->toArray())
30+
->searchable()
31+
->required(),
32+
])
33+
->action(function (array $data): void {
34+
$mediaId = $data['media_id'] ?? null;
35+
if (! $mediaId) {
36+
$this->notify('danger', 'No media selected.');
37+
return;
38+
}
39+
40+
$media = MediaObject::with('files')->find($mediaId);
41+
if (! $media) {
42+
$this->notify('danger', 'Selected media not found.');
43+
return;
44+
}
45+
46+
// Try to find an associated file record; use its `medi` field as the path/URL.
47+
$file = $media->files->first();
48+
$filePath = $file?->medi ?? null;
49+
50+
if (! $filePath) {
51+
$this->notify('danger', 'Selected media has no associated file path.');
52+
return;
53+
}
54+
55+
$record = $this->getRecord();
56+
// Normalize the file path/URL before storing in photo_url
57+
$url = $filePath;
58+
try {
59+
$startsWithHttp = str_starts_with(strtolower($filePath), 'http://') || str_starts_with(strtolower($filePath), 'https://');
60+
} catch (\Throwable $e) {
61+
$startsWithHttp = false;
62+
}
63+
64+
if ($startsWithHttp || str_starts_with($filePath, '/')) {
65+
$url = $filePath;
66+
} else {
67+
// If it exists on the public disk, build a public URL
68+
try {
69+
$disk = \Illuminate\Support\Facades\Storage::disk('public');
70+
if ($disk->exists($filePath)) {
71+
$url = $disk->url($filePath);
72+
}
73+
} catch (\Throwable $e) {
74+
// leave original value if disk check fails
75+
}
76+
}
77+
78+
$record->photo_url = $url;
79+
$record->save();
80+
81+
$this->notify('success', 'Person photo updated from GEDCOM media.');
82+
// Refresh the page to show updated image in the form/table.
83+
$this->redirect($this->getResource()::getUrl('edit', ['record' => $record]));
84+
}),
1885
];
1986
}
2087
}

app/Livewire/DescendantChartComponent.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ private function buildDescendantTree($person, $maxGenerations, $generation = 1):
4848
'sex' => $person->sex,
4949
'birth_date' => $person->birthday?->format('Y-m-d'),
5050
'death_date' => $person->deathday?->format('Y-m-d'),
51+
// include a safe image URL for use in charts (uses Person::profileImageUrl)
52+
'image' => method_exists($person, 'profileImageUrl') ? $person->profileImageUrl() : asset('images/default-avatar.svg'),
5153
'generation' => $generation,
5254
'children' => []
5355
];
@@ -80,14 +82,24 @@ private function buildDescendantTree($person, $maxGenerations, $generation = 1):
8082
public function setRootPerson(int $personId): void
8183
{
8284
$this->rootPersonId = $personId;
83-
$this->mount($personId);
85+
// reload data without remounting component lifecycle
86+
$this->descendantsData = [];
87+
if ($this->rootPersonId) {
88+
$rootPerson = Person::find($this->rootPersonId);
89+
$this->descendantsData = $this->buildDescendantTree($rootPerson, $this->generations);
90+
}
8491
$this->emit('descendant-chart-updated');
8592
}
8693

8794
public function setGenerations(int $generations): void
8895
{
8996
$this->generations = max(1, min(10, $generations));
90-
$this->mount($this->rootPersonId);
97+
// rebuild tree with new generation settings
98+
$this->descendantsData = [];
99+
if ($this->rootPersonId) {
100+
$rootPerson = Person::find($this->rootPersonId);
101+
$this->descendantsData = $this->buildDescendantTree($rootPerson, $this->generations);
102+
}
91103
$this->emit('descendant-chart-updated');
92104
}
93105

app/Livewire/FanChart.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ private function buildFanData($person, $maxGenerations, $generation = 0): array
6060
'generation' => $generation,
6161
'children' => []
6262
];
63+
// include image URL when available
64+
$personData['image'] = method_exists($person, 'profileImageUrl') ? $person->profileImageUrl() : asset('images/default-avatar.svg');
6365

6466
// For fan chart, we build ancestors (parents) not descendants
6567
if ($person->childInFamily && $generation < $maxGenerations - 1) {

app/Livewire/PedigreeChart.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ protected function buildTree(Person $person, int $currentGen, int $maxGen): arra
8585
'sex' => $person->sex,
8686
'birth' => optional($person->birthday)?->format('Y-m-d'),
8787
'death' => optional($person->deathday)?->format('Y-m-d'),
88+
'image' => method_exists($person, 'profileImageUrl') ? $person->profileImageUrl() : asset('images/default-avatar.svg'),
8889
'father' => null,
8990
'mother' => null,
9091
];
@@ -122,8 +123,12 @@ public function renderPedigreeTree(array $node, int $generation = 1): string
122123
}
123124

124125
$name = htmlspecialchars($node['name'] ?? 'Unknown', ENT_QUOTES, 'UTF-8');
126+
$imageSrc = htmlspecialchars($node['image'] ?? asset('images/default-avatar.svg'), ENT_QUOTES, 'UTF-8');
127+
$imageAlt = htmlspecialchars($node['name'] ?? 'Person', ENT_QUOTES, 'UTF-8');
125128
$editUrl = htmlspecialchars($this->personEditUrl($node['id']), ENT_QUOTES, 'UTF-8');
129+
// include thumbnail image in person box
126130
$personHtml = "<div class=\"person-box {$sexClass}\" onclick=\"expandPerson({$node['id']})\">".
131+
"<div class=\"person-thumb\"><img src=\"{$imageSrc}\" alt=\"{$imageAlt}\" loading=\"lazy\"/></div>".
127132
"<div class=\"person-name\"><a href=\"{$editUrl}\" class=\"hover:underline\" target=\"_blank\" rel=\"noopener\">{$name}</a></div>".
128133
$datesHtml.
129134
"<button class=\"expand-btn\" title=\"Set as root\" onclick=\"event.stopPropagation(); expandPerson({$node['id']});\">+</button>".

app/Models/MediaObject.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,12 @@ class MediaObject extends \FamilyTree365\LaravelGedcom\Models\MediaObject
99
{
1010
use HasFactory;
1111
use BelongsToTenant;
12+
13+
/**
14+
* Files associated with this media object (from media_objects_file table).
15+
*/
16+
public function files()
17+
{
18+
return $this->hasMany(MediaObjeectFile::class, 'gid', 'id')->where('group', 'obje');
19+
}
1220
}

app/Models/Person.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Person extends Model
2828
'appellative',
2929
'email',
3030
'phone',
31+
'photo_url',
3132
'birthday',
3233
'deathday',
3334
'burial_day',
@@ -210,6 +211,45 @@ public static function getBasicInfoCached($id)
210211
return cache()->remember("person_basic_info_{$id}", now()->addHours(1), fn() => self::withBasicInfo()->find($id));
211212
}
212213

214+
/**
215+
* Return the best-guess profile image URL for a person.
216+
* Tries multiple non-destructive locations and returns a default when none found.
217+
*/
218+
public function profileImageUrl(): string
219+
{
220+
// 1) Prefer an explicit attribute if present (e.g. photo_url or image)
221+
if (!empty($this->photo_url)) {
222+
return $this->photo_url;
223+
}
224+
if (!empty($this->image)) {
225+
return $this->image;
226+
}
227+
228+
// 2) Look for files in the public storage under predictable paths
229+
try {
230+
$disk = \Illuminate\Support\Facades\Storage::disk('public');
231+
$candidates = [
232+
"persons/{$this->id}.jpg",
233+
"persons/{$this->id}.jpeg",
234+
"persons/{$this->id}.png",
235+
"persons/{$this->id}.webp",
236+
"photos/{$this->id}.jpg",
237+
"photos/{$this->id}.png",
238+
];
239+
240+
foreach ($candidates as $path) {
241+
if ($disk->exists($path)) {
242+
return $disk->url($path);
243+
}
244+
}
245+
} catch (\Exception $e) {
246+
// ignore storage errors and continue to fallback
247+
}
248+
249+
// 3) Fallback to a bundled public asset (keeps UI consistent)
250+
return asset('images/default-avatar.svg');
251+
}
252+
213253
/**
214254
* Get checklists associated with this person
215255
*/
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class() extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('persons', function (Blueprint $table) {
15+
$table->string('photo_url')->nullable()->after('phone');
16+
});
17+
}
18+
19+
/**
20+
* Reverse the migrations.
21+
*/
22+
public function down(): void
23+
{
24+
Schema::table('persons', function (Blueprint $table) {
25+
$table->dropColumn('photo_url');
26+
});
27+
}
28+
};

public/images/default-avatar.svg

Lines changed: 7 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)