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
+33 -17
View File
@@ -10,8 +10,8 @@
{{-- Saludo --}}
<div>
<h3 class="text-lg font-bold">Hola, {{ $user?->first_name ?? auth()->user()->name }}</h3>
<p class="text-sm text-base-content/60">Un resumen rápido de lo tuyo.</p>
<h3 class="text-lg font-bold">{{ __('Hello, :name', ['name' => $user?->first_name ?? auth()->user()->name]) }}</h3>
<p class="text-sm text-base-content/60">{{ __('A quick summary of your items.') }}</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
@@ -21,10 +21,10 @@
<div class="card-body p-4">
<div class="flex items-center justify-between mb-1">
<h3 class="font-bold flex items-center gap-2">
<x-heroicon-o-folder class="w-5 h-5" /> Mis proyectos
<x-heroicon-o-folder class="w-5 h-5" /> {{ __('My Projects') }}
<span class="badge badge-sm">{{ $projectsCount }}</span>
</h3>
<a href="{{ route('projects.index') }}" wire:navigate class="link link-primary text-sm">Ver todos</a>
<a href="{{ route('projects.index') }}" wire:navigate class="link link-primary text-sm">{{ __('View all') }}</a>
</div>
@forelse($projects as $project)
@php $pct = round($project->phases->avg('progress_percent') ?? 0); @endphp
@@ -37,7 +37,7 @@
<span class="text-xs text-base-content/50 w-9 text-right">{{ $pct }}%</span>
</a>
@empty
<p class="text-sm text-base-content/40 py-3">No tienes proyectos asignados.</p>
<p class="text-sm text-base-content/40 py-3">{{ __('You have no assigned projects.') }}</p>
@endforelse
</div>
</div>
@@ -46,7 +46,7 @@
<div class="card bg-base-100 border border-base-300">
<div class="card-body p-4">
<h3 class="font-bold flex items-center gap-2 mb-1">
<x-heroicon-o-clipboard-document-check class="w-5 h-5" /> Mis tareas
<x-heroicon-o-clipboard-document-check class="w-5 h-5" /> {{ __('My Tasks') }}
<span class="badge badge-sm">{{ $myTasksCount }}</span>
</h3>
@forelse($myTasks as $task)
@@ -55,9 +55,7 @@
class="flex items-center gap-2 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1">
<div class="flex-1 min-w-0">
<div class="text-sm truncate">{{ $task->title }}</div>
<div class="text-xs text-base-content/50 truncate">
{{ $task->issue?->project?->name ?? '—' }}
</div>
<div class="text-xs text-base-content/50 truncate">{{ $task->issue?->project?->name ?? '—' }}</div>
</div>
@if($task->due_date)
<span class="text-xs whitespace-nowrap {{ $task->is_overdue ? 'text-error font-semibold' : 'text-base-content/50' }}">
@@ -66,7 +64,25 @@
@endif
</a>
@empty
<p class="text-sm text-base-content/40 py-3">No tienes tareas pendientes. 🎉</p>
@if($myTasksFromTasks->isNotEmpty())
@foreach($myTasksFromTasks as $task)
<a href="{{ route('projects.tasks.show', [$task->project_id, $task->id]) }}"
wire:navigate
class="flex items-center gap-2 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1">
<div class="flex-1 min-w-0">
<div class="text-sm truncate">{{ $task->title }}</div>
<div class="text-xs text-base-content/50 truncate">{{ $task->project?->name ?? '—' }}</div>
</div>
@if($task->due_date)
<span class="text-xs whitespace-nowrap {{ $task->is_overdue ? 'text-error font-semibold' : 'text-base-content/50' }}">
{{ $task->due_date->format('d/m/Y') }}
</span>
@endif
</a>
@endforeach
@else
<p class="text-sm text-base-content/40 py-3">{{ __('You have no pending tasks. 🎉') }}</p>
@endif
@endforelse
</div>
</div>
@@ -75,12 +91,12 @@
<div class="card bg-base-100 border border-base-300">
<div class="card-body p-4">
<h3 class="font-bold flex items-center gap-2 mb-1">
<x-heroicon-o-exclamation-triangle class="w-5 h-5" /> Incidencias asignadas
<x-heroicon-o-exclamation-triangle class="w-5 h-5" /> {{ __('Assigned Issues') }}
<span class="badge badge-sm">{{ $myIssuesCount }}</span>
</h3>
@forelse($myIssues as $issue)
@php
$sLabel = ['open'=>'Abierto','in_review'=>'En revisión'][$issue->status] ?? $issue->status;
$sLabel = ['open'=>__('Open'),'in_review'=>__('In Review')][$issue->status] ?? $issue->status;
@endphp
<a href="{{ route('projects.issues.show', [$issue->project_id, $issue->id]) }}" wire:navigate
class="flex items-center gap-2 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1">
@@ -92,7 +108,7 @@
<span class="badge badge-ghost badge-xs whitespace-nowrap">{{ $sLabel }}</span>
</a>
@empty
<p class="text-sm text-base-content/40 py-3">No tienes incidencias asignadas.</p>
<p class="text-sm text-base-content/40 py-3">{{ __('You have no assigned issues.') }}</p>
@endforelse
</div>
</div>
@@ -101,7 +117,7 @@
<div class="card bg-base-100 border border-base-300">
<div class="card-body p-4">
<h3 class="font-bold flex items-center gap-2 mb-1">
<x-heroicon-o-bell class="w-5 h-5" /> Notificaciones
<x-heroicon-o-bell class="w-5 h-5" /> {{ __('Notifications') }}
@if($unreadCount)<span class="badge badge-error badge-sm">{{ $unreadCount }}</span>@endif
</h3>
@forelse($notifications as $note)
@@ -115,12 +131,12 @@
class="flex items-start gap-2 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1 {{ is_null($note->read_at) ? 'font-medium' : '' }}">
<span class="mt-1 w-2 h-2 rounded-full shrink-0 {{ is_null($note->read_at) ? 'bg-error' : 'bg-base-300' }}"></span>
<div class="flex-1 min-w-0">
<div class="text-sm">{{ $d['message'] ?? 'Notificación' }}</div>
<div class="text-sm">{{ $d['message'] ?? __('Notification') }}</div>
<div class="text-xs text-base-content/40">{{ $note->created_at->diffForHumans() }}</div>
</div>
</a>
@empty
<p class="text-sm text-base-content/40 py-3">Sin notificaciones.</p>
<p class="text-sm text-base-content/40 py-3">{{ __('No notifications.') }}</p>
@endforelse
</div>
</div>
@@ -128,4 +144,4 @@
</div>
</div>
</div>
</x-app-layout>
</x-app-layout>