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:
@@ -28,12 +28,12 @@
|
||||
{{-- Tipo --}}
|
||||
@php
|
||||
$typeBadge = match($company->type) {
|
||||
'owner' => ['badge-success', 'Promotor'],
|
||||
'constructor' => ['badge-primary', 'Constructor'],
|
||||
'subcontractor' => ['badge-secondary','Subcontratista'],
|
||||
'consultant' => ['badge-info', 'Consultor'],
|
||||
'supplier' => ['badge-warning', 'Proveedor'],
|
||||
default => ['badge-ghost', 'Otro'],
|
||||
'owner' => ['badge-success', __('Owner')],
|
||||
'constructor' => ['badge-primary', __('Constructor')],
|
||||
'subcontractor' => ['badge-secondary',__('Subcontractor')],
|
||||
'consultant' => ['badge-info', __('Consultant')],
|
||||
'supplier' => ['badge-warning', __('Supplier')],
|
||||
default => ['badge-ghost', __('Other')],
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $typeBadge[0] }}">{{ $typeBadge[1] }}</span>
|
||||
@@ -80,9 +80,9 @@
|
||||
{{-- Estado --}}
|
||||
@php
|
||||
$estadoBadge = match($company->estado ?? 'activo') {
|
||||
'activo' => ['badge-success', 'Activo'],
|
||||
'inactivo' => ['badge-ghost', 'Inactivo'],
|
||||
'suspendido' => ['badge-error', 'Suspendido'],
|
||||
'activo' => ['badge-success', __('Active')],
|
||||
'inactivo' => ['badge-ghost', __('Inactive')],
|
||||
'suspendido' => ['badge-error', __('Suspended')],
|
||||
default => ['badge-ghost', ucfirst($company->estado ?? '')],
|
||||
};
|
||||
@endphp
|
||||
@@ -93,12 +93,12 @@
|
||||
<a href="{{ route('companies.edit', $company) }}"
|
||||
class="btn btn-outline btn-sm gap-1" wire:navigate>
|
||||
<x-heroicon-o-pencil class="w-4 h-4" />
|
||||
Editar
|
||||
{{ __('Edit') }}
|
||||
</a>
|
||||
<a href="{{ route('companies.manage') }}"
|
||||
class="btn btn-ghost btn-sm gap-1" wire:navigate>
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||||
Volver
|
||||
{{ __('Back') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,170 +111,170 @@
|
||||
<div class="py-6">
|
||||
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8 space-y-4">
|
||||
|
||||
{{-- Tabs --}}
|
||||
<div role="tablist" class="tabs tabs-bordered">
|
||||
<button role="tab" wire:click="setTab('summary')"
|
||||
class="tab gap-2 {{ $activeTab === 'summary' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-chart-bar class="w-4 h-4" />
|
||||
Resumen
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('people')"
|
||||
class="tab gap-2 {{ $activeTab === 'people' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-users class="w-4 h-4" />
|
||||
Personas
|
||||
<span class="badge badge-sm badge-outline">{{ $usersCount }}</span>
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('projects')"
|
||||
class="tab gap-2 {{ $activeTab === 'projects' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-folder-open class="w-4 h-4" />
|
||||
Proyectos
|
||||
<span class="badge badge-sm badge-outline">{{ $projectsCount }}</span>
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('notes')"
|
||||
class="tab gap-2 {{ $activeTab === 'notes' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-document-text class="w-4 h-4" />
|
||||
Notas
|
||||
@if($company->notes)
|
||||
<span class="badge badge-sm badge-primary">•</span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
<!-- Tabs --}}
|
||||
<div role="tablist" class="tabs tabs-bordered">
|
||||
<button role="tab" wire:click="setTab('summary')"
|
||||
class="tab gap-2 {{ $activeTab === 'summary' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-chart-bar class="w-4 h-4" />
|
||||
{{ __('Summary') }}
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('people')"
|
||||
class="tab gap-2 {{ $activeTab === 'people' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-users class="w-4 h-4" />
|
||||
{{ __('People') }}
|
||||
<span class="badge badge-sm badge-outline">{{ $usersCount }}</span>
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('projects')"
|
||||
class="tab gap-2 {{ $activeTab === 'projects' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-folder-open class="w-4 h-4" />
|
||||
{{ __('Projects') }}
|
||||
<span class="badge badge-sm badge-outline">{{ $projectsCount }}</span>
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('notes')"
|
||||
class="tab gap-2 {{ $activeTab === 'notes' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-document-text class="w-4 h-4" />
|
||||
{{ __('Notes') }}
|
||||
@if($company->notes)
|
||||
<span class="badge badge-sm badge-primary">•</span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: RESUMEN
|
||||
TAB: SUMMARY
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'summary')
|
||||
<div class="space-y-4">
|
||||
@if($activeTab === 'summary')
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- KPIs --}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{{-- KPIs --}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-users class="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ $usersCount }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Personas</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-folder-open class="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ $projectsCount }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Proyectos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-arrow-trending-up class="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ $avgProgress }}<span class="text-lg font-normal text-gray-400">%</span></p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Progreso medio</p>
|
||||
@if($projectsCount > 0)
|
||||
<progress class="progress progress-success w-full h-1 mt-1"
|
||||
value="{{ $avgProgress }}" max="100"></progress>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-orange-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-orange-500" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold {{ $openIssues > 0 ? 'text-warning' : '' }}">{{ $openIssues }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Issues abiertos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Proyectos con progreso --}}
|
||||
@if($company->projects->isNotEmpty())
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-sm mb-3 flex items-center gap-2">
|
||||
<x-heroicon-o-chart-bar-square class="w-4 h-4 text-primary" />
|
||||
Estado de proyectos
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($company->projects as $p)
|
||||
@php
|
||||
$avg = $p->phases->avg('progress_percent') ?? 0;
|
||||
$pStatusBadge = match($p->status) {
|
||||
'in_progress' => ['badge-primary', 'En progreso'],
|
||||
'completed' => ['badge-success', 'Completado'],
|
||||
'paused' => ['badge-warning', 'Pausado'],
|
||||
'planning' => ['badge-ghost', 'Planificación'],
|
||||
default => ['badge-ghost', ucfirst($p->status)],
|
||||
};
|
||||
@endphp
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<a href="{{ route('projects.dashboard', $p) }}"
|
||||
class="font-medium text-sm hover:text-primary transition-colors truncate" wire:navigate>
|
||||
{{ $p->name }}
|
||||
</a>
|
||||
<span class="badge badge-xs {{ $pStatusBadge[0] }} shrink-0">{{ $pStatusBadge[1] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<progress class="progress progress-primary flex-1 h-1.5"
|
||||
value="{{ round($avg) }}" max="100"></progress>
|
||||
<span class="text-xs text-gray-400 shrink-0 w-8 text-right">{{ round($avg) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@if($p->pivot->role_in_project)
|
||||
<span class="badge badge-sm badge-outline shrink-0">{{ $p->pivot->role_in_project }}</span>
|
||||
@endif
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-users class="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
@endforeach
|
||||
<p class="text-3xl font-bold">{{ $usersCount }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">{{ __('People') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Ficha empresa --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-sm mb-3 flex items-center gap-2">
|
||||
<x-heroicon-o-identification class="w-4 h-4 text-primary" />
|
||||
Ficha
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-2 text-sm">
|
||||
@foreach([
|
||||
['NIF/CIF', $company->tax_id],
|
||||
['Tipo', $typeBadge[1]],
|
||||
['Estado', $estadoBadge[1]],
|
||||
['Teléfono', $company->phone],
|
||||
['Email', $company->email],
|
||||
['Dirección', $company->address],
|
||||
['Web', $company->website],
|
||||
] as [$label, $val])
|
||||
@if($val)
|
||||
<div class="flex gap-2 py-1.5 border-b border-base-200">
|
||||
<span class="text-gray-400 w-24 shrink-0">{{ $label }}</span>
|
||||
@if($label === 'Web')
|
||||
<a href="{{ $val }}" target="_blank" rel="noopener"
|
||||
class="text-primary hover:underline truncate">{{ $val }}</a>
|
||||
@else
|
||||
<span class="font-medium truncate">{{ $val }}</span>
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-folder-open class="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ $projectsCount }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">{{ __('Projects') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-arrow-trending-up class="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ $avgProgress }}<span class="text-lg font-normal text-gray-400">%</span></p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">{{ __('Average progress') }}</p>
|
||||
@if($projectsCount > 0)
|
||||
<progress class="progress progress-success w-full h-1 mt-1"
|
||||
value="{{ $avgProgress }}" max="100"></progress>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-orange-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-orange-500" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold {{ $openIssues > 0 ? 'text-warning' : '' }}">{{ $openIssues }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">{{ __('Open issues') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Projects with progress --}}
|
||||
@if($company->projects->isNotEmpty())
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-sm mb-3 flex items-center gap-2">
|
||||
<x-heroicon-o-chart-bar-square class="w-4 h-4 text-primary" />
|
||||
{{ __('Projects status') }}
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($company->projects as $p)
|
||||
@php
|
||||
$avg = $p->phases->avg('progress_percent') ?? 0;
|
||||
$pStatusBadge = match($p->status) {
|
||||
'in_progress' => ['badge-primary', __('In progress')],
|
||||
'completed' => ['badge-success', __('Completed')],
|
||||
'paused' => ['badge-warning', __('Paused')],
|
||||
'planning' => ['badge-ghost', __('Planning')],
|
||||
default => ['badge-ghost', ucfirst($p->status)],
|
||||
};
|
||||
@endphp
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<a href="{{ route('projects.dashboard', $p) }}"
|
||||
class="font-medium text-sm hover:text-primary transition-colors truncate" wire:navigate>
|
||||
{{ $p->name }}
|
||||
</a>
|
||||
<span class="badge badge-xs {{ $pStatusBadge[0] }} shrink-0">{{ $pStatusBadge[1] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<progress class="progress progress-primary flex-1 h-1.5"
|
||||
value="{{ round($avg) }}" max="100"></progress>
|
||||
<span class="text-xs text-gray-400 shrink-0 w-8 text-right">{{ round($avg) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@if($p->pivot->role_in_project)
|
||||
<span class="badge badge-sm badge-outline shrink-0">{{ $p->pivot->role_in_project }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endif
|
||||
{{-- Company card --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-sm mb-3 flex items-center gap-2">
|
||||
<x-heroicon-o-identification class="w-4 h-4 text-primary" />
|
||||
{{ __('Company details') }}
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-2 text-sm">
|
||||
@foreach([
|
||||
[__('Tax ID'), $company->tax_id],
|
||||
[__('Type'), $typeBadge[1]],
|
||||
[__('Status'), $estadoBadge[1]],
|
||||
[__('Phone'), $company->phone],
|
||||
[__('Email'), $company->email],
|
||||
[__('Address'), $company->address],
|
||||
[__('Website'), $company->website],
|
||||
] as [$label, $val])
|
||||
@if($val)
|
||||
<div class="flex gap-2 py-1.5 border-b border-base-200">
|
||||
<span class="text-gray-400 w-24 shrink-0">{{ $label }}</span>
|
||||
@if($label === __('Website'))
|
||||
<a href="{{ $val }}" target="_blank" rel="noopener"
|
||||
class="text-primary hover:underline truncate">{{ $val }}</a>
|
||||
@else
|
||||
<span class="font-medium truncate">{{ $val }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: PERSONAS
|
||||
|
||||
Reference in New Issue
Block a user