From d4d5097fe2976be23308d594ef9c01562555b531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Mon, 25 May 2026 17:59:03 +0200 Subject: [PATCH] feat: Enhance offline sync system with support for multiple action types (progress_update, inspection, feature_create, media_upload) and improved error handling --- .../Controllers/OfflineSyncController.php | 91 +++++++++++++++---- resources/js/app.js | 61 ++++++++++--- 2 files changed, 122 insertions(+), 30 deletions(-) diff --git a/app/Http/Controllers/OfflineSyncController.php b/app/Http/Controllers/OfflineSyncController.php index 15767d0..fbdbfe1 100644 --- a/app/Http/Controllers/OfflineSyncController.php +++ b/app/Http/Controllers/OfflineSyncController.php @@ -4,15 +4,19 @@ namespace App\Http\Controllers; use App\Models\PendingSync; use App\Models\Phase; +use App\Models\Inspection; +use App\Models\Feature; +use App\Models\Media; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Storage; class OfflineSyncController extends Controller { public function storePending(Request $request) { $payload = $request->validate([ - 'action' => 'required|in:progress_update,task_complete', + 'action' => 'required|in:progress_update,inspection,feature_create,media_upload,task_complete', 'payload' => 'required|array', ]); $pending = PendingSync::create([ @@ -27,23 +31,78 @@ class OfflineSyncController extends Controller { $user = Auth::user(); $pendings = PendingSync::where('user_id', $user->id)->whereNull('synced_at')->get(); + $results = []; foreach ($pendings as $pending) { - if ($pending->action === 'progress_update') { - $phase = Phase::find($pending->payload['phase_id']); - if ($phase) { - $phase->progress_percent = $pending->payload['progress']; - $phase->save(); - $phase->progressUpdates()->create([ - 'user_id' => $user->id, - 'progress_percent' => $pending->payload['progress'], - 'comment' => $pending->payload['comment'] ?? '', - 'location' => $pending->payload['location'] ?? null, - ]); + $result = ['id' => $pending->id, 'action' => $pending->action, 'success' => false, 'error' => null]; + try { + if ($pending->action === 'progress_update') { + $phase = Phase::find($pending->payload['phase_id']); + if ($phase) { + $phase->progress_percent = $pending->payload['progress']; + $phase->save(); + $phase->progressUpdates()->create([ + 'user_id' => $user->id, + 'progress_percent' => $pending->payload['progress'], + 'comment' => $pending->payload['comment'] ?? '', + 'location' => $pending->payload['location'] ?? null, + ]); + } + $result['success'] = true; + } elseif ($pending->action === 'inspection') { + $inspection = Inspection::create($pending->payload); + $result['success'] = true; + $result['data'] = ['inspection_id' => $inspection->id]; + } elseif ($pending->action === 'feature_create') { + $feature = Feature::create($pending->payload); + $result['success'] = true; + $result['data'] = ['feature_id' => $feature->id]; + } elseif ($pending->action === 'media_upload') { + // Assuming payload has: 'file' (base64), 'path', 'model_type', 'model_id' + // We'll decode the base64 and store the file + if (isset($pending->payload['file'], $pending->payload['path'])) { + $decoded = base64_decode($pending->payload['file']); + if ($decoded !== false) { + $path = Storage::put($pending->payload['path'], $decoded); + // Attach to model if model_type and model_id are provided + if (isset($pending->payload['model_type'], $pending->payload['model_id'])) { + $model = new $pending->payload['model_type']; + $model = $model->find($pending->payload['model_id']); + if ($model) { + $model->media()->create([ + 'name' => $pending->payload['name'] ?? 'unnamed', + 'path' => $path, + 'mime_type' => $pending->payload['mime_type'] ?? 'application/octet-stream', + 'disk' => 'public', + ]); + } + } + $result['success'] = true; + $result['data'] = ['path' => $path]; + } else { + $result['error'] = 'Failed to decode base64 file'; + } + } else { + $result['error'] = 'Missing file or path in payload'; + } + } elseif ($pending->action === 'task_complete') { + // Example: mark a task as complete (you can adjust as needed) + // For now, just log and mark as success + \Log::info('Task completed offline', $pending->payload); + $result['success'] = true; + } else { + $result['error'] = 'Unknown action type'; } + } catch (\Exception $e) { + $result['error'] = $e->getMessage(); } - $pending->synced_at = now(); - $pending->save(); + + if ($result['success']) { + $pending->synced_at = now(); + $pending->save(); + } + + $results[] = $result; } - return response()->json(['synced' => count($pendings)]); + return response()->json(['synced' => $results]); } -} \ No newline at end of file +} diff --git a/resources/js/app.js b/resources/js/app.js index 56b10a5..ae2655c 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -2,28 +2,61 @@ import './bootstrap'; import localforage from 'localforage'; -// Sync pending actions when online -window.addEventListener('online', () => { - fetch('/offline/sync', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } }) - .then(res => res.json()) - .then(data => console.log('Synced:', data)); -}); - -// Function to store offline progress update -window.offlineProgressUpdate = function(phaseId, progress, comment, location) { - const payload = { phase_id: phaseId, progress, comment, location }; +// Generic function to queue any offline action +window.queueOfflineAction = function(action, payload) { + const pendingAction = { action, payload }; if (navigator.onLine) { + // Send immediately fetch('/offline/pending', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, - body: JSON.stringify({ action: 'progress_update', payload }) - }).then(() => alert('Actualizado online')); + body: JSON.stringify(pendingAction) + }) + .then(res => res.json()) + .then(data => { + if (data.queued) { + console.log('Action queued:', action); + } else { + console.error('Failed to queue action:', data); + } + }) + .catch(err => { + console.error('Error queuing action:', err); + }); } else { + // Store in IndexedDB (via localforage) localforage.getItem('pendingOffline').then(pending => { const list = pending || []; - list.push(payload); + list.push(pendingAction); localforage.setItem('pendingOffline', list); - alert('Guardado localmente, se sincronizará al recuperar internet'); + console.log('Action stored offline:', action); + }).catch(err => { + console.error('Error storing offline action:', err); }); } }; + +// Function to store offline progress update (for backward compatibility) +window.offlineProgressUpdate = function(phaseId, progress, comment, location) { + const payload = { phase_id: phaseId, progress, comment, location }; + window.queueOfflineAction('progress_update', payload); +}; + +// Sync pending actions when online +window.addEventListener('online', () => { + // Trigger a sync attempt + fetch('/offline/sync', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } }) + .then(res => res.json()) + .then(data => { + console.log('Synced:', data); + // Optionally, clear the local pending list if the sync was successful + // But note: the backend will mark them as synced, so we rely on the service worker to clean up? + // We'll leave it to the service worker to handle the state. + }) + .catch(err => { + console.error('Error triggering sync:', err); + }); +}); + +// Also, we can listen for the service worker's message to update the UI if needed +// But for now, we'll rely on the service worker's notification and the online event.