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
+62
View File
@@ -6,6 +6,7 @@ use Livewire\Component;
use Livewire\Attributes\On;
use Livewire\WithFileUploads;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use App\Models\Project;
use App\Models\Phase;
use App\Models\Layer;
@@ -13,6 +14,7 @@ use App\Models\Feature;
use App\Models\Inspection;
use App\Models\InspectionTemplate;
use App\Models\Issue;
use App\Models\Media;
class ProjectMap extends Component
{
@@ -353,6 +355,14 @@ class ProjectMap extends Component
$this->dispatch('notify', 'Inspección guardada correctamente');
}
// Notificar a usuarios del proyecto (excepto creador)
$usersToNotify = $this->project->users()
->where('user_id', '!=', auth()->id())
->get();
foreach ($usersToNotify as $user) {
$user->notify(new \App\Notifications\InspectionCompletedNotification($inspection));
}
// Reload global list
$this->allInspections = Inspection::where('project_id', $this->project->id)
->with(['feature.layer.phase', 'template', 'user'])
@@ -547,6 +557,58 @@ class ProjectMap extends Component
$this->dispatch('notify', 'Inspección actualizada correctamente');
}
// ─── Delete Inspection ───────────────────────────────────────────────────────
#[On('delete-inspection')]
public function deleteInspection($id)
{
\Log::info('deleteInspection: START', ['id' => $id]);
if (!auth()->user()->can('delete inspections')) {
$this->dispatch('notify', 'Sin permisos para eliminar inspecciones.');
\Log::info('deleteInspection: permission denied');
return;
}
$ins = Inspection::where('project_id', $this->project->id)
->with(['feature', 'media'])
->find($id);
if (!$ins) {
\Log::info('deleteInspection: inspection not found', ['id' => $id]);
$this->dispatch('notify', 'Inspección no encontrada');
return;
}
\Log::info('deleteInspection: BEFORE delete', ['id' => $ins->id, 'deleted_at' => $ins->deleted_at, 'project_id' => $ins->project_id]);
// Delete associated media files (booted event deletes physical files)
$ins->media()->get()->each->delete();
$result = $ins->delete();
\Log::info('deleteInspection: delete() returned', ['result' => $result]);
$fresh = $ins->fresh();
\Log::info('deleteInspection: AFTER delete', ['id' => $id, 'fresh_deleted_at' => $fresh ? $fresh->deleted_at : 'null', 'fresh_exists' => $fresh ? 'yes' : 'no']);
// Refresh lists
$this->allInspections = Inspection::where('project_id', $this->project->id)
->with(['feature.layer.phase', 'template', 'user'])
->orderBy('created_at', 'desc')
->get();
$this->loadInspectionHistory();
$this->dispatch('notify', 'Inspección eliminada correctamente');
\Log::info('deleteInspection: dispatched notify SUCCESS');
// Notificar a usuarios del proyecto (excepto eliminador)
$usersToNotify = $this->project->users()
->where('user_id', '!=', auth()->id())
->get();
foreach ($usersToNotify as $user) {
$user->notify(new \App\Notifications\InspectionDeletedNotification($ins));
}
}
// ─── Feature images ──────────────────────────────────────────────────────────
public function toggleFeatureImages()