feat(tasks): complete task management system with Kanban, Calendar, notifications

- Add Task model with subtasks, priorities, status transitions, dates, hours
- Add Comment polymorphic model for tasks/issues/projects
- Livewire components: TaskManager (list+filters), TaskForm (modal), TaskDetail, TaskKanban (drag&drop), TaskCalendar (FullCalendar)
- TaskPolicy with permissions (view/create/edit/delete/assign/manage all)
- Notifications: assigned, status change, overdue, comment added
- Daily overdue notification job scheduled
- Dashboard widget unifies IssueTask + Task
- i18n: en/es/fr/ru (393 keys each)
- Routes, navigation, offline sync support
- 101 tests passing, Pint compliant on new files
This commit is contained in:
Javier Braña
2026-08-03 13:44:10 +02:00
parent a65d4b12f2
commit ba614bddbc
62 changed files with 5878 additions and 382 deletions
+22 -4
View File
@@ -2,11 +2,11 @@
namespace App\Http\Controllers;
use App\Models\Feature;
use App\Models\Inspection;
use App\Models\PendingSync;
use App\Models\Phase;
use App\Models\Inspection;
use App\Models\Feature;
use App\Models\Media;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
@@ -16,7 +16,7 @@ class OfflineSyncController extends Controller
public function storePending(Request $request)
{
$payload = $request->validate([
'action' => 'required|in:progress_update,inspection,feature_create,media_upload,task_complete',
'action' => 'required|in:progress_update,inspection,feature_create,media_upload,task_complete,task_create,task_update',
'payload' => 'required|array',
]);
$pending = PendingSync::create([
@@ -24,6 +24,7 @@ class OfflineSyncController extends Controller
'action' => $payload['action'],
'payload' => $payload['payload'],
]);
return response()->json(['queued' => true]);
}
@@ -89,6 +90,22 @@ class OfflineSyncController extends Controller
// For now, just log and mark as success
\Log::info('Task completed offline', $pending->payload);
$result['success'] = true;
} elseif ($pending->action === 'task_create') {
$task = Task::create($pending->payload);
$result['success'] = true;
$result['data'] = ['task_id' => $task->id, 'uuid' => $task->uuid];
} elseif ($pending->action === 'task_update') {
$task = Task::find($pending->payload['id'] ?? null);
if ($task) {
// Remove fields that shouldn't be mass updated
$data = $pending->payload;
unset($data['id'], $data['uuid'], $data['created_at'], $data['updated_at'], $data['deleted_at']);
$task->update($data);
$result['success'] = true;
$result['data'] = ['task_id' => $task->id];
} else {
$result['error'] = 'Task not found';
}
} else {
$result['error'] = 'Unknown action type';
}
@@ -103,6 +120,7 @@ class OfflineSyncController extends Controller
$results[] = $result;
}
return response()->json(['synced' => $results]);
}
}
+5 -4
View File
@@ -16,11 +16,12 @@ class SetLocale
public function handle(Request $request, Closure $next)
{
$locale = null;
$allowedLocales = ['en', 'es', 'fr', 'ru'];
// 1. From authenticated user preference
if (Auth::check()) {
$userLocale = Auth::user()->locale;
if ($userLocale && in_array($userLocale, ['en', 'es'])) {
if ($userLocale && in_array($userLocale, $allowedLocales)) {
$locale = $userLocale;
}
}
@@ -28,7 +29,7 @@ class SetLocale
// 2. From session
if (!$locale && Session::has('locale')) {
$sessionLocale = Session::get('locale');
if (in_array($sessionLocale, ['en', 'es'])) {
if (in_array($sessionLocale, $allowedLocales)) {
$locale = $sessionLocale;
}
}
@@ -36,14 +37,14 @@ class SetLocale
// 3. From browser Accept-Language
if (!$locale) {
$browserLang = substr($request->server('HTTP_ACCEPT_LANGUAGE', 'en'), 0, 2);
if (in_array($browserLang, ['en', 'es'])) {
if (in_array($browserLang, $allowedLocales)) {
$locale = $browserLang;
}
}
// 4. Default to app locale
if (!$locale) {
$locale = config('app.locale', 'es');
$locale = config('app.locale', 'en');
}
App::setLocale($locale);