# Dashboard Loading Optimization

## Overview

This document describes the comprehensive optimization implemented to address loading issues on the tenant dashboard. The solution implements lazy loading, Redis-backed caching, and intelligent cache invalidation to dramatically improve dashboard performance.

## Problem Statement

### Identified Bottlenecks

1. **Concurrent Queries**: Each saved search on the dashboard triggered a separate query on initial page render, creating N× query latency and database contention
2. **No Result Caching**: Repeated work across users and page refreshes due to lack of caching
3. **Unindexed Filters/Sorts**: Potential performance degradation as data grows due to missing database indexes

### Impact

- Slow initial page load times
- High database load during peak usage
- Poor user experience with all tables loading simultaneously

## Implemented Solution

### Architecture Overview

```
┌─────────────────────────────────────────────────────────┐
│                     Client (Browser)                     │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Dashboard View with Loading Placeholders        │  │
│  │  - Shows skeletons while loading                 │  │
│  │  - Lazy loads components via wire:init           │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                          │
                          │ Lazy Load Request
                          ▼
┌─────────────────────────────────────────────────────────┐
│                  Livewire Component                      │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Records/Index Component                         │  │
│  │  - Checks cache before querying                  │  │
│  │  - Lazy initialization support                   │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                          │
                          │ Cache Check
                          ▼
┌─────────────────────────────────────────────────────────┐
│                  SavedSearchCacheService                 │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Redis Cache Layer (5 min TTL)                   │  │
│  │  Key: tenant:ss_id:filters:sort:page             │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                          │
                          │ Cache Miss
                          ▼
┌─────────────────────────────────────────────────────────┐
│                       Database                           │
│  - Indexed columns for fast filtering/sorting           │
└─────────────────────────────────────────────────────────┘
                          │
                          │ Data Change
                          ▼
┌─────────────────────────────────────────────────────────┐
│               RecordCacheInvalidator Observer            │
│  - Automatically invalidates cache on model changes      │
└─────────────────────────────────────────────────────────┘
```

## Components

### 1. SavedSearchCacheService

**Location**: `src/Domain/SavedSearches/Services/SavedSearchCacheService.php`

**Purpose**: Manages Redis-backed caching for saved search results.

**Features**:
- 5-minute TTL for cached results
- Cache key includes: tenant ID, saved search ID, filters, sort params, and pagination
- **First page only caching** - only the first page of results is cached (dashboard optimization)
- Granular invalidation by record type or tenant
- Automatic fallback on cache failures

**Key Methods**:
```php
getCacheKey(): string          // Generate unique cache key
get(string $key): ?array       // Retrieve cached results
put(string $key, array $data)  // Store results in cache
invalidateByRecordType()       // Clear cache for specific record type
invalidateByTenant()           // Clear all tenant cache
```

### 2. Lazy Loading in Records/Index Component

**Location**: `src/App/Livewire/Records/Index.php`

**Changes**:
- Added `lazy` property to enable deferred loading
- Added `readyToLoad` flag to track initialization state
- Implemented `loadData()` method called via `wire:init`
- Split `mount()` into `mount()` and `initializeComponent()` for lazy initialization
- Integrated cache checking in `render()` method
- Added `getCachedResults()` and `cacheQueryResults()` methods

**How It Works**:
1. Component initially mounts without data when `lazy=true`
2. Browser calls `loadData()` via `wire:init` after mounting
3. Component checks cache before executing database queries (first page only)
4. First page results are cached for future requests
5. Pagination requests (page > 1) bypass cache and query database directly

### 3. Dashboard View Updates

**Location**: `resources/views/dashboard.blade.php`

**Changes**:
- Added loading placeholders with skeleton UI
- Implemented Alpine.js loading state management
- Added `wire:init="loadData"` for deferred loading
- Added `lazy="true"` prop to all saved search components

**Visual Improvements**:
- Animated skeleton loaders while data loads
- Progressive rendering (above-the-fold first)
- Smooth transitions between loading and loaded states

### 4. Cache Invalidation Observer

**Location**: `src/Domain/Records/Observers/RecordCacheInvalidator.php`

**Purpose**: Automatically invalidates cache when records change.

**Events Handled**:
- `created`: Invalidate cache when new records are created
- `updated`: Invalidate cache when records are updated
- `deleted`: Invalidate cache when records are deleted
- `restored`: Invalidate cache when soft-deleted records are restored

**Smart Invalidation**:
- Only invalidates cache for affected record type
- Scoped to tenant to prevent cross-tenant cache issues
- Graceful error handling (cache failures don't break app)
- Comprehensive logging for debugging

### 5. Database Indexes

**Location**: `database/migrations/2025_01_30_000000_add_dashboard_performance_indexes.php`

**Indexes Added**:

#### Common Indexes (all major tables)
- `refid` - Primary sorting column
- `isinactive` - Frequently filtered
- `title` - Common display/search field
- `status` - Frequently filtered

#### Relationship Indexes
- Foreign key columns (e.g., `project`, `projecttask`, `customer`)
- Join columns for related models

#### Special Indexes
- `projecttasks.eventid` - Gantt chart ordering
- `projecttasks.isparent` - Hierarchical queries
- `projecttasks.parent` - Parent-child relationships
- `transactionlines.netamount` - Financial filtering

**Tables Indexed**:
- projects
- projecttasks
- employees
- customers
- vendors
- timeentries
- invoices
- opportunities
- estimates
- salesorders
- purchaseorders
- assignees
- subtasks
- transactionlines

## Configuration

### Redis Configuration

Ensure Redis is configured in `config/cache.php`:

```php
'default' => env('CACHE_DRIVER', 'redis'),

'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => 'cache',
    ],
],
```

### Cache TTL

Default TTL is 5 minutes. To adjust:

```php
// In SavedSearchCacheService.php
private const CACHE_TTL = 300; // seconds
```

### Enable/Disable Lazy Loading

To disable lazy loading for specific saved searches:

```blade
<livewire:records.index
    :lazy="false"
    ... />
```

## Performance Improvements

### Expected Results

1. **Initial Page Load**:
   - Before: All N saved searches query simultaneously (N × query time)
   - After: Page loads immediately with placeholders, queries execute progressively

2. **Cached Requests**:
   - Before: Every request hits database
   - After: Cache hit returns results in < 10ms

3. **Subsequent Requests**:
   - Before: Same queries repeated for each user
   - After: Shared cache across users (5 min TTL)

4. **Database Load**:
   - Before: High concurrent query load
   - After: Reduced by ~80% during cache hits

### Monitoring

Check cache performance:

```bash
# View Redis cache keys
redis-cli keys "saved_search:*"

# Monitor cache hit/miss logs
tail -f storage/logs/laravel.log | grep "Cache hit\|Cache miss"

# Check invalidation events
tail -f storage/logs/laravel.log | grep "Cache invalidated"
```

## Migration Steps

### 1. Run Database Migration

```bash
php artisan migrate
```

This adds indexes to improve query performance.

### 2. Clear Existing Cache (if any)

```bash
php artisan cache:clear
```

### 3. Test Dashboard Loading

1. Visit tenant dashboard
2. Observe loading placeholders
3. Watch tables load progressively
4. Verify cache hits in logs on subsequent loads

### 4. Monitor Performance

Check application logs for:
- Cache hit/miss ratios
- Query execution times
- Cache invalidation events

## Troubleshooting

### Cache Not Working

**Symptom**: No cache hits in logs

**Solutions**:
1. Verify Redis is running: `redis-cli ping`
2. Check Redis connection in `.env`: `CACHE_DRIVER=redis`
3. Verify tenant() helper returns valid ID
4. Check logs for cache errors

### Stale Data Displayed

**Symptom**: Dashboard shows outdated records

**Solutions**:
1. Verify observer is registered in `EventServiceProvider`
2. Check observer logs for invalidation events
3. Manually clear cache: `php artisan cache:clear`
4. Reduce TTL if needed

### Slow Initial Load

**Symptom**: First load still slow despite caching

**Solutions**:
1. Verify indexes were created: Check migration status
2. Run `ANALYZE TABLE` on affected tables
3. Check for N+1 queries in saved search configuration
4. Consider implementing cache warming job

### Loading Placeholders Not Showing

**Symptom**: No skeleton loaders visible

**Solutions**:
1. Verify Alpine.js is loaded
2. Check browser console for JavaScript errors
3. Ensure `x-data` and `x-show` directives are present
4. Verify Tailwind CSS classes are compiled

## Future Enhancements

### Potential Improvements

1. **Cache Warming**:
   - Implement scheduled job to pre-populate cache for popular saved searches
   - Warm cache during off-peak hours

2. **Smart Cache Invalidation**:
   - Only invalidate affected saved searches, not all for record type
   - Track which saved searches use which fields

3. **Progressive Enhancement**:
   - Load above-the-fold tables first
   - Defer below-the-fold tables until scroll

4. **Cache Analytics**:
   - Dashboard showing cache hit/miss ratios
   - Identify cache optimization opportunities

5. **Conditional Caching**:
   - Different TTLs based on update frequency
   - Longer cache for historical/archived data

## Code Examples

### Using Cache Service Directly

```php
use Domain\SavedSearches\Services\SavedSearchCacheService;

$cacheService = app(SavedSearchCacheService::class);

// Get cache key
$key = $cacheService->getCacheKey(
    tenantId: 1,
    savedSearchId: 10,
    filters: ['status' => 'active'],
    sortBy: 'title',
    sortDir: 'asc',
    page: 1,
    perPage: 50
);

// Get cached results
$results = $cacheService->get($key);

// Store results
$cacheService->put($key, $results);

// Invalidate by record type
$cacheService->invalidateByRecordType(1, 'project');
```

### Manual Cache Invalidation

```php
use Domain\SavedSearches\Services\SavedSearchCacheService;

// Invalidate all caches for a tenant
$cacheService = app(SavedSearchCacheService::class);
$cacheService->invalidateByTenant(tenant('id'));

// Invalidate specific record type
$cacheService->invalidateByRecordType(tenant('id'), 'projecttask');
```

## Testing

### Verify Lazy Loading

1. Open browser DevTools (Network tab)
2. Load dashboard
3. Observe: Initial page load should be fast
4. Watch: Individual table requests load progressively

### Verify Caching

1. Load dashboard (cache miss - check logs)
2. Refresh page (cache hit - check logs)
3. Update a record
4. Refresh page (cache miss due to invalidation)
5. Refresh again (cache hit with new data)

### Verify Indexes

```sql
-- Check indexes on projects table
SHOW INDEXES FROM projects;

-- Explain query plan (should use indexes)
EXPLAIN SELECT * FROM projects WHERE refid = 'PROJ123';
```

## Rollback Plan

If issues arise:

1. **Disable Lazy Loading**:
   - Set `:lazy="false"` in dashboard.blade.php
   - Remove `wire:init="loadData"`

2. **Disable Caching**:
   - Comment out cache check in Records/Index::render()
   - Set `CACHE_DRIVER=array` in .env (in-memory only)

3. **Rollback Migration**:
   ```bash
   php artisan migrate:rollback --step=1
   ```

4. **Remove Observer**:
   - Comment out observer registration in EventServiceProvider

## Support

For issues or questions:
- Check application logs: `storage/logs/laravel.log`
- Monitor Redis: `redis-cli monitor`
- Review this documentation
- Check database query logs for slow queries

## Conclusion

This optimization provides significant performance improvements through:
- **Lazy Loading**: Progressive rendering reduces initial load time
- **Redis Caching**: Eliminates repeated database queries
- **Smart Invalidation**: Keeps cache fresh without excessive invalidation
- **Database Indexes**: Speeds up queries that do hit the database

The solution is production-ready, scalable, and maintainable with comprehensive error handling and logging.


