Overview
The HealthHistory component provides a comprehensive, paginated list of all health records (appointments, exercises, weight, and heart rate measurements). It supports search, filtering by record type, and deletion with automatic attachment cleanup.
Namespace: App\Livewire\History\HealthHistory
Route: GET /history
Properties
Search query for filtering appointments and exercises by title/description
Filter by record type: '' (all), 'appointment', 'exercise', 'weight', or 'heart'
Number of records to display per page
Traits
WithPagination - Enables pagination of results
HasContext - Provides $this->getTargetUserId() and $this->isReadOnly for viewer mode
Event Listeners
Refreshes the component after a record is added, edited, or deleted
app/Livewire/History/HealthHistory.php:26
protected $listeners = [ 'refresh-history' => '$refresh' ];
Methods
deleteRecord()
Deletes a health record and its associated attachments.
app/Livewire/History/HealthHistory.php:32-60
public function deleteRecord ( $id , $type )
{
if ( $this -> isReadOnly ) {
return ;
}
$modelClass = match ( $type ) {
'MedicalAppointment' => MedicalAppointment :: class ,
'ActivityExercise' => ActivityExercise :: class ,
'MeasurementWeight' => MeasurementWeight :: class ,
'MeasurementHeart' => MeasurementHeart :: class ,
default => null ,
};
if ( ! $modelClass ) return ;
$record = $modelClass :: where ( 'id' , $id ) -> where ( 'user_id' , auth () -> id ()) -> first ();
if ( $record ) {
if ( method_exists ( $record , 'attachments' )) {
foreach ( $record -> attachments as $attachment ) {
if ( Storage :: disk ( 'local' ) -> exists ( $attachment -> file_path )) {
Storage :: disk ( 'local' ) -> delete ( $attachment -> file_path );
}
$attachment -> delete ();
}
}
$record -> delete ();
}
}
The ID of the record to delete
One of: MedicalAppointment, ActivityExercise, MeasurementWeight, MeasurementHeart
Read-Only Protection: Deletes are blocked if $this->isReadOnly is true (viewer mode).
Cascade Delete: For appointments and exercises, all attachments are deleted from disk and database before deleting the record.
render()
Builds and paginates the combined record list.
app/Livewire/History/HealthHistory.php:62-112
public function render ()
{
$userId = $this -> getTargetUserId ();
$allRecords = collect ();
// Load appointments
if ( empty ( $this -> type ) || $this -> type === 'appointment' ) {
$query = MedicalAppointment :: where ( 'user_id' , $userId );
if ( ! empty ( $this -> search )) {
$query -> where ( function ( Builder $q ) {
$q -> where ( 'title' , 'like' , '%' . $this -> search . '%' )
-> orWhere ( 'description' , 'like' , '%' . $this -> search . '%' );
});
}
$allRecords = $allRecords -> concat ( $query -> latest ( 'date' ) -> get ());
}
// Load exercises
if ( empty ( $this -> type ) || $this -> type === 'exercise' ) {
$query = ActivityExercise :: where ( 'user_id' , $userId );
if ( ! empty ( $this -> search )) {
$query -> where ( function ( Builder $q ) {
$q -> where ( 'title' , 'like' , '%' . $this -> search . '%' )
-> orWhere ( 'description' , 'like' , '%' . $this -> search . '%' );
});
}
$allRecords = $allRecords -> concat ( $query -> latest ( 'date' ) -> get ());
}
// Load weight (no search)
if ( empty ( $this -> search ) && ( empty ( $this -> type ) || $this -> type === 'weight' )) {
$allRecords = $allRecords -> concat ( MeasurementWeight :: where ( 'user_id' , $userId ) -> latest ( 'date' ) -> get ());
}
// Load heart rate (no search)
if ( empty ( $this -> search ) && ( empty ( $this -> type ) || $this -> type === 'heart' )) {
$allRecords = $allRecords -> concat ( MeasurementHeart :: where ( 'user_id' , $userId ) -> latest ( 'date' ) -> get ());
}
$sortedRecords = $allRecords -> sortByDesc ( 'date' );
// Manual pagination
$items = $sortedRecords -> forPage ( $this -> getPage (), $this -> perPage );
$paginatedRecords = new LengthAwarePaginator (
$items ,
$sortedRecords -> count (),
$this -> perPage ,
$this -> getPage (),
[ 'path' => request () -> url (), 'query' => request () -> query ()]
);
return view ( 'livewire.history.health-history' , [
'records' => $paginatedRecords
]) -> layout ( 'layouts.app' );
}
Search Behavior:
Only appointments and exercises are searchable (title and description fields)
Weight and heart measurements are excluded when a search query is active
Filtering:
Empty type shows all records
Specific type shows only that record type
Pagination:
Records from all types are combined into a single collection
Sorted by date (newest first)
Manually paginated using Laravel’s LengthAwarePaginator
UI Features
Search Bar:
< input wire:model.live = "search" type = "text" placeholder = "Search appointments and exercises..." >
Type Filter:
< select wire:model.live = "type" >
< option value = "" > All Types </ option >
< option value = "appointment" > Appointments </ option >
< option value = "exercise" > Exercise </ option >
< option value = "weight" > Weight </ option >
< option value = "heart" > Heart Rate </ option >
</ select >
Per-Page Selector:
< select wire:model.live = "perPage" >
< option value = "10" > 10 </ option >
< option value = "20" > 20 </ option >
< option value = "50" > 50 </ option >
< option value = "100" > 100 </ option >
</ select >
Property Watchers
All three properties reset pagination when changed:
app/Livewire/History/HealthHistory.php:28-30
public function updatedSearch () { $this -> resetPage (); }
public function updatedType () { $this -> resetPage (); }
public function updatedPerPage () { $this -> resetPage (); }
Viewer Mode Support
When viewing another user’s data:
$this->getTargetUserId() returns the selected user ID from session
$this->isReadOnly is true, preventing deletions
Delete buttons are hidden in the UI
Dependencies
All health models - MedicalAppointment, ActivityExercise, MeasurementWeight, MeasurementHeart
Attachment Model - For cascade deletion
Storage Facade - For deleting attachment files
HasContext Trait - For viewer mode support
HealthStats Component View weight trends and statistics
Export Controller Export all records to PDF