feat(issues): incidencias enriquecidas (tareas/comentarios/fotos/verificación) + tabla Rappasoft + logo

Web:
- IssueTask + IssueComment (modelos, migraciones, soft-deletes, campos de sync).
  Issue gana tasks()/comments() y accessor de % de avance derivado de tareas.
- IssueDetail (página): checklist con asignado/fecha límite/progreso, hilo de
  comentarios con foto por comentario, galería de fotos de la incidencia y flujo
  de verificación open→in_review→resolved/closed (+reabrir) con notas.
- Creación/edición en páginas propias (IssueForm), sin modal; al guardar redirige
  al detalle. Rutas projects.issues.create/edit/show.
- Listado con tabla Rappasoft (IssueTable): filtros por estado/prioridad, búsqueda,
  barra de progreso y acciones por fila gateadas por permisos; IssueManager queda
  como contenedor (cabecera + stats) que embebe la tabla.
- Seguridad: pertenencia al proyecto + permisos por acción (view/create/edit/delete
  issues, upload/delete media) en todos los componentes.

API móvil (offline):
- /sync: issue_task.create/update y issue_comment.create (idempotente, LWW).
- /media: parent_entity issue_task / issue_comment.
- bundle + tombstones incluyen issue_tasks / issue_comments.
- openapi.yaml + MOBILE_SYNC_PROTOCOL.md actualizados.

Tests: MobileApiTest 23 passing (+5); IssuesTablePageTest (3) smoke de la tabla.

Branding: logo RTE International — MAI Group (public/images/logo-rte.png) en login
y navegación; application-logo pasa de SVG por defecto a <img>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 12:12:39 +02:00
parent 14758136b6
commit 3f240e5277
25 changed files with 1604 additions and 566 deletions
+17 -11
View File
@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Feature;
use App\Models\Issue;
use App\Models\IssueComment;
use App\Models\IssueTask;
use App\Models\Layer;
use App\Models\Media;
use App\Models\Phase;
@@ -17,11 +19,13 @@ use Illuminate\Support\Str;
class MediaController extends Controller
{
private array $map = [
'feature' => Feature::class,
'issue' => Issue::class,
'project' => Project::class,
'phase' => Phase::class,
'layer' => Layer::class,
'feature' => Feature::class,
'issue' => Issue::class,
'issue_task' => IssueTask::class,
'issue_comment' => IssueComment::class,
'project' => Project::class,
'phase' => Phase::class,
'layer' => Layer::class,
];
/** Upload a file (multipart) and attach it to a parent record. Idempotent by uuid. */
@@ -73,12 +77,14 @@ class MediaController extends Controller
private function projectOf(string $entity, $parent): ?Project
{
return match ($entity) {
'project' => $parent,
'phase' => $parent->project,
'layer' => $parent->phase?->project,
'feature' => $parent->layer?->phase?->project,
'issue' => $parent->project,
default => null,
'project' => $parent,
'phase' => $parent->project,
'layer' => $parent->phase?->project,
'feature' => $parent->layer?->phase?->project,
'issue' => $parent->project,
'issue_task' => $parent->issue?->project,
'issue_comment' => $parent->issue?->project,
default => null,
};
}