feat: Enhance offline sync system with support for multiple action types (progress_update, inspection, feature_create, media_upload) and improved error handling

This commit is contained in:
2026-05-25 17:59:03 +02:00
parent c556a4910b
commit d4d5097fe2
2 changed files with 122 additions and 30 deletions
+75 -16
View File
@@ -4,15 +4,19 @@ namespace App\Http\Controllers;
use App\Models\PendingSync; use App\Models\PendingSync;
use App\Models\Phase; use App\Models\Phase;
use App\Models\Inspection;
use App\Models\Feature;
use App\Models\Media;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class OfflineSyncController extends Controller class OfflineSyncController extends Controller
{ {
public function storePending(Request $request) public function storePending(Request $request)
{ {
$payload = $request->validate([ $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', 'payload' => 'required|array',
]); ]);
$pending = PendingSync::create([ $pending = PendingSync::create([
@@ -27,23 +31,78 @@ class OfflineSyncController extends Controller
{ {
$user = Auth::user(); $user = Auth::user();
$pendings = PendingSync::where('user_id', $user->id)->whereNull('synced_at')->get(); $pendings = PendingSync::where('user_id', $user->id)->whereNull('synced_at')->get();
$results = [];
foreach ($pendings as $pending) { foreach ($pendings as $pending) {
if ($pending->action === 'progress_update') { $result = ['id' => $pending->id, 'action' => $pending->action, 'success' => false, 'error' => null];
$phase = Phase::find($pending->payload['phase_id']); try {
if ($phase) { if ($pending->action === 'progress_update') {
$phase->progress_percent = $pending->payload['progress']; $phase = Phase::find($pending->payload['phase_id']);
$phase->save(); if ($phase) {
$phase->progressUpdates()->create([ $phase->progress_percent = $pending->payload['progress'];
'user_id' => $user->id, $phase->save();
'progress_percent' => $pending->payload['progress'], $phase->progressUpdates()->create([
'comment' => $pending->payload['comment'] ?? '', 'user_id' => $user->id,
'location' => $pending->payload['location'] ?? null, '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]);
} }
} }
+47 -14
View File
@@ -2,28 +2,61 @@ import './bootstrap';
import localforage from 'localforage'; import localforage from 'localforage';
// Sync pending actions when online // Generic function to queue any offline action
window.addEventListener('online', () => { window.queueOfflineAction = function(action, payload) {
fetch('/offline/sync', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } }) const pendingAction = { action, payload };
.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 };
if (navigator.onLine) { if (navigator.onLine) {
// Send immediately
fetch('/offline/pending', { fetch('/offline/pending', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify({ action: 'progress_update', payload }) body: JSON.stringify(pendingAction)
}).then(() => alert('Actualizado online')); })
.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 { } else {
// Store in IndexedDB (via localforage)
localforage.getItem('pendingOffline').then(pending => { localforage.getItem('pendingOffline').then(pending => {
const list = pending || []; const list = pending || [];
list.push(payload); list.push(pendingAction);
localforage.setItem('pendingOffline', list); 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.