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
@@ -1,12 +1,33 @@
<div class="flex items-center gap-1"
x-on:locale-changed.window="window.location.reload()">
@foreach(['es' => ['flag' => 'es.svg', 'label' => 'ES'], 'en' => ['flag' => 'gb.svg', 'label' => 'EN']] as $code => $lang)
<button wire:click="switchLanguage('{{ $code }}')"
class="btn btn-xs gap-1.5 {{ $currentLocale === $code ? 'btn-primary' : 'btn-ghost' }}"
title="{{ __('Language') }}: {{ __($code === 'en' ? 'English' : 'Spanish') }}">
<img src="{{ asset('images/flags/' . $lang['flag']) }}"
class="w-4 h-auto rounded-sm border border-base-300" alt="{{ $lang['label'] }}">
{{ $lang['label'] }}
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="btn btn-ghost btn-sm gap-2" type="button">
<img src="{{ asset('images/flags/' . ($languages[$currentLocale]['flag'] ?? 'gb.svg')) }}"
class="w-5 h-4 rounded-sm border border-base-300"
alt="{{ $languages[$currentLocale]['name'] ?? 'English' }}">
<span class="hidden sm:inline">{{ $languages[$currentLocale]['name'] ?? 'English' }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
@endforeach
</div>
</x-slot>
<x-slot name="content">
<div class="py-1">
@foreach($languages as $code => $lang)
<button wire:click="switchLanguage('{{ $code }}')"
class="flex items-center gap-2 w-full px-3 py-2 text-sm hover:bg-base-200 rounded-md {{ $currentLocale === $code ? 'bg-primary/10 text-primary font-medium' : 'text-base-content' }}"
type="button">
<img src="{{ asset('images/flags/' . $lang['flag']) }}"
class="w-5 h-4 rounded-sm border border-base-300"
alt="{{ $lang['name'] }}">
<span>{{ $lang['name'] }}</span>
@if($currentLocale === $code)
<svg class="ml-auto w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
@endif
</button>
@endforeach
</div>
</x-slot>
</x-dropdown>
@@ -18,11 +18,11 @@
<div tabindex="0" class="dropdown-content z-[50] menu p-0 shadow-lg bg-base-100 rounded-box w-80 mt-1 border border-base-200">
<!-- Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-base-200">
<span class="font-semibold text-base-content">Notificaciones</span>
<span class="font-semibold text-base-content">{{ __('Notifications') }}</span>
@if($unreadCount > 0)
<button wire:click="markAllAsRead"
class="text-xs text-primary hover:underline focus:outline-none">
Marcar todas
{{ __('Mark all as read') }}
</button>
@endif
</div>
@@ -48,7 +48,7 @@
<!-- Content -->
<div class="flex-1 min-w-0">
<p class="text-sm text-base-content leading-snug">
{{ $data['message'] ?? 'Notificación' }}
{{ $data['message'] ?? __('Notification') }}
</p>
<p class="text-xs text-base-content/50 mt-1">
{{ $createdAt->diffForHumans() }}
@@ -59,7 +59,7 @@
@if($isUnread)
<button wire:click="markAsRead('{{ $notification['id'] }}')"
class="shrink-0 text-base-content/40 hover:text-primary focus:outline-none"
title="Marcar como leída">
title="{{ __('Mark as read') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
@@ -68,7 +68,7 @@
</li>
@empty
<li class="px-4 py-8 text-center text-sm text-base-content/50">
No hay notificaciones
{{ __('No notifications') }}
</li>
@endforelse
</ul>
@@ -78,7 +78,7 @@
<div class="border-t border-base-200 px-4 py-2 text-center">
<button wire:click="markAllAsRead"
class="btn btn-ghost btn-xs w-full text-primary">
Marcar todas como leídas
{{ __('Mark all as read') }}
</button>
</div>
@endif