diff --git a/app/Console/Commands/ConvertSpatialFile.php b/app/Console/Commands/ConvertSpatialFile.php deleted file mode 100644 index 63728b5..0000000 --- a/app/Console/Commands/ConvertSpatialFile.php +++ /dev/null @@ -1,44 +0,0 @@ -argument('file'); - $phaseId = $this->argument('phase_id'); - $phase = Phase::findOrFail($phaseId); - $file = new \Illuminate\Http\UploadedFile($filePath, basename($filePath)); - - $geojson = SpatialFileConverter::convertToGeoJson($file, $file->getClientOriginalName()); - if ($geojson) { - $layer = $phase->layers()->create([ - 'project_id' => $phase->project_id, - 'name' => 'Converted: ' . basename($filePath), - 'geojson_data' => $geojson, - 'uploaded_by' => 1, // admin - 'original_file' => $filePath - ]); - $this->info("GeoJSON saved to layer ID {$layer->id}"); - } else { - $this->error("Conversion failed for file type."); - } - } -} diff --git a/app/Console/Commands/MigrateGeojsonToFeatures.php b/app/Console/Commands/MigrateGeojsonToFeatures.php deleted file mode 100644 index b732cc3..0000000 --- a/app/Console/Commands/MigrateGeojsonToFeatures.php +++ /dev/null @@ -1,45 +0,0 @@ -get(); - $totalFeatures = 0; - - foreach ($layers as $layer) { - $geojson = $layer->geojson_data; - if (!isset($geojson['features'])) continue; - - foreach ($geojson['features'] as $featureData) { - $geometry = $featureData['geometry']; - $props = $featureData['properties'] ?? []; - - Feature::create([ - 'layer_id' => $layer->id, - 'name' => $props['name'] ?? null, - 'geometry' => $geometry, - 'properties' => $props, - 'template_id' => $props['template_id'] ?? null, - 'progress' => $props['progress'] ?? 0, - 'responsible' => $props['responsible'] ?? null, - ]); - $totalFeatures++; - } - - // Opcional: Marcar la capa como migrada (podrías agregar columna 'migrated_at') - $this->info("Layer {$layer->id} ({$layer->name}) migrated."); - } - - $this->info("Total features migrated: {$totalFeatures}"); - } -} \ No newline at end of file diff --git a/app/DTO/ReportFilters.php b/app/DTO/ReportFilters.php index ec19c79..090e5c5 100644 --- a/app/DTO/ReportFilters.php +++ b/app/DTO/ReportFilters.php @@ -3,7 +3,6 @@ namespace App\DTO; use Carbon\Carbon; -use Illuminate\Support\Collection; class ReportFilters { @@ -21,10 +20,10 @@ class ReportFilters return new self( dateFrom: isset($data['date_from']) ? Carbon::parse($data['date_from']) : null, dateTo: isset($data['date_to']) ? Carbon::parse($data['date_to']) : null, - entityTypes: $data['entity_types'] ?? ['phases','features','inspections','issues','tasks'], - includePhotos: (bool)($data['include_photos'] ?? false), + entityTypes: $data['entity_types'] ?? ['phases', 'features', 'inspections', 'issues', 'tasks'], + includePhotos: (bool) ($data['include_photos'] ?? false), format: $data['format'] ?? 'html', - includeCharts: (bool)($data['include_charts'] ?? false), + includeCharts: (bool) ($data['include_charts'] ?? false), ); } @@ -55,14 +54,15 @@ class ReportFilters public function getDateRangeLabel(): string { if ($this->dateFrom && $this->dateTo) { - return $this->dateFrom->format('d/m/Y') . ' - ' . $this->dateTo->format('d/m/Y'); + return $this->dateFrom->format('d/m/Y').' - '.$this->dateTo->format('d/m/Y'); } if ($this->dateFrom) { - return 'Desde ' . $this->dateFrom->format('d/m/Y'); + return 'Desde '.$this->dateFrom->format('d/m/Y'); } if ($this->dateTo) { - return 'Hasta ' . $this->dateTo->format('d/m/Y'); + return 'Hasta '.$this->dateTo->format('d/m/Y'); } + return 'Todo el período'; } -} \ No newline at end of file +} diff --git a/app/Exports/InspectionsExport.php b/app/Exports/InspectionsExport.php index 06d156a..8df91ca 100644 --- a/app/Exports/InspectionsExport.php +++ b/app/Exports/InspectionsExport.php @@ -18,7 +18,7 @@ class InspectionsExport implements FromCollection, WithHeadings 'status', 'notes', 'created_at', - 'updated_at' + 'updated_at', ])->get(); } @@ -32,7 +32,7 @@ class InspectionsExport implements FromCollection, WithHeadings 'Estado', 'Notas', 'Creado el', - 'Actualizado el' + 'Actualizado el', ]; } } diff --git a/app/Exports/PhasesExport.php b/app/Exports/PhasesExport.php index 3df9db4..4bd5476 100644 --- a/app/Exports/PhasesExport.php +++ b/app/Exports/PhasesExport.php @@ -18,7 +18,7 @@ class PhasesExport implements FromCollection, WithHeadings 'start_date', 'end_date', 'created_at', - 'updated_at' + 'updated_at', ])->get(); } @@ -32,7 +32,7 @@ class PhasesExport implements FromCollection, WithHeadings 'Fecha de inicio', 'Fecha de fin', 'Creado el', - 'Actualizado el' + 'Actualizado el', ]; } } diff --git a/app/Exports/ProjectReportExport.php b/app/Exports/ProjectReportExport.php index 58c3058..6c54aa8 100644 --- a/app/Exports/ProjectReportExport.php +++ b/app/Exports/ProjectReportExport.php @@ -5,20 +5,22 @@ namespace App\Exports; use App\DTO\ReportFilters; use App\Models\Project; use Maatwebsite\Excel\Concerns\FromArray; -use Maatwebsite\Excel\Concerns\WithMultipleSheets; -use Maatwebsite\Excel\Concerns\WithTitle; -use Maatwebsite\Excel\Concerns\WithHeadings; -use Maatwebsite\Excel\Concerns\WithStyles; use Maatwebsite\Excel\Concerns\WithColumnWidths; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use Maatwebsite\Excel\Concerns\WithHeadings; +use Maatwebsite\Excel\Concerns\WithMultipleSheets; +use Maatwebsite\Excel\Concerns\WithStyles; +use Maatwebsite\Excel\Concerns\WithTitle; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Fill; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class ProjectReportExport implements WithMultipleSheets { protected Project $project; + protected ReportFilters $filters; + protected array $data; public function __construct(Project $project, ReportFilters $filters, array $data) @@ -34,35 +36,35 @@ class ProjectReportExport implements WithMultipleSheets new SummarySheet($this->data['summary'] ?? [], $this->project, $this->filters), ]; - if (!empty($this->data['phases'])) { + if (! empty($this->data['phases'])) { $sheets[] = new PhasesSheet($this->data['phases']); } - if (!empty($this->data['features'])) { + if (! empty($this->data['features'])) { $sheets[] = new FeaturesSheet($this->data['features']); } - if (!empty($this->data['inspections'])) { + if (! empty($this->data['inspections'])) { $sheets[] = new InspectionsSheet($this->data['inspections']); } - if (!empty($this->data['issues'])) { + if (! empty($this->data['issues'])) { $sheets[] = new IssuesSheet($this->data['issues']); } - if (!empty($this->data['tasks'])) { + if (! empty($this->data['tasks'])) { $sheets[] = new TasksSheet($this->data['tasks']); } - if (!empty($this->data['deviations'])) { + if (! empty($this->data['deviations'])) { $sheets[] = new DeviationsSheet($this->data['deviations']); } - if (!empty($this->data['media'])) { + if (! empty($this->data['media'])) { $sheets[] = new MediaSheet($this->data['media']); } - if (!empty($this->data['progress_curve'])) { + if (! empty($this->data['progress_curve'])) { $sheets[] = new ProgressCurveSheet($this->data['progress_curve']); } @@ -77,7 +79,7 @@ class ProjectReportExport implements WithMultipleSheets // Base Sheet with common styling // ============================================================ -abstract class BaseSheet implements FromArray, WithHeadings, WithStyles, WithColumnWidths, WithTitle +abstract class BaseSheet implements FromArray, WithColumnWidths, WithHeadings, WithStyles, WithTitle { protected array $rows = []; @@ -129,7 +131,7 @@ abstract class BaseSheet implements FromArray, WithHeadings, WithStyles, WithCol // Auto-filter $sheet->setAutoFilter($sheet->calculateWorksheetDimension()); - + // Freeze header row $sheet->freezePane('A2'); } @@ -147,13 +149,14 @@ abstract class BaseSheet implements FromArray, WithHeadings, WithStyles, WithCol class SummarySheet extends BaseSheet { protected Project $project; + protected ReportFilters $filters; public function __construct(array $summary, Project $project, ReportFilters $filters) { $this->project = $project; $this->filters = $filters; - + $rows = [ ['Informe de Proyecto', $project->name], ['Referencia', $project->reference ?? '—'], @@ -194,7 +197,7 @@ class SummarySheet extends BaseSheet ['Retrasadas', $summary['phases_delayed'] ?? 0], ['Sin datos', $summary['phases_no_data'] ?? 0], ]; - + parent::__construct($rows); } @@ -218,10 +221,10 @@ class PhasesSheet extends BaseSheet public function headings(): array { return [ - 'ID', 'Fase', 'Orden', 'Color', 'Inicio Plan', 'Fin Plan', - 'Inicio Real', 'Fin Real', 'Progreso (%)', 'Progreso Plan (%)', + 'ID', 'Fase', 'Orden', 'Color', 'Inicio Plan', 'Fin Plan', + 'Inicio Real', 'Fin Real', 'Progreso (%)', 'Progreso Plan (%)', 'Desvío Fin (días)', 'Desvío Inicio (días)', 'SPI', 'En Plazo', - 'Elementos', 'Completados', 'Capas' + 'Elementos', 'Completados', 'Capas', ]; } @@ -252,9 +255,9 @@ class PhasesSheet extends BaseSheet public function columnWidths(): array { - return ['A' => 8, 'B' => 25, 'C' => 8, 'D' => 10, 'E' => 12, 'F' => 12, - 'G' => 12, 'H' => 12, 'I' => 12, 'J' => 14, 'K' => 14, 'L' => 14, - 'M' => 8, 'N' => 10, 'O' => 10, 'P' => 12, 'Q' => 10]; + return ['A' => 8, 'B' => 25, 'C' => 8, 'D' => 10, 'E' => 12, 'F' => 12, + 'G' => 12, 'H' => 12, 'I' => 12, 'J' => 14, 'K' => 14, 'L' => 14, + 'M' => 8, 'N' => 10, 'O' => 10, 'P' => 12, 'Q' => 10]; } } @@ -270,7 +273,7 @@ class FeaturesSheet extends BaseSheet 'ID', 'Elemento', 'Fase', 'Capa', 'Estado', 'Progreso (%)', 'Progreso Plan (%)', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', 'Desvío Fin (días)', 'Desvío Inicio (días)', 'SPI', 'En Plazo', 'Responsable', 'Template', - 'Última Inspección', 'Resultado', 'Inspecciones', 'Issues Abiertos' + 'Última Inspección', 'Resultado', 'Inspecciones', 'Issues Abiertos', ]; } @@ -305,10 +308,10 @@ class FeaturesSheet extends BaseSheet public function columnWidths(): array { - return ['A' => 8, 'B' => 25, 'C' => 20, 'D' => 20, 'E' => 15, 'F' => 12, - 'G' => 14, 'H' => 12, 'I' => 12, 'J' => 12, 'K' => 12, 'L' => 14, - 'M' => 14, 'N' => 8, 'O' => 10, 'P' => 20, 'Q' => 20, 'R' => 14, - 'S' => 12, 'T' => 12, 'U' => 12]; + return ['A' => 8, 'B' => 25, 'C' => 20, 'D' => 20, 'E' => 15, 'F' => 12, + 'G' => 14, 'H' => 12, 'I' => 12, 'J' => 12, 'K' => 12, 'L' => 14, + 'M' => 14, 'N' => 8, 'O' => 10, 'P' => 20, 'Q' => 20, 'R' => 14, + 'S' => 12, 'T' => 12, 'U' => 12]; } } @@ -321,8 +324,8 @@ class InspectionsSheet extends BaseSheet public function headings(): array { return [ - 'ID', 'Elemento', 'Fase', 'Template', 'Inspector', 'Fecha', - 'Estado', 'Resultado', 'Notas', 'Fotos' + 'ID', 'Elemento', 'Fase', 'Template', 'Inspector', 'Fecha', + 'Estado', 'Resultado', 'Notas', 'Fotos', ]; } @@ -346,8 +349,8 @@ class InspectionsSheet extends BaseSheet public function columnWidths(): array { - return ['A' => 8, 'B' => 25, 'C' => 20, 'D' => 25, 'E' => 20, 'F' => 18, - 'G' => 12, 'H' => 12, 'I' => 40, 'J' => 8]; + return ['A' => 8, 'B' => 25, 'C' => 20, 'D' => 25, 'E' => 20, 'F' => 18, + 'G' => 12, 'H' => 12, 'I' => 40, 'J' => 8]; } } @@ -360,9 +363,9 @@ class IssuesSheet extends BaseSheet public function headings(): array { return [ - 'ID', 'Título', 'Elemento', 'Fase', 'Prioridad', 'Estado', - 'Reportado por', 'Asignado a', 'Creado', 'Cerrado', 'Días abierto', - 'Tareas total', 'Tareas completadas' + 'ID', 'Título', 'Elemento', 'Fase', 'Prioridad', 'Estado', + 'Reportado por', 'Asignado a', 'Creado', 'Cerrado', 'Días abierto', + 'Tareas total', 'Tareas completadas', ]; } @@ -389,8 +392,8 @@ class IssuesSheet extends BaseSheet public function columnWidths(): array { - return ['A' => 8, 'B' => 35, 'C' => 20, 'D' => 20, 'E' => 12, 'F' => 15, - 'G' => 20, 'H' => 20, 'I' => 12, 'J' => 12, 'K' => 12, 'L' => 12, 'M' => 14]; + return ['A' => 8, 'B' => 35, 'C' => 20, 'D' => 20, 'E' => 12, 'F' => 15, + 'G' => 20, 'H' => 20, 'I' => 12, 'J' => 12, 'K' => 12, 'L' => 12, 'M' => 14]; } } @@ -404,15 +407,16 @@ class TasksSheet extends BaseSheet { return [ 'ID', 'Tarea', 'Fase', 'Estado', 'Prioridad', 'Asignado', 'Creador', - 'Fecha inicio', 'Fecha fin', 'Completada', 'Horas est.', 'Horas real', - 'Progreso (%)', 'Vencida', 'Subtareas' + 'Fecha inicio', 'Fecha fin', 'Completada', 'Horas est.', 'Horas real', + 'Progreso (%)', 'Vencida', 'Subtareas', ]; } public function array(): array { return array_map(function ($task) { - $subtasksStr = implode('; ', array_map(fn($st) => "{$st['title']} ({$st['status']})", $task['subtasks'])); + $subtasksStr = implode('; ', array_map(fn ($st) => "{$st['title']} ({$st['status']})", $task['subtasks'])); + return [ $task['id'], $task['title'], @@ -435,9 +439,9 @@ class TasksSheet extends BaseSheet public function columnWidths(): array { - return ['A' => 8, 'B' => 30, 'C' => 20, 'D' => 15, 'E' => 12, 'F' => 20, - 'G' => 20, 'H' => 12, 'I' => 12, 'J' => 12, 'K' => 10, 'L' => 10, - 'M' => 12, 'N' => 10, 'O' => 40]; + return ['A' => 8, 'B' => 30, 'C' => 20, 'D' => 15, 'E' => 12, 'F' => 20, + 'G' => 20, 'H' => 12, 'I' => 12, 'J' => 12, 'K' => 10, 'L' => 10, + 'M' => 12, 'N' => 10, 'O' => 40]; } } @@ -453,14 +457,14 @@ class DeviationsSheet extends BaseSheet { $this->deviations = $deviations; $rows = []; - + // Phase deviations - if (!empty($deviations['phases'])) { + if (! empty($deviations['phases'])) { $rows[] = ['=== DESVÍOS POR FASE ===', '', '', '', '', '', '', '', '', '', '']; - $rows[] = ['ID', 'Fase', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', - 'Desvío Inicio (d)', 'Desvío Fin (d)', 'Prog. Plan (%)', 'Prog. Real (%)', - 'Desvío Prog.', 'SPI', 'En Plazo']; - + $rows[] = ['ID', 'Fase', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', + 'Desvío Inicio (d)', 'Desvío Fin (d)', 'Prog. Plan (%)', 'Prog. Real (%)', + 'Desvío Prog.', 'SPI', 'En Plazo']; + foreach ($deviations['phases'] as $phase) { $rows[] = [ $phase['id'], $phase['name'], $phase['planned_start'], $phase['planned_end'], @@ -475,12 +479,12 @@ class DeviationsSheet extends BaseSheet } // Feature deviations - if (!empty($deviations['features'])) { + if (! empty($deviations['features'])) { $rows[] = ['=== DESVÍOS POR ELEMENTO ===', '', '', '', '', '', '', '', '', '', '', '', '']; - $rows[] = ['ID', 'Elemento', 'Fase', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', - 'Desvío Inicio (d)', 'Desvío Fin (d)', 'Prog. Plan (%)', 'Prog. Real (%)', - 'Desvío Prog.', 'SPI', 'En Plazo', 'Responsable']; - + $rows[] = ['ID', 'Elemento', 'Fase', 'Inicio Plan', 'Fin Plan', 'Inicio Real', 'Fin Real', + 'Desvío Inicio (d)', 'Desvío Fin (d)', 'Prog. Plan (%)', 'Prog. Real (%)', + 'Desvío Prog.', 'SPI', 'En Plazo', 'Responsable']; + foreach ($deviations['features'] as $feature) { $rows[] = [ $feature['id'], $feature['name'], $feature['phase'], @@ -497,7 +501,7 @@ class DeviationsSheet extends BaseSheet } // Summary - if (!empty($deviations['summary'])) { + if (! empty($deviations['summary'])) { $rows[] = ['=== RESUMEN DESVÍOS ===', '']; $rows[] = ['Fases retrasadas', $deviations['summary']['phases_delayed'] ?? 0]; $rows[] = ['Fases adelantadas', $deviations['summary']['phases_early'] ?? 0]; @@ -522,9 +526,9 @@ class DeviationsSheet extends BaseSheet public function columnWidths(): array { - return ['A' => 25, 'B' => 25, 'C' => 20, 'D' => 12, 'E' => 12, 'F' => 12, - 'G' => 12, 'H' => 14, 'I' => 14, 'J' => 14, 'K' => 14, 'L' => 10, - 'M' => 10, 'N' => 20]; + return ['A' => 25, 'B' => 25, 'C' => 20, 'D' => 12, 'E' => 12, 'F' => 12, + 'G' => 12, 'H' => 14, 'I' => 14, 'J' => 14, 'K' => 14, 'L' => 10, + 'M' => 10, 'N' => 20]; } } @@ -558,8 +562,8 @@ class MediaSheet extends BaseSheet public function columnWidths(): array { - return ['A' => 8, 'B' => 30, 'C' => 12, 'D' => 15, 'E' => 30, 'F' => 12, - 'G' => 20, 'H' => 18, 'I' => 50]; + return ['A' => 8, 'B' => 30, 'C' => 12, 'D' => 15, 'E' => 30, 'F' => 12, + 'G' => 20, 'H' => 18, 'I' => 50]; } } @@ -574,17 +578,17 @@ class ProgressCurveSheet extends BaseSheet public function __construct(array $curveData) { $this->curveData = $curveData; - + $rows = [['Fecha', 'Progreso Planificado (%)', 'Progreso Real (%)']]; - + $labels = $curveData['labels'] ?? []; $planned = $curveData['planned'] ?? []; $actual = $curveData['actual'] ?? []; - + for ($i = 0; $i < count($labels); $i++) { $rows[] = [$labels[$i], $planned[$i] ?? 0, $actual[$i] ?? 0]; } - + $this->rows = $rows; } @@ -611,13 +615,14 @@ class ProgressCurveSheet extends BaseSheet class ParametersSheet extends BaseSheet { protected ReportFilters $filters; + protected Project $project; public function __construct(ReportFilters $filters, Project $project) { $this->filters = $filters; $this->project = $project; - + $rows = [ ['Parámetro', 'Valor'], ['Proyecto', $project->name], @@ -625,7 +630,7 @@ class ParametersSheet extends BaseSheet ['Fecha desde', $filters->dateFrom?->format('d/m/Y') ?? '—'], ['Fecha hasta', $filters->dateTo?->format('d/m/Y') ?? '—'], ['Entidades incluidas', implode(', ', array_map( - fn($e) => $filters->getAvailableEntities()[$e] ?? $e, + fn ($e) => $filters->getAvailableEntities()[$e] ?? $e, $filters->entityTypes ))], ['Incluir fotos', $filters->includePhotos ? 'Sí' : 'No'], @@ -634,7 +639,7 @@ class ParametersSheet extends BaseSheet ['Generado', now()->format('d/m/Y H:i')], ['Generado por', auth()->guard()->user()?->name ?? 'Sistema'], ]; - + parent::__construct($rows); } @@ -652,4 +657,4 @@ class ParametersSheet extends BaseSheet { return ['A' => 30, 'B' => 50]; } -} \ No newline at end of file +} diff --git a/app/Exports/ProjectsExport.php b/app/Exports/ProjectsExport.php index 3ad7ad6..d9e8caa 100644 --- a/app/Exports/ProjectsExport.php +++ b/app/Exports/ProjectsExport.php @@ -18,7 +18,7 @@ class ProjectsExport implements FromCollection, WithHeadings 'end_date', 'status', 'created_at', - 'updated_at' + 'updated_at', ])->get(); } @@ -32,7 +32,7 @@ class ProjectsExport implements FromCollection, WithHeadings 'Fecha de fin', 'Estado', 'Creado el', - 'Actualizado el' + 'Actualizado el', ]; } } diff --git a/app/Http/Controllers/Api/V1/AuthController.php b/app/Http/Controllers/Api/V1/AuthController.php index dcf6e5f..039579b 100644 --- a/app/Http/Controllers/Api/V1/AuthController.php +++ b/app/Http/Controllers/Api/V1/AuthController.php @@ -17,8 +17,8 @@ class AuthController extends Controller public function login(Request $request) { $data = $request->validate([ - 'email' => ['required', 'email'], - 'password' => ['required', 'string'], + 'email' => ['required', 'email'], + 'password' => ['required', 'string'], 'device_name' => ['required', 'string', 'max:255'], 'app_version' => ['nullable', 'string', 'max:50'], ]); @@ -39,15 +39,15 @@ class AuthController extends Controller Device::updateOrCreate( ['user_id' => $user->id, 'name' => $data['device_name']], [ - 'token_id' => $token->accessToken->id, - 'app_version' => $data['app_version'] ?? null, + 'token_id' => $token->accessToken->id, + 'app_version' => $data['app_version'] ?? null, 'last_seen_at' => now(), ] ); return response()->json([ 'token' => $token->plainTextToken, - 'user' => $this->userPayload($user), + 'user' => $this->userPayload($user), ]); } @@ -70,10 +70,10 @@ class AuthController extends Controller private function userPayload(User $user): array { return [ - 'id' => $user->id, - 'name' => $user->name, - 'email' => $user->email, - 'roles' => $user->getRoleNames(), + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'roles' => $user->getRoleNames(), 'permissions' => $user->getAllPermissions()->pluck('name')->values(), ]; } diff --git a/app/Http/Controllers/Api/V1/MediaController.php b/app/Http/Controllers/Api/V1/MediaController.php index 77ecca5..439dc30 100644 --- a/app/Http/Controllers/Api/V1/MediaController.php +++ b/app/Http/Controllers/Api/V1/MediaController.php @@ -13,31 +13,31 @@ use App\Models\Phase; use App\Models\Project; use App\Models\User; use Illuminate\Http\Request; -use Illuminate\Validation\Rule; use Illuminate\Support\Str; +use Illuminate\Validation\Rule; class MediaController extends Controller { private array $map = [ - 'feature' => Feature::class, - 'issue' => Issue::class, - 'issue_task' => IssueTask::class, + 'feature' => Feature::class, + 'issue' => Issue::class, + 'issue_task' => IssueTask::class, 'issue_comment' => IssueComment::class, - 'project' => Project::class, - 'phase' => Phase::class, - 'layer' => Layer::class, + 'project' => Project::class, + 'phase' => Phase::class, + 'layer' => Layer::class, ]; /** Upload a file (multipart) and attach it to a parent record. Idempotent by uuid. */ public function upload(Request $request) { $data = $request->validate([ - 'uuid' => ['required', 'uuid'], + 'uuid' => ['required', 'uuid'], 'parent_entity' => ['required', Rule::in(array_keys($this->map))], - 'parent_id' => ['required', 'integer'], - 'file' => ['required', 'file', 'max:20480'], // 20 MB - 'category' => ['nullable', 'in:image,document,other'], - 'description' => ['nullable', 'string'], + 'parent_id' => ['required', 'integer'], + 'file' => ['required', 'file', 'max:20480'], // 20 MB + 'category' => ['nullable', 'in:image,document,other'], + 'description' => ['nullable', 'string'], ]); // Idempotency: same uuid already uploaded → return it. @@ -59,15 +59,15 @@ class MediaController extends Controller $mime = $file->getClientMimeType(); $media = $parent->media()->create([ - 'uuid' => $data['uuid'], - 'name' => $file->getClientOriginalName(), - 'file_path' => $path, - 'file_type' => $mime, - 'file_extension' => $file->getClientOriginalExtension(), - 'file_size' => $file->getSize(), - 'category' => $data['category'] ?? (Str::startsWith($mime, 'image/') ? 'image' : 'document'), - 'description' => $data['description'] ?? null, - 'uploaded_by' => $user->id, + 'uuid' => $data['uuid'], + 'name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'file_type' => $mime, + 'file_extension' => $file->getClientOriginalExtension(), + 'file_size' => $file->getSize(), + 'category' => $data['category'] ?? (Str::startsWith($mime, 'image/') ? 'image' : 'document'), + 'description' => $data['description'] ?? null, + 'uploaded_by' => $user->id, 'client_updated_at' => $request->input('client_updated_at'), ]); @@ -77,14 +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, - 'issue_task' => $parent->issue?->project, + '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, + default => null, }; } @@ -93,6 +93,7 @@ class MediaController extends Controller if (! $project) { return false; } + return $user->can('manage all') || $project->users()->where('user_id', $user->id)->exists(); } @@ -100,12 +101,12 @@ class MediaController extends Controller private function payload(Media $m): array { return [ - 'id' => $m->id, - 'uuid' => $m->uuid, - 'url' => $m->url, - 'name' => $m->name, - 'file_type' => $m->file_type, - 'category' => $m->category, + 'id' => $m->id, + 'uuid' => $m->uuid, + 'url' => $m->url, + 'name' => $m->name, + 'file_type' => $m->file_type, + 'category' => $m->category, 'updated_at' => $m->updated_at?->toIso8601String(), ]; } diff --git a/app/Http/Controllers/Api/V1/ProjectApiController.php b/app/Http/Controllers/Api/V1/ProjectApiController.php index 113757b..ee32c00 100644 --- a/app/Http/Controllers/Api/V1/ProjectApiController.php +++ b/app/Http/Controllers/Api/V1/ProjectApiController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\V1; use App\Http\Controllers\Controller; use App\Models\Feature; +use App\Models\FeatureType; use App\Models\Inspection; use App\Models\InspectionTemplate; use App\Models\Issue; @@ -53,35 +54,35 @@ class ProjectApiController extends Controller $templates = $changed(InspectionTemplate::whereHas('projects', fn ($q) => $q->where('projects.id', $project->id)))->get(); $allIssueIds = Issue::withTrashed()->where('project_id', $project->id)->pluck('id'); - $issueTasks = $changed(IssueTask::whereIn('issue_id', $allIssueIds))->get(); + $issueTasks = $changed(IssueTask::whereIn('issue_id', $allIssueIds))->get(); $issueComments = $changed(IssueComment::whereIn('issue_id', $allIssueIds))->get(); $featureIds = Feature::whereIn('layer_id', $allLayerIds)->pluck('id'); - $issueIds = Issue::where('project_id', $project->id)->pluck('id'); - $taskIds = IssueTask::whereIn('issue_id', $allIssueIds)->pluck('id'); + $issueIds = Issue::where('project_id', $project->id)->pluck('id'); + $taskIds = IssueTask::whereIn('issue_id', $allIssueIds)->pluck('id'); $commentIds = IssueComment::whereIn('issue_id', $allIssueIds)->pluck('id'); $media = $changed(Media::where(function ($q) use ($project, $featureIds, $issueIds, $taskIds, $commentIds) { $q->where(fn ($w) => $w->where('mediable_type', Project::class)->where('mediable_id', $project->id)) - ->orWhere(fn ($w) => $w->where('mediable_type', Feature::class)->whereIn('mediable_id', $featureIds)) - ->orWhere(fn ($w) => $w->where('mediable_type', Issue::class)->whereIn('mediable_id', $issueIds)) - ->orWhere(fn ($w) => $w->where('mediable_type', IssueTask::class)->whereIn('mediable_id', $taskIds)) - ->orWhere(fn ($w) => $w->where('mediable_type', IssueComment::class)->whereIn('mediable_id', $commentIds)); + ->orWhere(fn ($w) => $w->where('mediable_type', Feature::class)->whereIn('mediable_id', $featureIds)) + ->orWhere(fn ($w) => $w->where('mediable_type', Issue::class)->whereIn('mediable_id', $issueIds)) + ->orWhere(fn ($w) => $w->where('mediable_type', IssueTask::class)->whereIn('mediable_id', $taskIds)) + ->orWhere(fn ($w) => $w->where('mediable_type', IssueComment::class)->whereIn('mediable_id', $commentIds)); }))->get(); return response()->json([ - 'server_time' => now()->toIso8601String(), - 'project' => $this->mapProject($project), - 'phases' => $phases->map(fn ($p) => $this->mapPhase($p))->values(), - 'layers' => $layers->map(fn ($l) => $this->mapLayer($l))->values(), - 'features' => $features->map(fn ($f) => $this->mapFeature($f))->values(), - 'feature_types' => \App\Models\FeatureType::orderBy('name')->get(['id', 'name', 'color'])->values(), - 'inspections' => $inspections->map(fn ($i) => $this->mapInspection($i))->values(), - 'issues' => $issues->map(fn ($i) => $this->mapIssue($i))->values(), - 'issue_tasks' => $issueTasks->map(fn ($t) => $this->mapIssueTask($t))->values(), + 'server_time' => now()->toIso8601String(), + 'project' => $this->mapProject($project), + 'phases' => $phases->map(fn ($p) => $this->mapPhase($p))->values(), + 'layers' => $layers->map(fn ($l) => $this->mapLayer($l))->values(), + 'features' => $features->map(fn ($f) => $this->mapFeature($f))->values(), + 'feature_types' => FeatureType::orderBy('name')->get(['id', 'name', 'color'])->values(), + 'inspections' => $inspections->map(fn ($i) => $this->mapInspection($i))->values(), + 'issues' => $issues->map(fn ($i) => $this->mapIssue($i))->values(), + 'issue_tasks' => $issueTasks->map(fn ($t) => $this->mapIssueTask($t))->values(), 'issue_comments' => $issueComments->map(fn ($c) => $this->mapIssueComment($c))->values(), - 'templates' => $templates->map(fn ($t) => $this->mapTemplate($t))->values(), - 'media' => $media->map(fn ($m) => $this->mapMedia($m))->values(), - 'deleted' => $since ? $this->tombstones($since, $project, $allPhaseIds, $allLayerIds, $allIssueIds) : (object) [], + 'templates' => $templates->map(fn ($t) => $this->mapTemplate($t))->values(), + 'media' => $media->map(fn ($m) => $this->mapMedia($m))->values(), + 'deleted' => $since ? $this->tombstones($since, $project, $allPhaseIds, $allLayerIds, $allIssueIds) : (object) [], ]); } @@ -118,12 +119,12 @@ class ProjectApiController extends Controller private function tombstones(Carbon $since, Project $project, $allPhaseIds, $allLayerIds, $allIssueIds): array { return [ - 'phases' => Phase::onlyTrashed()->where('project_id', $project->id)->where('deleted_at', '>', $since)->pluck('id')->values(), - 'layers' => Layer::onlyTrashed()->whereIn('phase_id', $allPhaseIds)->where('deleted_at', '>', $since)->pluck('id')->values(), - 'features' => Feature::onlyTrashed()->whereIn('layer_id', $allLayerIds)->where('deleted_at', '>', $since)->pluck('id')->values(), - 'inspections' => Inspection::onlyTrashed()->where('project_id', $project->id)->where('deleted_at', '>', $since)->pluck('id')->values(), - 'issues' => Issue::onlyTrashed()->where('project_id', $project->id)->where('deleted_at', '>', $since)->pluck('id')->values(), - 'issue_tasks' => IssueTask::onlyTrashed()->whereIn('issue_id', $allIssueIds)->where('deleted_at', '>', $since)->pluck('id')->values(), + 'phases' => Phase::onlyTrashed()->where('project_id', $project->id)->where('deleted_at', '>', $since)->pluck('id')->values(), + 'layers' => Layer::onlyTrashed()->whereIn('phase_id', $allPhaseIds)->where('deleted_at', '>', $since)->pluck('id')->values(), + 'features' => Feature::onlyTrashed()->whereIn('layer_id', $allLayerIds)->where('deleted_at', '>', $since)->pluck('id')->values(), + 'inspections' => Inspection::onlyTrashed()->where('project_id', $project->id)->where('deleted_at', '>', $since)->pluck('id')->values(), + 'issues' => Issue::onlyTrashed()->where('project_id', $project->id)->where('deleted_at', '>', $since)->pluck('id')->values(), + 'issue_tasks' => IssueTask::onlyTrashed()->whereIn('issue_id', $allIssueIds)->where('deleted_at', '>', $since)->pluck('id')->values(), 'issue_comments' => IssueComment::onlyTrashed()->whereIn('issue_id', $allIssueIds)->where('deleted_at', '>', $since)->pluck('id')->values(), ]; } @@ -209,7 +210,7 @@ class ProjectApiController extends Controller 'id' => $t->id, 'project_id' => $t->project_id, 'phase_id' => $t->phase_id, 'name' => $t->name, 'description' => $t->description, 'fields' => $t->fields, 'version' => $t->updated_at?->timestamp, - 'hash' => md5(json_encode($t->fields) . $t->name), + 'hash' => md5(json_encode($t->fields).$t->name), 'updated_at' => $t->updated_at?->toIso8601String(), ]; } @@ -217,10 +218,10 @@ class ProjectApiController extends Controller private function mapMedia(Media $m): array { $entity = [ - Project::class => 'project', - Feature::class => 'feature', - Issue::class => 'issue', - IssueTask::class => 'issue_task', + Project::class => 'project', + Feature::class => 'feature', + Issue::class => 'issue', + IssueTask::class => 'issue_task', IssueComment::class => 'issue_comment', ][$m->mediable_type] ?? class_basename($m->mediable_type); @@ -232,4 +233,3 @@ class ProjectApiController extends Controller ]; } } - diff --git a/app/Http/Controllers/Api/V1/SyncController.php b/app/Http/Controllers/Api/V1/SyncController.php index ea4a011..0781a2b 100644 --- a/app/Http/Controllers/Api/V1/SyncController.php +++ b/app/Http/Controllers/Api/V1/SyncController.php @@ -30,12 +30,12 @@ class SyncController extends Controller public function sync(Request $request) { $data = $request->validate([ - 'operations' => ['required', 'array'], - 'operations.*.entity' => ['required', 'string'], - 'operations.*.op' => ['required', 'string'], - 'operations.*.uuid' => ['required', 'uuid'], - 'operations.*.data' => ['required', 'array'], - 'operations.*.client_updated_at' => ['nullable', 'date'], + 'operations' => ['required', 'array'], + 'operations.*.entity' => ['required', 'string'], + 'operations.*.op' => ['required', 'string'], + 'operations.*.uuid' => ['required', 'uuid'], + 'operations.*.data' => ['required', 'array'], + 'operations.*.client_updated_at' => ['nullable', 'date'], ]); $user = $request->user(); @@ -74,16 +74,16 @@ class SyncController extends Controller } try { - $result = match ($op['entity'] . '.' . $op['op']) { + $result = match ($op['entity'].'.'.$op['op']) { 'progress_update.create' => $this->progressUpdateCreate($user, $uuid, $op), - 'inspection.create' => $this->inspectionCreate($user, $uuid, $op), - 'issue.create' => $this->issueCreate($user, $uuid, $op), - 'issue.update' => $this->issueUpdate($user, $uuid, $op), - 'issue_task.create' => $this->issueTaskCreate($user, $uuid, $op), - 'issue_task.update' => $this->issueTaskUpdate($user, $uuid, $op), - 'issue_comment.create' => $this->issueCommentCreate($user, $uuid, $op), - 'feature.update' => $this->featureUpdate($user, $uuid, $op), - default => $this->error($uuid, 'unsupported entity/op: ' . $op['entity'] . '.' . $op['op']), + 'inspection.create' => $this->inspectionCreate($user, $uuid, $op), + 'issue.create' => $this->issueCreate($user, $uuid, $op), + 'issue.update' => $this->issueUpdate($user, $uuid, $op), + 'issue_task.create' => $this->issueTaskCreate($user, $uuid, $op), + 'issue_task.update' => $this->issueTaskUpdate($user, $uuid, $op), + 'issue_comment.create' => $this->issueCommentCreate($user, $uuid, $op), + 'feature.update' => $this->featureUpdate($user, $uuid, $op), + default => $this->error($uuid, 'unsupported entity/op: '.$op['entity'].'.'.$op['op']), }; } catch (\Throwable $e) { $result = $this->error($uuid, $e->getMessage()); @@ -92,11 +92,11 @@ class SyncController extends Controller // Record only terminal successes so conflicts/errors can be safely retried. if ($result['status'] === 'applied') { SyncLog::create([ - 'user_id' => $user->id, - 'op_uuid' => $uuid, - 'entity' => $op['entity'], - 'op' => $op['op'], - 'status' => 'applied', + 'user_id' => $user->id, + 'op_uuid' => $uuid, + 'entity' => $op['entity'], + 'op' => $op['op'], + 'status' => 'applied', 'server_id' => $result['server_id'] ?? null, ]); } @@ -115,11 +115,11 @@ class SyncController extends Controller $v = Validator::make($op['data'], [ 'phase_id' => ['required', 'integer', 'exists:phases,id'], 'progress' => ['required', 'integer', 'min:0', 'max:100'], - 'comment' => ['nullable', 'string'], + 'comment' => ['nullable', 'string'], 'location' => ['nullable', 'array'], ]); if ($v->fails()) { - return $this->error($uuid, 'validation: ' . $v->errors()->first()); + return $this->error($uuid, 'validation: '.$v->errors()->first()); } $d = $v->validated(); @@ -129,12 +129,12 @@ class SyncController extends Controller } $pu = ProgressUpdate::create([ - 'uuid' => $uuid, - 'phase_id' => $phase->id, - 'user_id' => $user->id, - 'progress_percent' => $d['progress'], - 'comment' => $d['comment'] ?? null, - 'location' => $d['location'] ?? null, + 'uuid' => $uuid, + 'phase_id' => $phase->id, + 'user_id' => $user->id, + 'progress_percent' => $d['progress'], + 'comment' => $d['comment'] ?? null, + 'location' => $d['location'] ?? null, 'client_updated_at' => $op['client_updated_at'] ?? null, ]); @@ -153,15 +153,15 @@ class SyncController extends Controller } $v = Validator::make($op['data'], [ - 'feature_id' => ['required', 'integer', 'exists:features,id'], + 'feature_id' => ['required', 'integer', 'exists:features,id'], 'template_id' => ['nullable', 'integer', 'exists:inspection_templates,id'], - 'data' => ['nullable', 'array'], - 'status' => ['nullable', 'string'], - 'result' => ['nullable', 'string'], - 'notes' => ['nullable', 'string'], + 'data' => ['nullable', 'array'], + 'status' => ['nullable', 'string'], + 'result' => ['nullable', 'string'], + 'notes' => ['nullable', 'string'], ]); if ($v->fails()) { - return $this->error($uuid, 'validation: ' . $v->errors()->first()); + return $this->error($uuid, 'validation: '.$v->errors()->first()); } $d = $v->validated(); @@ -172,16 +172,16 @@ class SyncController extends Controller } $inspection = Inspection::create([ - 'uuid' => $uuid, - 'project_id' => $project->id, - 'layer_id' => $feature->layer_id, - 'feature_id' => $feature->id, - 'template_id' => $d['template_id'] ?? null, - 'user_id' => $user->id, - 'data' => $d['data'] ?? [], - 'status' => $d['status'] ?? 'completed', - 'result' => $d['result'] ?? null, - 'notes' => $d['notes'] ?? null, + 'uuid' => $uuid, + 'project_id' => $project->id, + 'layer_id' => $feature->layer_id, + 'feature_id' => $feature->id, + 'template_id' => $d['template_id'] ?? null, + 'user_id' => $user->id, + 'data' => $d['data'] ?? [], + 'status' => $d['status'] ?? 'completed', + 'result' => $d['result'] ?? null, + 'notes' => $d['notes'] ?? null, 'client_updated_at' => $op['client_updated_at'] ?? null, ]); @@ -197,16 +197,16 @@ class SyncController extends Controller } $v = Validator::make($op['data'], [ - 'project_id' => ['required', 'integer', 'exists:projects,id'], - 'feature_id' => ['nullable', 'integer', 'exists:features,id'], - 'title' => ['required', 'string', 'max:255'], + 'project_id' => ['required', 'integer', 'exists:projects,id'], + 'feature_id' => ['nullable', 'integer', 'exists:features,id'], + 'title' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], - 'priority' => ['nullable', 'in:' . implode(',', Issue::PRIORITIES)], - 'status' => ['nullable', 'in:' . implode(',', Issue::STATUSES)], - 'type' => ['nullable', 'in:' . implode(',', Issue::TYPES)], + 'priority' => ['nullable', 'in:'.implode(',', Issue::PRIORITIES)], + 'status' => ['nullable', 'in:'.implode(',', Issue::STATUSES)], + 'type' => ['nullable', 'in:'.implode(',', Issue::TYPES)], ]); if ($v->fails()) { - return $this->error($uuid, 'validation: ' . $v->errors()->first()); + return $this->error($uuid, 'validation: '.$v->errors()->first()); } $d = $v->validated(); @@ -216,15 +216,15 @@ class SyncController extends Controller } $issue = Issue::create([ - 'uuid' => $uuid, - 'project_id' => $project->id, - 'feature_id' => $d['feature_id'] ?? null, - 'title' => $d['title'], - 'description' => $d['description'] ?? null, - 'priority' => $d['priority'] ?? 'medium', - 'status' => $d['status'] ?? 'open', - 'type' => $d['type'] ?? 'other', - 'reported_by' => $user->id, + 'uuid' => $uuid, + 'project_id' => $project->id, + 'feature_id' => $d['feature_id'] ?? null, + 'title' => $d['title'], + 'description' => $d['description'] ?? null, + 'priority' => $d['priority'] ?? 'medium', + 'status' => $d['status'] ?? 'open', + 'type' => $d['type'] ?? 'other', + 'reported_by' => $user->id, 'client_updated_at' => $op['client_updated_at'] ?? null, ]); @@ -234,17 +234,17 @@ class SyncController extends Controller private function issueUpdate(User $user, string $uuid, array $op): array { $v = Validator::make($op['data'], [ - 'id' => ['required', 'integer', 'exists:issues,id'], - 'title' => ['nullable', 'string', 'max:255'], - 'description' => ['nullable', 'string'], - 'priority' => ['nullable', 'in:' . implode(',', Issue::PRIORITIES)], - 'status' => ['nullable', 'in:' . implode(',', Issue::STATUSES)], - 'type' => ['nullable', 'in:' . implode(',', Issue::TYPES)], - 'assigned_to' => ['nullable', 'integer', 'exists:users,id'], + 'id' => ['required', 'integer', 'exists:issues,id'], + 'title' => ['nullable', 'string', 'max:255'], + 'description' => ['nullable', 'string'], + 'priority' => ['nullable', 'in:'.implode(',', Issue::PRIORITIES)], + 'status' => ['nullable', 'in:'.implode(',', Issue::STATUSES)], + 'type' => ['nullable', 'in:'.implode(',', Issue::TYPES)], + 'assigned_to' => ['nullable', 'integer', 'exists:users,id'], 'resolution_notes' => ['nullable', 'string'], ]); if ($v->fails()) { - return $this->error($uuid, 'validation: ' . $v->errors()->first()); + return $this->error($uuid, 'validation: '.$v->errors()->first()); } $d = $v->validated(); @@ -276,14 +276,14 @@ class SyncController extends Controller } $v = Validator::make($op['data'], [ - 'issue_id' => ['required', 'integer', 'exists:issues,id'], - 'title' => ['required', 'string', 'max:255'], + 'issue_id' => ['required', 'integer', 'exists:issues,id'], + 'title' => ['required', 'string', 'max:255'], 'assigned_to' => ['nullable', 'integer', 'exists:users,id'], - 'due_date' => ['nullable', 'date'], - 'is_done' => ['nullable', 'boolean'], + 'due_date' => ['nullable', 'date'], + 'is_done' => ['nullable', 'boolean'], ]); if ($v->fails()) { - return $this->error($uuid, 'validation: ' . $v->errors()->first()); + return $this->error($uuid, 'validation: '.$v->errors()->first()); } $d = $v->validated(); @@ -294,15 +294,15 @@ class SyncController extends Controller $done = $d['is_done'] ?? false; $task = IssueTask::create([ - 'uuid' => $uuid, - 'issue_id' => $issue->id, - 'title' => $d['title'], - 'assigned_to' => $d['assigned_to'] ?? null, - 'due_date' => $d['due_date'] ?? null, - 'is_done' => $done, - 'done_at' => $done ? now() : null, - 'done_by' => $done ? $user->id : null, - 'order' => ((int) $issue->tasks()->max('order')) + 1, + 'uuid' => $uuid, + 'issue_id' => $issue->id, + 'title' => $d['title'], + 'assigned_to' => $d['assigned_to'] ?? null, + 'due_date' => $d['due_date'] ?? null, + 'is_done' => $done, + 'done_at' => $done ? now() : null, + 'done_by' => $done ? $user->id : null, + 'order' => ((int) $issue->tasks()->max('order')) + 1, 'client_updated_at' => $op['client_updated_at'] ?? null, ]); @@ -312,14 +312,14 @@ class SyncController extends Controller private function issueTaskUpdate(User $user, string $uuid, array $op): array { $v = Validator::make($op['data'], [ - 'id' => ['required', 'integer', 'exists:issue_tasks,id'], - 'title' => ['nullable', 'string', 'max:255'], + 'id' => ['required', 'integer', 'exists:issue_tasks,id'], + 'title' => ['nullable', 'string', 'max:255'], 'assigned_to' => ['nullable', 'integer', 'exists:users,id'], - 'due_date' => ['nullable', 'date'], - 'is_done' => ['nullable', 'boolean'], + 'due_date' => ['nullable', 'date'], + 'is_done' => ['nullable', 'boolean'], ]); if ($v->fails()) { - return $this->error($uuid, 'validation: ' . $v->errors()->first()); + return $this->error($uuid, 'validation: '.$v->errors()->first()); } $d = $v->validated(); @@ -354,10 +354,10 @@ class SyncController extends Controller $v = Validator::make($op['data'], [ 'issue_id' => ['required', 'integer', 'exists:issues,id'], - 'body' => ['required', 'string', 'max:5000'], + 'body' => ['required', 'string', 'max:5000'], ]); if ($v->fails()) { - return $this->error($uuid, 'validation: ' . $v->errors()->first()); + return $this->error($uuid, 'validation: '.$v->errors()->first()); } $d = $v->validated(); @@ -367,10 +367,10 @@ class SyncController extends Controller } $comment = IssueComment::create([ - 'uuid' => $uuid, - 'issue_id' => $issue->id, - 'user_id' => $user->id, - 'body' => $d['body'], + 'uuid' => $uuid, + 'issue_id' => $issue->id, + 'user_id' => $user->id, + 'body' => $d['body'], 'client_updated_at' => $op['client_updated_at'] ?? null, ]); @@ -382,15 +382,15 @@ class SyncController extends Controller private function featureUpdate(User $user, string $uuid, array $op): array { $v = Validator::make($op['data'], [ - 'id' => ['required', 'integer', 'exists:features,id'], - 'status' => ['nullable', 'string'], - 'progress' => ['nullable', 'integer', 'min:0', 'max:100'], - 'responsible' => ['nullable', 'string'], - 'is_active' => ['nullable', 'boolean'], + 'id' => ['required', 'integer', 'exists:features,id'], + 'status' => ['nullable', 'string'], + 'progress' => ['nullable', 'integer', 'min:0', 'max:100'], + 'responsible' => ['nullable', 'string'], + 'is_active' => ['nullable', 'boolean'], 'feature_type_id' => ['nullable', 'integer', 'exists:feature_types,id'], ]); if ($v->fails()) { - return $this->error($uuid, 'validation: ' . $v->errors()->first()); + return $this->error($uuid, 'validation: '.$v->errors()->first()); } $d = $v->validated(); @@ -424,6 +424,7 @@ class SyncController extends Controller if (! $project) { return false; } + return $user->can('manage all') || $project->users()->where('user_id', $user->id)->exists(); } @@ -440,11 +441,12 @@ class SyncController extends Controller $clientAt = Carbon::parse($op['client_updated_at']); if ($model->updated_at && $model->updated_at->gt($clientAt)) { return [ - 'uuid' => $uuid, + 'uuid' => $uuid, 'status' => 'conflict', 'server' => $model->fresh()->toArray(), ]; } + return null; } diff --git a/app/Http/Controllers/FeaturesController.php b/app/Http/Controllers/FeaturesController.php deleted file mode 100644 index 3856c59..0000000 --- a/app/Http/Controllers/FeaturesController.php +++ /dev/null @@ -1,65 +0,0 @@ -validate([ 'name' => 'required|string|max:255', - 'email' => 'required|email|max:255|unique:users,email,' . $user->id, + 'email' => 'required|email|max:255|unique:users,email,'.$user->id, ]); $user->update($validated); @@ -52,4 +50,4 @@ class ProfileController extends Controller return redirect('/'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 5cb882e..2009d85 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers; use App\Models\Project; -use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; @@ -16,6 +15,7 @@ class ProjectController extends Controller public function index() { Gate::authorize('view projects'); + return view('projects.index'); } @@ -37,6 +37,7 @@ class ProjectController extends Controller // Assign creator as supervisor in project $project->users()->attach(Auth::id(), ['role_in_project' => 'supervisor']); + return redirect()->route('projects.map', $project)->with('success', 'Proyecto creado'); } @@ -94,4 +95,4 @@ class ProjectController extends Controller // (lo validaremos dentro del componente Livewire) return view('projects.map', compact('project')); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/ProjectReportController.php b/app/Http/Controllers/ProjectReportController.php index 5f23411..d8a210d 100644 --- a/app/Http/Controllers/ProjectReportController.php +++ b/app/Http/Controllers/ProjectReportController.php @@ -1,8 +1,10 @@ can('manage all') && !$project->users()->where('user_id', $user->id)->exists()) { + if (! $user->can('manage all') && ! $project->users()->where('user_id', $user->id)->exists()) { abort(403); } @@ -20,11 +22,11 @@ class ProjectReportController extends Controller ->get(); $stats = [ - 'total_features' => $phases->flatMap(fn($p) => $p->layers)->flatMap(fn($l) => $l->features)->count(), - 'completed_features' => $phases->flatMap(fn($p) => $p->layers)->flatMap(fn($l) => $l->features)->where('status', 'completed')->count(), - 'total_inspections' => \App\Models\Inspection::where('project_id', $project->id)->count(), - 'open_issues' => \App\Models\Issue::where('project_id', $project->id)->where('status', 'open')->count(), - 'avg_progress' => round($phases->avg('progress_percent') ?? 0), + 'total_features' => $phases->flatMap(fn ($p) => $p->layers)->flatMap(fn ($l) => $l->features)->count(), + 'completed_features' => $phases->flatMap(fn ($p) => $p->layers)->flatMap(fn ($l) => $l->features)->where('status', 'completed')->count(), + 'total_inspections' => Inspection::where('project_id', $project->id)->count(), + 'open_issues' => Issue::where('project_id', $project->id)->where('status', 'open')->count(), + 'avg_progress' => round($phases->avg('progress_percent') ?? 0), ]; $pdf_data = compact('project', 'phases', 'stats'); diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 95a07a3..c03b03c 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -3,12 +3,12 @@ namespace App\Http\Controllers; use App\DTO\ReportFilters; +use App\Exports\ProjectReportExport; use App\Models\Project; use App\Services\ReportGenerator; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Maatwebsite\Excel\Facades\Excel; -use App\Exports\ProjectReportExport; class ReportController extends Controller { @@ -19,7 +19,7 @@ class ReportController extends Controller { $this->authorizeProjectAccess($project); - return \Livewire\Livewire::mount(\App\Livewire\Reports\ReportBuilder::class, [ + return view('reports.builder', [ 'project' => $project, ]); } @@ -65,8 +65,8 @@ class ReportController extends Controller protected function downloadExcel(Project $project, ReportFilters $filters, array $data) { $export = new ProjectReportExport($project, $filters, $data); - $filename = 'informe_' . $project->name . '_' . now()->format('Ymd_His') . '.xlsx'; - + $filename = 'informe_'.$project->name.'_'.now()->format('Ymd_His').'.xlsx'; + return Excel::download($export, $filename); } @@ -76,13 +76,13 @@ class ReportController extends Controller protected function authorizeProjectAccess(Project $project): void { $user = Auth::user(); - + if ($user->can('manage all')) { return; } - if (!$project->users()->where('user_id', $user->id)->exists()) { + if (! $project->users()->where('user_id', $user->id)->exists()) { abort(403, 'No tienes acceso a este proyecto.'); } } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Reports/ExportController.php b/app/Http/Controllers/Reports/ExportController.php index 8f66457..db1f97c 100644 --- a/app/Http/Controllers/Reports/ExportController.php +++ b/app/Http/Controllers/Reports/ExportController.php @@ -2,15 +2,12 @@ namespace App\Http\Controllers\Reports; -use App\Http\Controllers\Controller; -use App\Models\Project; -use App\Models\Phase; -use App\Models\Inspection; -use Maatwebsite\Excel\Facades\Excel; -use App\Exports\ProjectsExport; -use App\Exports\PhasesExport; use App\Exports\InspectionsExport; +use App\Exports\PhasesExport; +use App\Exports\ProjectsExport; +use App\Http\Controllers\Controller; use Illuminate\Http\Request; +use Maatwebsite\Excel\Facades\Excel; class ExportController extends Controller { diff --git a/app/Http/Middleware/BindProjectModel.php b/app/Http/Middleware/BindProjectModel.php index 1efef77..966a7ec 100644 --- a/app/Http/Middleware/BindProjectModel.php +++ b/app/Http/Middleware/BindProjectModel.php @@ -2,9 +2,9 @@ namespace App\Http\Middleware; +use App\Models\Project; use Closure; use Illuminate\Http\Request; -use App\Models\Project; class BindProjectModel { @@ -18,6 +18,7 @@ class BindProjectModel $route->setParameter('project', $project); } } + return $next($request); } -} \ No newline at end of file +} diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php index 3c4547e..9505fb5 100644 --- a/app/Http/Middleware/SetLocale.php +++ b/app/Http/Middleware/SetLocale.php @@ -27,7 +27,7 @@ class SetLocale } // 2. From session - if (!$locale && Session::has('locale')) { + if (! $locale && Session::has('locale')) { $sessionLocale = Session::get('locale'); if (in_array($sessionLocale, $allowedLocales)) { $locale = $sessionLocale; @@ -35,7 +35,7 @@ class SetLocale } // 3. From browser Accept-Language - if (!$locale) { + if (! $locale) { $browserLang = substr($request->server('HTTP_ACCEPT_LANGUAGE', 'en'), 0, 2); if (in_array($browserLang, $allowedLocales)) { $locale = $browserLang; @@ -43,7 +43,7 @@ class SetLocale } // 4. Default to app locale - if (!$locale) { + if (! $locale) { $locale = config('app.locale', 'en'); } @@ -52,4 +52,4 @@ class SetLocale return $next($request); } -} \ No newline at end of file +} diff --git a/app/Jobs/CaptureDailyProgressSnapshot.php b/app/Jobs/CaptureDailyProgressSnapshot.php index a635f7b..e07e06d 100644 --- a/app/Jobs/CaptureDailyProgressSnapshot.php +++ b/app/Jobs/CaptureDailyProgressSnapshot.php @@ -22,7 +22,7 @@ class CaptureDailyProgressSnapshot implements ShouldQueue public function handle(): void { $date = $this->snapshotDate ? Carbon::parse($this->snapshotDate) : Carbon::today(); - + Log::info('Starting daily progress snapshot capture', ['date' => $date->toDateString()]); $captured = 0; @@ -45,7 +45,7 @@ class CaptureDailyProgressSnapshot implements ShouldQueue private function capturePhaseSnapshots(Carbon $date): int { $count = 0; - + Phase::with(['project'])->chunkById(100, function ($phases) use ($date, &$count) { foreach ($phases as $phase) { // Skip if snapshot already exists for this date @@ -87,7 +87,7 @@ class CaptureDailyProgressSnapshot implements ShouldQueue private function captureFeatureSnapshots(Carbon $date): int { $count = 0; - + Feature::with(['layer.phase.project'])->chunkById(200, function ($features) use ($date, &$count) { foreach ($features as $feature) { if (ProgressSnapshot::where('trackable_type', Feature::class) @@ -131,7 +131,7 @@ class CaptureDailyProgressSnapshot implements ShouldQueue private function captureTaskSnapshots(Carbon $date): int { $count = 0; - + Task::with(['project', 'phase'])->chunkById(200, function ($tasks) use ($date, &$count) { foreach ($tasks as $task) { if (ProgressSnapshot::where('trackable_type', Task::class) @@ -171,4 +171,4 @@ class CaptureDailyProgressSnapshot implements ShouldQueue return $count; } -} \ No newline at end of file +} diff --git a/app/Livewire/Admin/RoleForm.php b/app/Livewire/Admin/RoleForm.php index 43c5649..e0254a4 100644 --- a/app/Livewire/Admin/RoleForm.php +++ b/app/Livewire/Admin/RoleForm.php @@ -2,9 +2,9 @@ namespace App\Livewire\Admin; -use Livewire\Component; -use Livewire\Attributes\Layout; use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Layout; +use Livewire\Component; use Spatie\Permission\Models\Role; use Spatie\Permission\PermissionRegistrar; @@ -14,9 +14,11 @@ class RoleForm extends Component public ?Role $role = null; public string $name = ''; + public string $description = ''; private const PROTECTED_ROLES = ['Admin']; + private const CORE_PERMISSION = 'manage all'; public function mount(?Role $role = null): void @@ -24,8 +26,8 @@ class RoleForm extends Component abort_unless(Auth::user()?->can('manage roles'), 403); if ($role && $role->exists) { - $this->role = $role; - $this->name = $role->name; + $this->role = $role; + $this->name = $role->name; $this->description = $role->description ?? ''; } } @@ -33,7 +35,7 @@ class RoleForm extends Component public function save() { $this->validate([ - 'name' => 'required|string|max:50|unique:roles,name' . ($this->role ? ',' . $this->role->id : ''), + 'name' => 'required|string|max:50|unique:roles,name'.($this->role ? ','.$this->role->id : ''), 'description' => 'nullable|string|max:255', ], [], ['name' => 'nombre', 'description' => 'descripción']); @@ -46,7 +48,7 @@ class RoleForm extends Component $this->role->save(); } else { Role::create([ - 'name' => $this->name, + 'name' => $this->name, 'description' => $this->description ?: null, ]); } diff --git a/app/Livewire/Admin/RolePermissionManager.php b/app/Livewire/Admin/RolePermissionManager.php index 109432f..055575c 100644 --- a/app/Livewire/Admin/RolePermissionManager.php +++ b/app/Livewire/Admin/RolePermissionManager.php @@ -2,21 +2,23 @@ namespace App\Livewire\Admin; -use Livewire\Component; -use Livewire\Attributes\Layout; use Illuminate\Support\Facades\Auth; -use Spatie\Permission\Models\Role; +use Livewire\Attributes\Layout; +use Livewire\Component; use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; use Spatie\Permission\PermissionRegistrar; #[Layout('layouts.app')] class RolePermissionManager extends Component { public string $newRole = ''; + public string $newPermission = ''; /** Roles that must not be deleted or stripped of core powers. */ private const PROTECTED_ROLES = ['Admin']; + private const CORE_PERMISSION = 'manage all'; public function mount(): void @@ -36,7 +38,8 @@ class RolePermissionManager extends Component if ($role->hasPermissionTo($permissionName)) { // Admin must always keep the core permission if ($role->name === 'Admin' && $permissionName === self::CORE_PERMISSION) { - $this->dispatch('notify', "El rol Admin no puede perder '" . self::CORE_PERMISSION . "'."); + $this->dispatch('notify', "El rol Admin no puede perder '".self::CORE_PERMISSION."'."); + return; } $role->revokePermissionTo($permissionName); @@ -66,6 +69,7 @@ class RolePermissionManager extends Component if (in_array($role->name, self::PROTECTED_ROLES, true)) { $this->dispatch('notify', "El rol '{$role->name}' está protegido y no se puede borrar."); + return; } @@ -91,7 +95,8 @@ class RolePermissionManager extends Component $permission = Permission::findOrFail($permissionId); if ($permission->name === self::CORE_PERMISSION) { - $this->dispatch('notify', "El permiso '" . self::CORE_PERMISSION . "' está protegido y no se puede borrar."); + $this->dispatch('notify', "El permiso '".self::CORE_PERMISSION."' está protegido y no se puede borrar."); + return; } @@ -103,7 +108,7 @@ class RolePermissionManager extends Component public function render() { return view('livewire.roles.role-permission-manager', [ - 'roles' => Role::with('permissions')->orderBy('name')->get(), + 'roles' => Role::with('permissions')->orderBy('name')->get(), 'permissions' => Permission::orderBy('name')->get(), ]); } diff --git a/app/Livewire/Admin/RoleTable.php b/app/Livewire/Admin/RoleTable.php index 7e1e443..be6da0b 100644 --- a/app/Livewire/Admin/RoleTable.php +++ b/app/Livewire/Admin/RoleTable.php @@ -2,9 +2,9 @@ namespace App\Livewire\Admin; +use Illuminate\Database\Eloquent\Builder; use Rappasoft\LaravelLivewireTables\DataTableComponent; use Rappasoft\LaravelLivewireTables\Views\Column; -use Illuminate\Database\Eloquent\Builder; use Spatie\Permission\Models\Role; use Spatie\Permission\PermissionRegistrar; @@ -17,8 +17,8 @@ class RoleTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('name', 'asc') - ->setSortingPillsEnabled(false); + ->setDefaultSort('name', 'asc') + ->setSortingPillsEnabled(false); } public function builder(): Builder @@ -30,48 +30,48 @@ class RoleTable extends DataTableComponent { return [ Column::make(__('Name'), 'name') - ->sortable() - ->searchable() - ->format(fn ($value, $row) => - ''.e($value).'' - . (in_array($row->name, self::PROTECTED_ROLES, true) ? ' protegido' : '') - ) - ->html(), + ->sortable() + ->searchable() + ->format(fn ($value, $row) => ''.e($value).'' + .(in_array($row->name, self::PROTECTED_ROLES, true) ? ' protegido' : '') + ) + ->html(), Column::make(__('Description'), 'description') - ->sortable() - ->searchable() - ->format(fn ($value) => $value - ? ''.e($value).'' - : '') - ->html(), + ->sortable() + ->searchable() + ->format(fn ($value) => $value + ? ''.e($value).'' + : '') + ->html(), Column::make(__('Permissions')) - ->label(fn ($row) => ''.(int) $row->permissions_count.'') - ->html(), + ->label(fn ($row) => ''.(int) $row->permissions_count.'') + ->html(), Column::make(__('Users')) - ->label(fn ($row) => ''.(int) $row->users_count.'') - ->html(), + ->label(fn ($row) => ''.(int) $row->users_count.'') + ->html(), Column::make(__('Actions')) - ->label(function ($row) { - $show = route('admin.roles.show', $row->id); - $edit = route('admin.roles.edit', $row->id); - $eye = ''; - $pencil = ''; - $trash = ''; + ->label(function ($row) { + $show = route('admin.roles.show', $row->id); + $edit = route('admin.roles.edit', $row->id); + $eye = ''; + $pencil = ''; + $trash = ''; - $html = '
'; - $html .= ''.$eye.''; - $html .= ''.$pencil.''; - if (! in_array($row->name, self::PROTECTED_ROLES, true)) { - $html .= ''; - } - $html .= '
'; - return $html; - }) - ->html(), + $html = '
'; + $html .= ''.$eye.''; + $html .= ''.$pencil.''; + if (! in_array($row->name, self::PROTECTED_ROLES, true)) { + $html .= ''; + } + $html .= '
'; + + return $html; + }) + ->html(), ]; } @@ -84,7 +84,9 @@ class RoleTable extends DataTableComponent { $roles = Role::whereIn('id', $this->selected)->get(); foreach ($roles as $role) { - if (in_array($role->name, self::PROTECTED_ROLES, true)) continue; + if (in_array($role->name, self::PROTECTED_ROLES, true)) { + continue; + } $role->delete(); } $this->clearSelected(); diff --git a/app/Livewire/Admin/RoleView.php b/app/Livewire/Admin/RoleView.php index 5010288..871fdc3 100644 --- a/app/Livewire/Admin/RoleView.php +++ b/app/Livewire/Admin/RoleView.php @@ -2,23 +2,26 @@ namespace App\Livewire\Admin; -use Livewire\Component; -use Livewire\Attributes\Layout; +use App\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; -use App\Models\User; -use Spatie\Permission\Models\Role; +use Livewire\Attributes\Layout; +use Livewire\Component; use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; use Spatie\Permission\PermissionRegistrar; #[Layout('layouts.app')] class RoleView extends Component { public Role $role; + public string $tab = 'ficha'; // ficha | permisos + public $newUserId = ''; private const PROTECTED_ROLES = ['Admin']; + private const CORE_PERMISSION = 'manage all'; public function mount(Role $role): void @@ -38,7 +41,8 @@ class RoleView extends Component if ($this->role->name === 'Admin' && $permissionName === self::CORE_PERMISSION && $this->role->hasPermissionTo($permissionName)) { - $this->dispatch('notify', "El rol Admin no puede perder '" . self::CORE_PERMISSION . "'."); + $this->dispatch('notify', "El rol Admin no puede perder '".self::CORE_PERMISSION."'."); + return; } @@ -91,6 +95,7 @@ class RoleView extends Component { if (in_array($this->role->name, self::PROTECTED_ROLES, true)) { $this->dispatch('notify', "El rol '{$this->role->name}' está protegido y no se puede borrar."); + return; } $this->role->delete(); @@ -107,6 +112,7 @@ class RoleView extends Component return 'General'; } $resource = Str::afterLast($name, ' '); + return Str::headline($resource ?: 'General'); } @@ -126,6 +132,7 @@ class RoleView extends Component ->groupBy(fn ($perm) => $perm->group ?: $this->sectionFor($perm->name)) ->sortBy(function ($perms, $section) use ($order) { $i = array_search($section, $order, true); + return $i === false ? 999 : $i; }); @@ -133,11 +140,11 @@ class RoleView extends Component ->orderBy('first_name')->orderBy('name')->get(); return view('livewire.roles.role-view', [ - 'users' => $users, + 'users' => $users, 'availableUsers' => $availableUsers, - 'grouped' => $grouped, - 'rolePerms' => $this->role->permissions->pluck('name')->toArray(), - 'isProtected' => in_array($this->role->name, self::PROTECTED_ROLES, true), + 'grouped' => $grouped, + 'rolePerms' => $this->role->permissions->pluck('name')->toArray(), + 'isProtected' => in_array($this->role->name, self::PROTECTED_ROLES, true), ]); } } diff --git a/app/Livewire/Client/ClientProjects.php b/app/Livewire/Client/ClientProjects.php index 1f24371..8789b93 100644 --- a/app/Livewire/Client/ClientProjects.php +++ b/app/Livewire/Client/ClientProjects.php @@ -2,20 +2,20 @@ namespace App\Livewire\Client; -use Livewire\Component; -use App\Models\Project; -use App\Models\Phase; -use App\Models\Inspection; -use App\Models\Feature; use App\Models\ChangeOrder; -use Carbon\Carbon; +use App\Models\Project; +use Livewire\Component; class ClientProjects extends Component { public $projects = []; + public $selectedProject = null; + public $projectDetails = []; + public $galleryImages = []; + public $changeOrders = []; public function mount() @@ -29,7 +29,7 @@ class ClientProjects extends Component $user = auth()->user(); $this->projects = $user->projects() ->wherePivot('role_in_project', 'client') - ->with(['phases' => function($query) { + ->with(['phases' => function ($query) { $query->select('id', 'project_id', 'name', 'progress_percent'); }]) ->get() @@ -44,17 +44,17 @@ class ClientProjects extends Component public function loadProjectDetails() { - if (!$this->selectedProject) { + if (! $this->selectedProject) { return; } $project = Project::with([ 'phases.features', 'inspections.template', - 'changeOrders' // Load change orders for this project + 'changeOrders', // Load change orders for this project ])->find($this->selectedProject); - if (!$project) { + if (! $project) { return; } @@ -75,11 +75,11 @@ class ClientProjects extends Component ->latest() ->take(3) ->get() - ->map(function($media) { + ->map(function ($media) { return [ 'url' => $media->url, 'title' => $media->name, - 'date' => $media->created_at->format('d/m/Y') + 'date' => $media->created_at->format('d/m/Y'), ]; }) ->toArray(); @@ -93,18 +93,18 @@ class ClientProjects extends Component [ 'url' => 'https://via.placeholder.com/400x300?text=Avance+1', 'title' => 'Avance inicial', - 'date' => now()->subDays(30)->format('d/m/Y') + 'date' => now()->subDays(30)->format('d/m/Y'), ], [ 'url' => 'https://via.placeholder.com/400x300?text=Avance+2', 'title' => 'Estructura levantada', - 'date' => now()->subDays(15)->format('d/m/Y') + 'date' => now()->subDays(15)->format('d/m/Y'), ], [ 'url' => 'https://via.placeholder.com/400x300?text=Avance+3', 'title' => 'Instalaciones', - 'date' => now()->subDays(5)->format('d/m/Y') - ] + 'date' => now()->subDays(5)->format('d/m/Y'), + ], ]; } @@ -112,14 +112,14 @@ class ClientProjects extends Component $this->changeOrders = $project->changeOrders ->orderBy('requested_at', 'desc') ->get() - ->map(function($order) { + ->map(function ($order) { return [ 'id' => $order->id, 'title' => $order->title, 'description' => $order->description, 'status' => $order->status, 'requested_at' => $order->requested_at->format('d/m/Y'), - 'amount' => $order->amount + 'amount' => $order->amount, ]; }) ->toArray(); diff --git a/app/Livewire/Common/LanguageSwitcher.php b/app/Livewire/Common/LanguageSwitcher.php index 8e26307..72924b7 100644 --- a/app/Livewire/Common/LanguageSwitcher.php +++ b/app/Livewire/Common/LanguageSwitcher.php @@ -2,10 +2,10 @@ namespace App\Livewire\Common; -use Livewire\Component; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Session; +use Livewire\Component; class LanguageSwitcher extends Component { @@ -26,7 +26,7 @@ class LanguageSwitcher extends Component public function updatedCurrentLocale(string $locale): void { - if (!in_array($locale, ['en', 'es', 'fr', 'ru'])) { + if (! in_array($locale, ['en', 'es', 'fr', 'ru'])) { return; } @@ -51,4 +51,4 @@ class LanguageSwitcher extends Component { return view('livewire.common.language-switcher'); } -} \ No newline at end of file +} diff --git a/app/Livewire/Common/NotificationBell.php b/app/Livewire/Common/NotificationBell.php index a4dd1af..bd9a87e 100644 --- a/app/Livewire/Common/NotificationBell.php +++ b/app/Livewire/Common/NotificationBell.php @@ -2,13 +2,15 @@ namespace App\Livewire\Common; -use Livewire\Component; use Illuminate\Support\Facades\Auth; +use Livewire\Component; class NotificationBell extends Component { public $notifications = []; + public $unreadCount = 0; + public $showDropdown = false; public function mount() diff --git a/app/Livewire/Companies/CompanyForm.php b/app/Livewire/Companies/CompanyForm.php index d9c3970..25ef4c3 100644 --- a/app/Livewire/Companies/CompanyForm.php +++ b/app/Livewire/Companies/CompanyForm.php @@ -2,11 +2,11 @@ namespace App\Livewire\Companies; -use Livewire\Component; -use Livewire\WithFileUploads; -use Livewire\Attributes\Layout; use App\Models\Company; use Illuminate\Support\Facades\Storage; +use Livewire\Attributes\Layout; +use Livewire\Component; +use Livewire\WithFileUploads; #[Layout('layouts.app')] class CompanyForm extends Component @@ -16,50 +16,61 @@ class CompanyForm extends Component public ?Company $company = null; // Form fields - public string $name = ''; - public string $apodo = ''; - public string $tax_id = ''; - public string $estado = 'activo'; - public string $type = 'other'; + public string $name = ''; + + public string $apodo = ''; + + public string $tax_id = ''; + + public string $estado = 'activo'; + + public string $type = 'other'; + public string $address = ''; - public string $phone = ''; - public string $email = ''; + + public string $phone = ''; + + public string $email = ''; + public string $website = ''; - public string $notes = ''; + + public string $notes = ''; + public $logo = null; public function mount(?Company $company = null): void { if ($company && $company->exists) { $this->company = $company; - $this->name = $company->name; - $this->apodo = $company->apodo ?? ''; - $this->tax_id = $company->tax_id ?? ''; - $this->estado = $company->estado ?? 'activo'; - $this->type = $company->type ?? 'other'; + $this->name = $company->name; + $this->apodo = $company->apodo ?? ''; + $this->tax_id = $company->tax_id ?? ''; + $this->estado = $company->estado ?? 'activo'; + $this->type = $company->type ?? 'other'; $this->address = $company->address ?? ''; - $this->phone = $company->phone ?? ''; - $this->email = $company->email ?? ''; + $this->phone = $company->phone ?? ''; + $this->email = $company->email ?? ''; $this->website = $company->website ?? ''; - $this->notes = $company->notes ?? ''; + $this->notes = $company->notes ?? ''; } } protected function rules(): array { $id = $this->company?->id ?? 'NULL'; + return [ - 'name' => 'required|string|max:255', - 'apodo' => 'nullable|string|max:100', - 'tax_id' => "nullable|string|max:50|unique:companies,tax_id,{$id}", - 'estado' => 'required|in:activo,inactivo,suspendido', - 'type' => 'required|in:owner,constructor,subcontractor,consultant,supplier,other', + 'name' => 'required|string|max:255', + 'apodo' => 'nullable|string|max:100', + 'tax_id' => "nullable|string|max:50|unique:companies,tax_id,{$id}", + 'estado' => 'required|in:activo,inactivo,suspendido', + 'type' => 'required|in:owner,constructor,subcontractor,consultant,supplier,other', 'address' => 'nullable|string', - 'phone' => 'nullable|string|max:30', - 'email' => 'nullable|email|max:255', + 'phone' => 'nullable|string|max:30', + 'email' => 'nullable|email|max:255', 'website' => 'nullable|url|max:255', - 'notes' => 'nullable|string', - 'logo' => 'nullable|image|max:2048', + 'notes' => 'nullable|string', + 'logo' => 'nullable|image|max:2048', ]; } @@ -68,16 +79,16 @@ class CompanyForm extends Component $this->validate(); $data = [ - 'name' => $this->name, - 'apodo' => $this->apodo ?: null, - 'tax_id' => $this->tax_id ?: null, - 'estado' => $this->estado, - 'type' => $this->type, + 'name' => $this->name, + 'apodo' => $this->apodo ?: null, + 'tax_id' => $this->tax_id ?: null, + 'estado' => $this->estado, + 'type' => $this->type, 'address' => $this->address ?: null, - 'phone' => $this->phone ?: null, - 'email' => $this->email ?: null, + 'phone' => $this->phone ?: null, + 'email' => $this->email ?: null, 'website' => $this->website ?: null, - 'notes' => $this->notes ?: null, + 'notes' => $this->notes ?: null, ]; if ($this->logo) { diff --git a/app/Livewire/Companies/CompanyManagement.php b/app/Livewire/Companies/CompanyManagement.php index ec44bc7..eecbd49 100644 --- a/app/Livewire/Companies/CompanyManagement.php +++ b/app/Livewire/Companies/CompanyManagement.php @@ -2,29 +2,31 @@ namespace App\Livewire\Companies; -use Livewire\Component; -use Livewire\Attributes\Layout; use App\Models\Company; use Illuminate\Support\Facades\Storage; +use Livewire\Attributes\Layout; +use Livewire\Component; #[Layout('layouts.app')] class CompanyManagement extends Component { - public string $search = ''; - public string $filterType = ''; + public string $search = ''; + + public string $filterType = ''; + public string $filterEstado = ''; public function getCompaniesProperty() { return Company::when($this->search, function ($q) { - $s = '%' . $this->search . '%'; - $q->where(fn($q2) => $q2 - ->where('name', 'like', $s) - ->orWhere('apodo', 'like', $s) - ->orWhere('tax_id', 'like', $s)); - }) - ->when($this->filterType, fn($q) => $q->where('type', $this->filterType)) - ->when($this->filterEstado, fn($q) => $q->where('estado', $this->filterEstado)) + $s = '%'.$this->search.'%'; + $q->where(fn ($q2) => $q2 + ->where('name', 'like', $s) + ->orWhere('apodo', 'like', $s) + ->orWhere('tax_id', 'like', $s)); + }) + ->when($this->filterType, fn ($q) => $q->where('type', $this->filterType)) + ->when($this->filterEstado, fn ($q) => $q->where('estado', $this->filterEstado)) ->withCount('projects') ->orderBy('name') ->get(); @@ -45,7 +47,7 @@ class CompanyManagement extends Component return response()->streamDownload(function () use ($companies) { $handle = fopen('php://output', 'w'); - fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF)); + fprintf($handle, chr(0xEF).chr(0xBB).chr(0xBF)); fputcsv($handle, ['Nombre', 'Apodo', 'NIF/Tax ID', 'Tipo', 'Estado', 'Dirección', 'Teléfono', 'Email', 'Website', 'Proyectos', 'Creación']); foreach ($companies as $c) { fputcsv($handle, [ diff --git a/app/Livewire/Companies/CompanyTable.php b/app/Livewire/Companies/CompanyTable.php index 695583f..0447530 100644 --- a/app/Livewire/Companies/CompanyTable.php +++ b/app/Livewire/Companies/CompanyTable.php @@ -2,13 +2,12 @@ namespace App\Livewire\Companies; +use App\Models\Company; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Storage; use Rappasoft\LaravelLivewireTables\DataTableComponent; use Rappasoft\LaravelLivewireTables\Views\Column; use Rappasoft\LaravelLivewireTables\Views\Filters\SelectFilter; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Storage; -use App\Models\Company; class CompanyTable extends DataTableComponent { @@ -17,17 +16,17 @@ class CompanyTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('name', 'asc') - ->setSortingPillsEnabled(false) - ->setAdditionalSelects([ - 'companies.id as id', - 'companies.apodo as apodo', - 'companies.tax_id as tax_id', - 'companies.phone as phone', - 'companies.email as email', - 'companies.logo_path as logo_path', - 'companies.created_at as created_at', - ]); + ->setDefaultSort('name', 'asc') + ->setSortingPillsEnabled(false) + ->setAdditionalSelects([ + 'companies.id as id', + 'companies.apodo as apodo', + 'companies.tax_id as tax_id', + 'companies.phone as phone', + 'companies.email as email', + 'companies.logo_path as logo_path', + 'companies.created_at as created_at', + ]); } public function builder(): Builder @@ -39,100 +38,108 @@ class CompanyTable extends DataTableComponent { return [ Column::make('Empresa', 'name') - ->sortable() - ->searchable() - ->format(function ($value, $row) { - $logoHtml = ''; - if ($row->logo_path && Storage::disk('public')->exists($row->logo_path)) { - $url = Storage::disk('public')->url($row->logo_path); - $logoHtml = ''; - } else { - $logoHtml = '
+ ->sortable() + ->searchable() + ->format(function ($value, $row) { + $logoHtml = ''; + if ($row->logo_path && Storage::disk('public')->exists($row->logo_path)) { + $url = Storage::disk('public')->url($row->logo_path); + $logoHtml = ''; + } else { + $logoHtml = '
'; - } - $html = '
'.$logoHtml.'
'; - $html .= '

'.e($value).'

'; - if ($row->apodo) $html .= '

'.e($row->apodo).'

'; - if ($row->tax_id) $html .= '

NIF: '.e($row->tax_id).'

'; - $html .= '
'; - return $html; - }) - ->html(), + } + $html = '
'.$logoHtml.'
'; + $html .= '

'.e($value).'

'; + if ($row->apodo) { + $html .= '

'.e($row->apodo).'

'; + } + if ($row->tax_id) { + $html .= '

NIF: '.e($row->tax_id).'

'; + } + $html .= '
'; + + return $html; + }) + ->html(), Column::make('Tipo', 'type') - ->sortable() - ->format(function ($value) { - $map = [ - 'owner' => ['badge-success', 'Promotor'], - 'constructor' => ['badge-primary', 'Constructor'], - 'subcontractor' => ['badge-secondary', 'Subcontratista'], - 'consultant' => ['badge-info', 'Consultor'], - 'supplier' => ['badge-warning', 'Proveedor'], - ]; - [$cls, $label] = $map[$value] ?? ['badge-ghost', 'Otro']; - return ''.$label.''; - }) - ->html(), + ->sortable() + ->format(function ($value) { + $map = [ + 'owner' => ['badge-success', 'Promotor'], + 'constructor' => ['badge-primary', 'Constructor'], + 'subcontractor' => ['badge-secondary', 'Subcontratista'], + 'consultant' => ['badge-info', 'Consultor'], + 'supplier' => ['badge-warning', 'Proveedor'], + ]; + [$cls, $label] = $map[$value] ?? ['badge-ghost', 'Otro']; + + return ''.$label.''; + }) + ->html(), Column::make('Contacto', 'phone') - ->format(function ($value, $row) { - $html = ''; - if ($row->phone) { - $html .= '
+ ->format(function ($value, $row) { + $html = ''; + if ($row->phone) { + $html .= '
'.e($row->phone).'
'; - } - if ($row->email) { - $html .= '
+ } + if ($row->email) { + $html .= '
'.e($row->email).'
'; - } - return $html ?: ''; - }) - ->html(), + } + + return $html ?: ''; + }) + ->html(), Column::make('Estado', 'estado') - ->sortable() - ->format(function ($value) { - $map = [ - 'activo' => ['badge-success', 'Activo'], - 'inactivo' => ['badge-ghost', 'Inactivo'], - 'suspendido' => ['badge-error', 'Suspendido'], - ]; - [$cls, $label] = $map[$value ?? 'activo'] ?? ['badge-ghost', ucfirst($value ?? 'activo')]; - return ''.$label.''; - }) - ->html(), + ->sortable() + ->format(function ($value) { + $map = [ + 'activo' => ['badge-success', 'Activo'], + 'inactivo' => ['badge-ghost', 'Inactivo'], + 'suspendido' => ['badge-error', 'Suspendido'], + ]; + [$cls, $label] = $map[$value ?? 'activo'] ?? ['badge-ghost', ucfirst($value ?? 'activo')]; + + return ''.$label.''; + }) + ->html(), Column::make('Proyectos') - ->label(fn ($row) => - ''.(int)($row->projects_count ?? 0).'' - ) - ->html(), + ->label(fn ($row) => ''.(int) ($row->projects_count ?? 0).'' + ) + ->html(), Column::make('Acciones') - ->label(function ($row) { - $ver = route('companies.show', $row->id); - $editar = route('companies.edit', $row->id); - $name = addslashes($row->name); + ->label(function ($row) { + $ver = route('companies.show', $row->id); + $editar = route('companies.edit', $row->id); + $name = addslashes($row->name); - $html = '
'; - $html .= ' + $html = '
'; + $html .= ' '; - $html .= ' + $html .= ' '; - $html .= ''; - $html .= '
'; - return $html; - }) - ->html(), + $html .= '
'; + + return $html; + }) + ->html(), ]; } @@ -141,21 +148,21 @@ class CompanyTable extends DataTableComponent return [ SelectFilter::make('Tipo', 'type') ->options([ - '' => 'Tipo: todos', - 'owner' => 'Promotor', - 'constructor' => 'Constructor', + '' => 'Tipo: todos', + 'owner' => 'Promotor', + 'constructor' => 'Constructor', 'subcontractor' => 'Subcontratista', - 'consultant' => 'Consultor', - 'supplier' => 'Proveedor', - 'other' => 'Otro', + 'consultant' => 'Consultor', + 'supplier' => 'Proveedor', + 'other' => 'Otro', ]) ->filter(fn (Builder $query, string $value) => $query->where('type', $value)), SelectFilter::make('Estado', 'estado') ->options([ - '' => 'Estado: todos', - 'activo' => 'Activo', - 'inactivo' => 'Inactivo', + '' => 'Estado: todos', + 'activo' => 'Activo', + 'inactivo' => 'Inactivo', 'suspendido' => 'Suspendido', ]) ->filter(fn (Builder $query, string $value) => $query->where('estado', $value)), diff --git a/app/Livewire/Companies/CompanyView.php b/app/Livewire/Companies/CompanyView.php index 51ba887..38d62a4 100644 --- a/app/Livewire/Companies/CompanyView.php +++ b/app/Livewire/Companies/CompanyView.php @@ -2,45 +2,53 @@ namespace App\Livewire\Companies; -use Livewire\Component; -use Livewire\Attributes\Layout; use App\Models\Company; +use App\Models\Issue; use App\Models\Project; use App\Models\User; -use App\Models\Issue; use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Layout; +use Livewire\Component; #[Layout('layouts.app')] class CompanyView extends Component { public Company $company; - public string $activeTab = 'summary'; + + public string $activeTab = 'summary'; // Projects tab - public ?int $addProjectId = null; - public string $addProjectRole = ''; + public ?int $addProjectId = null; + + public string $addProjectRole = ''; + public $availableProjects; // People tab - public ?int $assignUserId = null; + public ?int $assignUserId = null; + public $assignableUsers; // Notes tab - public string $notes = ''; - public bool $editingNotes = false; + public string $notes = ''; + + public bool $editingNotes = false; // Stats (computed once in mount, refreshed on mutations) - public int $usersCount = 0; - public int $projectsCount = 0; - public float $avgProgress = 0.0; - public int $openIssues = 0; + public int $usersCount = 0; + + public int $projectsCount = 0; + + public float $avgProgress = 0.0; + + public int $openIssues = 0; public function mount(Company $company): void { abort_unless(Auth::user()->can('view companies'), 403); $this->company = $company->load(['users.roles', 'projects.phases']); - $this->notes = $company->notes ?? ''; + $this->notes = $company->notes ?? ''; $this->loadAvailableProjects(); $this->loadAssignableUsers(); @@ -60,16 +68,16 @@ class CompanyView extends Component { $this->assignableUsers = User::where(function ($q) { $q->where('company_id', '!=', $this->company->id) - ->orWhereNull('company_id'); + ->orWhereNull('company_id'); })->orderBy('name')->get(); } private function computeStats(): void { - $this->usersCount = $this->company->users->count(); + $this->usersCount = $this->company->users->count(); $this->projectsCount = $this->company->projects->count(); - $this->avgProgress = round( - $this->company->projects->flatMap(fn($p) => $p->phases)->avg('progress_percent') ?? 0 + $this->avgProgress = round( + $this->company->projects->flatMap(fn ($p) => $p->phases)->avg('progress_percent') ?? 0 ); $userIds = $this->company->users->pluck('id'); $this->openIssues = $userIds->isNotEmpty() @@ -89,7 +97,7 @@ class CompanyView extends Component public function assignProject(): void { $this->validate([ - 'addProjectId' => 'required|exists:projects,id', + 'addProjectId' => 'required|exists:projects,id', 'addProjectRole' => 'required|string|max:150', ], [], ['addProjectId' => 'proyecto', 'addProjectRole' => 'rol en proyecto']); @@ -98,7 +106,7 @@ class CompanyView extends Component ]); $this->company->load('projects.phases'); - $this->addProjectId = null; + $this->addProjectId = null; $this->addProjectRole = ''; $this->loadAvailableProjects(); $this->computeStats(); diff --git a/app/Livewire/Inspections/GlobalTemplateManager.php b/app/Livewire/Inspections/GlobalTemplateManager.php index f6db27e..653b631 100644 --- a/app/Livewire/Inspections/GlobalTemplateManager.php +++ b/app/Livewire/Inspections/GlobalTemplateManager.php @@ -18,29 +18,35 @@ class GlobalTemplateManager extends Component public $templates; public $editingTemplate = null; + public $showForm = false; + public $form = [ - 'name' => '', + 'name' => '', 'description' => '', - 'fields' => [], + 'fields' => [], ]; // ── Importar desde CSV/Excel ─────────────────────────────────────────── - public $showImportFileModal = false; - public $importFile = null; - public $importPreviewFields = []; - public $importTemplateName = ''; - public $importError = ''; + public $showImportFileModal = false; + + public $importFile = null; + + public $importPreviewFields = []; + + public $importTemplateName = ''; + + public $importError = ''; public $fieldTypes = [ - 'text' => 'Texto corto', - 'textarea' => 'Texto largo', - 'integer' => 'Número entero', - 'decimal' => 'Número decimal', + 'text' => 'Texto corto', + 'textarea' => 'Texto largo', + 'integer' => 'Número entero', + 'decimal' => 'Número decimal', 'percentage' => 'Porcentaje (0-100)', - 'boolean' => 'Sí/No (checkbox)', - 'date' => 'Fecha', - 'select' => 'Lista desplegable', + 'boolean' => 'Sí/No (checkbox)', + 'date' => 'Fecha', + 'select' => 'Lista desplegable', ]; public function mount() @@ -66,9 +72,9 @@ class GlobalTemplateManager extends Component { $template = InspectionTemplate::findOrFail($id); $this->form = [ - 'name' => $template->name, + 'name' => $template->name, 'description' => $template->description ?? '', - 'fields' => $template->fields ?? [], + 'fields' => $template->fields ?? [], ]; $this->editingTemplate = $id; $this->showForm = true; @@ -83,9 +89,9 @@ class GlobalTemplateManager extends Component public function resetForm() { $this->form = [ - 'name' => '', + 'name' => '', 'description' => '', - 'fields' => [], + 'fields' => [], ]; $this->editingTemplate = null; } @@ -93,17 +99,17 @@ class GlobalTemplateManager extends Component public function addField() { $this->form['fields'][] = [ - 'group' => '', - 'name' => '', - 'label' => '', + 'group' => '', + 'name' => '', + 'label' => '', 'question' => '', - 'type' => 'text', - 'options' => '', + 'type' => 'text', + 'options' => '', 'required' => false, - 'min' => null, - 'max' => null, - 'step' => null, - 'help' => '', + 'min' => null, + 'max' => null, + 'step' => null, + 'help' => '', ]; } @@ -116,15 +122,15 @@ class GlobalTemplateManager extends Component public function saveTemplate() { $this->validate([ - 'form.name' => 'required|string|max:255', + 'form.name' => 'required|string|max:255', 'form.fields' => 'array', ]); $data = [ - 'name' => $this->form['name'], + 'name' => $this->form['name'], 'description' => $this->form['description'], - 'project_id' => null, - 'fields' => array_values($this->form['fields']), + 'project_id' => null, + 'fields' => array_values($this->form['fields']), ]; if ($this->editingTemplate) { @@ -153,10 +159,10 @@ class GlobalTemplateManager extends Component public function openImportFileModal() { - $this->importFile = null; + $this->importFile = null; $this->importPreviewFields = []; - $this->importTemplateName = ''; - $this->importError = ''; + $this->importTemplateName = ''; + $this->importError = ''; $this->showImportFileModal = true; } @@ -164,31 +170,34 @@ class GlobalTemplateManager extends Component { $headers = ['Content-Type' => 'text/csv']; $csv = "\xEF\xBB\xBF" // BOM UTF-8 (para que Excel respete los acentos) - . "group,name,label,question,type,required,options,min,max,step,help\n" - . "Dimensiones,altura,Altura (m),¿Cumple la altura de proyecto?,decimal,1,,0,100,0.1,Medir con flexómetro\n" - . "Dimensiones,material,Material,,select,1,Hormigón|Acero|Madera,,,,\n" - . "Acabados,ok,¿Acabado correcto?,,boolean,1,,,,,\n"; - return response()->streamDownload(fn () => print($csv), 'plantilla_ejemplo.csv', $headers); + ."group,name,label,question,type,required,options,min,max,step,help\n" + ."Dimensiones,altura,Altura (m),¿Cumple la altura de proyecto?,decimal,1,,0,100,0.1,Medir con flexómetro\n" + ."Dimensiones,material,Material,,select,1,Hormigón|Acero|Madera,,,,\n" + ."Acabados,ok,¿Acabado correcto?,,boolean,1,,,,,\n"; + + return response()->streamDownload(fn () => print ($csv), 'plantilla_ejemplo.csv', $headers); } public function parseImportFile() { $this->importError = ''; $this->validate([ - 'importFile' => 'required|file|mimes:csv,txt,xlsx,xls|max:5120', + 'importFile' => 'required|file|mimes:csv,txt,xlsx,xls|max:5120', 'importTemplateName' => 'required|string|max:255', ]); try { $rows = $this->readFileRows(); } catch (\Throwable $e) { - $this->importError = 'No se pudo leer el archivo: ' . $e->getMessage(); + $this->importError = 'No se pudo leer el archivo: '.$e->getMessage(); + return; } $fields = $this->parseRows($rows); if (empty($fields)) { $this->importError = 'No se encontraron filas válidas.'; + return; } $this->importPreviewFields = $fields; @@ -196,19 +205,21 @@ class GlobalTemplateManager extends Component public function confirmImportFile() { - if (empty($this->importPreviewFields) || empty($this->importTemplateName)) return; + if (empty($this->importPreviewFields) || empty($this->importTemplateName)) { + return; + } InspectionTemplate::create([ - 'name' => $this->importTemplateName, + 'name' => $this->importTemplateName, 'description' => 'Importado desde archivo', - 'project_id' => null, - 'fields' => array_values($this->importPreviewFields), + 'project_id' => null, + 'fields' => array_values($this->importPreviewFields), ]); $this->showImportFileModal = false; $this->importPreviewFields = []; - $this->importTemplateName = ''; - $this->importFile = null; + $this->importTemplateName = ''; + $this->importFile = null; $this->loadTemplates(); $this->dispatch('templates-changed'); $this->dispatch('notify', 'Plantilla importada'); @@ -216,7 +227,7 @@ class GlobalTemplateManager extends Component private function readFileRows(): array { - $ext = strtolower($this->importFile->getClientOriginalExtension()); + $ext = strtolower($this->importFile->getClientOriginalExtension()); $path = $this->importFile->getRealPath(); // Fila no vacía = tiene al menos una celda con contenido (no filtramos por @@ -226,21 +237,27 @@ class GlobalTemplateManager extends Component if ($ext === 'xlsx' || $ext === 'xls') { $spreadsheet = IOFactory::load($path); - $sheet = $spreadsheet->getActiveSheet(); - $rows = $sheet->toArray(null, true, true, false); + $sheet = $spreadsheet->getActiveSheet(); + $rows = $sheet->toArray(null, true, true, false); array_shift($rows); + return array_values(array_filter($rows, $notEmpty)); } - $rows = []; + $rows = []; $handle = fopen($path, 'r'); $bom = fread($handle, 3); - if ($bom !== "\xEF\xBB\xBF") rewind($handle); + if ($bom !== "\xEF\xBB\xBF") { + rewind($handle); + } fgetcsv($handle); while (($row = fgetcsv($handle)) !== false) { - if ($notEmpty($row)) $rows[] = $row; + if ($notEmpty($row)) { + $rows[] = $row; + } } fclose($handle); + return $rows; } @@ -250,24 +267,27 @@ class GlobalTemplateManager extends Component // group, name, label, question, type, required, options, min, max, step, help $fields = []; foreach ($rows as $row) { - $row = array_values((array) $row); + $row = array_values((array) $row); $rawName = trim($row[1] ?? ''); - if ($rawName === '') continue; + if ($rawName === '') { + continue; + } $fields[] = [ - 'group' => trim($row[0] ?? ''), - 'name' => $this->slugify($rawName), - 'label' => trim($row[2] ?? '') ?: $rawName, + 'group' => trim($row[0] ?? ''), + 'name' => $this->slugify($rawName), + 'label' => trim($row[2] ?? '') ?: $rawName, 'question' => trim($row[3] ?? ''), - 'type' => $this->normalizeType($row[4] ?? 'text'), + 'type' => $this->normalizeType($row[4] ?? 'text'), 'required' => in_array(strtolower(trim($row[5] ?? '0')), ['1', 'si', 'sí', 'yes', 'true']), - 'options' => trim($row[6] ?? ''), - 'min' => ($row[7] ?? '') !== '' ? $row[7] : null, - 'max' => ($row[8] ?? '') !== '' ? $row[8] : null, - 'step' => ($row[9] ?? '') !== '' ? $row[9] : null, - 'help' => trim($row[10] ?? ''), + 'options' => trim($row[6] ?? ''), + 'min' => ($row[7] ?? '') !== '' ? $row[7] : null, + 'max' => ($row[8] ?? '') !== '' ? $row[8] : null, + 'step' => ($row[9] ?? '') !== '' ? $row[9] : null, + 'help' => trim($row[10] ?? ''), ]; } + return $fields; } @@ -276,6 +296,7 @@ class GlobalTemplateManager extends Component $str = mb_strtolower(trim($str)); $str = preg_replace('/\s+/', '_', $str); $str = preg_replace('/[^a-z0-9_]/i', '', $str); + return trim($str, '_') ?: 'campo'; } @@ -291,6 +312,7 @@ class GlobalTemplateManager extends Component 'date' => 'date', 'fecha' => 'date', 'select' => 'select', 'lista' => 'select', 'dropdown' => 'select', 'opciones' => 'select', ]; + return $map[strtolower(trim($type))] ?? 'text'; } diff --git a/app/Livewire/Inspections/InspectionTemplatesTable.php b/app/Livewire/Inspections/InspectionTemplatesTable.php index 6dadec3..e0f387f 100644 --- a/app/Livewire/Inspections/InspectionTemplatesTable.php +++ b/app/Livewire/Inspections/InspectionTemplatesTable.php @@ -18,13 +18,13 @@ class InspectionTemplatesTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('inspection_templates.name', 'asc') - ->setSortingPillsEnabled(false) - ->setSecondaryHeaderEnabled() - ->setAdditionalSelects([ - 'inspection_templates.id as id', - 'inspection_templates.fields as fields', - ]); + ->setDefaultSort('inspection_templates.name', 'asc') + ->setSortingPillsEnabled(false) + ->setSecondaryHeaderEnabled() + ->setAdditionalSelects([ + 'inspection_templates.id as id', + 'inspection_templates.fields as fields', + ]); } /** Refrescar cuando el manager crea/edita/borra. */ @@ -46,47 +46,46 @@ class InspectionTemplatesTable extends DataTableComponent { return [ Column::make('Plantilla', 'name') - ->sortable()->searchable() - ->secondaryHeaderFilter('name') - ->format(fn ($value) => '' . e($value) . '') - ->html(), + ->sortable()->searchable() + ->secondaryHeaderFilter('name') + ->format(fn ($value) => ''.e($value).'') + ->html(), Column::make('Descripción', 'description') - ->searchable() - ->secondaryHeaderFilter('description') - ->format(fn ($value) => $value - ? '' . e($value) . '' - : '') - ->html(), + ->searchable() + ->secondaryHeaderFilter('description') + ->format(fn ($value) => $value + ? ''.e($value).'' + : '') + ->html(), Column::make('Campos') - ->label(fn ($row) => - '' . count($row->fields ?? []) . '') - ->html(), + ->label(fn ($row) => ''.count($row->fields ?? []).'') + ->html(), Column::make('Proyectos') - ->secondaryHeaderFilter('usage') - ->label(function ($row) { - $n = (int) ($row->projects_count ?? 0); - $cls = $n > 0 ? 'badge-info' : 'badge-ghost'; - return '' . $n . ''; - }) - ->html(), + ->secondaryHeaderFilter('usage') + ->label(function ($row) { + $n = (int) ($row->projects_count ?? 0); + $cls = $n > 0 ? 'badge-info' : 'badge-ghost'; + + return ''.$n.''; + }) + ->html(), Column::make('Acciones') - ->label(fn ($row) => - '
- -
') - ->html(), + ->html(), ]; } @@ -95,17 +94,21 @@ class InspectionTemplatesTable extends DataTableComponent return [ TextFilter::make('Plantilla', 'name') ->config(['placeholder' => 'Buscar nombre…']) - ->filter(fn (Builder $q, string $v) => $q->where('inspection_templates.name', 'like', '%' . $v . '%')), + ->filter(fn (Builder $q, string $v) => $q->where('inspection_templates.name', 'like', '%'.$v.'%')), TextFilter::make('Descripción', 'description') ->config(['placeholder' => 'Buscar descripción…']) - ->filter(fn (Builder $q, string $v) => $q->where('inspection_templates.description', 'like', '%' . $v . '%')), + ->filter(fn (Builder $q, string $v) => $q->where('inspection_templates.description', 'like', '%'.$v.'%')), SelectFilter::make('Uso', 'usage') ->options(['' => 'Todos', 'used' => 'En uso (≥1 proyecto)', 'unused' => 'Sin uso']) ->filter(function (Builder $q, string $v) { - if ($v === 'used') $q->has('projects'); - if ($v === 'unused') $q->doesntHave('projects'); + if ($v === 'used') { + $q->has('projects'); + } + if ($v === 'unused') { + $q->doesntHave('projects'); + } }), ]; } diff --git a/app/Livewire/Issues/IssueChecklistManager.php b/app/Livewire/Issues/IssueChecklistManager.php index abb337e..4876e47 100644 --- a/app/Livewire/Issues/IssueChecklistManager.php +++ b/app/Livewire/Issues/IssueChecklistManager.php @@ -12,11 +12,15 @@ use Livewire\Component; class IssueChecklistManager extends Component { public Project $project; + public $templates = []; public bool $showForm = false; + public $editingId = null; + public string $name = ''; + public array $items = ['']; public function mount(Project $project) @@ -29,6 +33,7 @@ class IssueChecklistManager extends Component private function canAccessProject(): bool { $user = Auth::user(); + return $user->can('manage all') || $this->project->users()->where('user_id', $user->id)->exists(); } @@ -74,14 +79,15 @@ class IssueChecklistManager extends Component public function save(): void { $this->validate([ - 'name' => 'required|string|max:255', - 'items' => 'required|array', + 'name' => 'required|string|max:255', + 'items' => 'required|array', 'items.*' => 'nullable|string|max:255', ]); $items = array_values(array_filter(array_map('trim', $this->items), fn ($v) => $v !== '')); if (empty($items)) { $this->addError('items', 'Añade al menos una tarea.'); + return; } diff --git a/app/Livewire/Issues/IssueDetail.php b/app/Livewire/Issues/IssueDetail.php index 295d75b..935d3f4 100644 --- a/app/Livewire/Issues/IssueDetail.php +++ b/app/Livewire/Issues/IssueDetail.php @@ -4,13 +4,13 @@ namespace App\Livewire\Issues; use App\Models\Issue; use App\Models\IssueChecklistTemplate; -use App\Models\IssueComment; -use App\Models\IssueTask; +use App\Models\Media; use App\Models\Project; use App\Notifications\IssueCommentedNotification; use App\Notifications\IssueStatusChangedNotification; use App\Notifications\IssueTaskAssignedNotification; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Livewire\Attributes\Layout; use Livewire\Component; @@ -22,19 +22,24 @@ class IssueDetail extends Component use WithFileUploads; public Project $project; + public Issue $issue; // New task form public string $newTaskTitle = ''; + public $newTaskAssignee = ''; + public $newTaskDue = ''; // Checklist templates public $checklistTemplates = []; + public $applyTemplateId = ''; // New comment form public string $newComment = ''; + public $commentPhoto = null; // single optional photo on a comment // Issue-level photos @@ -61,6 +66,7 @@ class IssueDetail extends Component private function canAccessProject(): bool { $user = Auth::user(); + return $user->can('manage all') || $this->project->users()->where('user_id', $user->id)->exists(); } @@ -97,17 +103,17 @@ class IssueDetail extends Component { abort_unless($this->canEdit(), 403); $this->validate([ - 'newTaskTitle' => 'required|string|max:255', + 'newTaskTitle' => 'required|string|max:255', 'newTaskAssignee' => 'nullable|exists:users,id', - 'newTaskDue' => 'nullable|date', + 'newTaskDue' => 'nullable|date', ]); $task = $this->issue->tasks()->create([ - 'title' => $this->newTaskTitle, + 'title' => $this->newTaskTitle, 'assigned_to' => $this->newTaskAssignee ?: null, - 'due_date' => $this->newTaskDue ?: null, - 'order' => ((int) $this->issue->tasks()->max('order')) + 1, - 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'due_date' => $this->newTaskDue ?: null, + 'order' => ((int) $this->issue->tasks()->max('order')) + 1, + 'uuid' => (string) Str::uuid(), ]); // Notify the assignee (unless they assigned it to themselves). @@ -134,7 +140,7 @@ class IssueDetail extends Component $this->issue->tasks()->create([ 'title' => $title, 'order' => ++$order, - 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'uuid' => (string) Str::uuid(), ]); } @@ -170,7 +176,7 @@ class IssueDetail extends Component { // Anyone who can view the issue (and is a member) may comment / report progress. $this->validate([ - 'newComment' => 'nullable|string|max:5000', + 'newComment' => 'nullable|string|max:5000', 'commentPhoto' => 'nullable|image|max:20480', // 20 MB ]); @@ -182,8 +188,8 @@ class IssueDetail extends Component $comment = $this->issue->comments()->create([ 'user_id' => Auth::id(), - 'body' => trim($this->newComment) ?: '(foto)', - 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'body' => trim($this->newComment) ?: '(foto)', + 'uuid' => (string) Str::uuid(), ]); if ($this->commentPhoto) { @@ -215,7 +221,7 @@ class IssueDetail extends Component public function deleteMedia($mediaId): void { - $media = \App\Models\Media::findOrFail($mediaId); + $media = Media::findOrFail($mediaId); $user = Auth::user(); abort_unless($user->can('delete media') || $media->uploaded_by === $user->id, 403); $media->delete(); @@ -230,14 +236,14 @@ class IssueDetail extends Component $path = $file->store("uploads/issues/{$this->issue->id}/{$entity}", 'public'); $parent->media()->create([ - 'name' => $file->getClientOriginalName(), - 'file_path' => $path, - 'file_type' => $mime, + 'name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'file_type' => $mime, 'file_extension' => $file->getClientOriginalExtension(), - 'file_size' => $file->getSize(), - 'category' => str_starts_with($mime, 'image/') ? 'image' : 'document', - 'uploaded_by' => Auth::id(), - 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'file_size' => $file->getSize(), + 'category' => str_starts_with($mime, 'image/') ? 'image' : 'document', + 'uploaded_by' => Auth::id(), + 'uuid' => (string) Str::uuid(), ]); } @@ -256,8 +262,8 @@ class IssueDetail extends Component { abort_unless($this->canEdit(), 403); $this->issue->update([ - 'status' => 'resolved', - 'resolved_at' => $this->issue->resolved_at ?? now(), + 'status' => 'resolved', + 'resolved_at' => $this->issue->resolved_at ?? now(), 'resolution_notes' => $this->resolutionNotes ?: null, ]); $this->notifyStakeholders(new IssueStatusChangedNotification($this->issue, 'resolved')); @@ -269,7 +275,7 @@ class IssueDetail extends Component { abort_unless($this->canEdit(), 403); $this->issue->update([ - 'status' => 'closed', + 'status' => 'closed', 'resolved_at' => $this->issue->resolved_at ?? now(), ]); $this->notifyStakeholders(new IssueStatusChangedNotification($this->issue, 'closed')); diff --git a/app/Livewire/Issues/IssueForm.php b/app/Livewire/Issues/IssueForm.php index ef88866..11a44d7 100644 --- a/app/Livewire/Issues/IssueForm.php +++ b/app/Livewire/Issues/IssueForm.php @@ -2,6 +2,7 @@ namespace App\Livewire\Issues; +use App\Models\Feature; use App\Models\Issue; use App\Models\Project; use App\Notifications\IssueAssignedNotification; @@ -14,22 +15,31 @@ use Livewire\Component; class IssueForm extends Component { public Project $project; + public ?Issue $issue = null; // null = create, set = edit public $projectUsers = []; // Form fields public $title = ''; + public $description = ''; + public $status = 'open'; + public $priority = 'medium'; + public $type = 'defect'; + public $assignedTo = ''; + public $resolutionNotes = ''; // Optional context (e.g. when reporting from a map feature) public $featureId = null; + public $inspectionId = null; + public $featureName = null; // shown when the issue is pre-linked to a map element public function mount(Project $project, ?Issue $issue = null) @@ -44,22 +54,22 @@ class IssueForm extends Component $this->projectUsers = $project->users()->orderBy('name')->get(); if ($issue) { - $this->issue = $issue; - $this->title = $issue->title; - $this->description = $issue->description ?? ''; - $this->status = $issue->status; - $this->priority = $issue->priority; - $this->type = $issue->type ?? 'defect'; - $this->assignedTo = $issue->assigned_to ?? ''; + $this->issue = $issue; + $this->title = $issue->title; + $this->description = $issue->description ?? ''; + $this->status = $issue->status; + $this->priority = $issue->priority; + $this->type = $issue->type ?? 'defect'; + $this->assignedTo = $issue->assigned_to ?? ''; $this->resolutionNotes = $issue->resolution_notes ?? ''; - $this->featureId = $issue->feature_id; - $this->inspectionId = $issue->inspection_id; - $this->featureName = $issue->feature?->name; + $this->featureId = $issue->feature_id; + $this->inspectionId = $issue->inspection_id; + $this->featureName = $issue->feature?->name; } elseif ($featureId = request()->integer('feature')) { // Pre-link to a map element when reporting from the project map. - $feature = \App\Models\Feature::with('layer.phase')->find($featureId); + $feature = Feature::with('layer.phase')->find($featureId); if ($feature && $feature->layer?->phase?->project_id === $project->id) { - $this->featureId = $feature->id; + $this->featureId = $feature->id; $this->featureName = $feature->name; } } @@ -68,6 +78,7 @@ class IssueForm extends Component private function canAccessProject(): bool { $user = Auth::user(); + return $user->can('manage all') || $this->project->users()->where('user_id', $user->id)->exists(); } @@ -75,12 +86,12 @@ class IssueForm extends Component protected function rules(): array { return [ - 'title' => 'required|string|max:255', - 'description' => 'nullable|string', - 'status' => 'required|in:' . implode(',', Issue::STATUSES), - 'priority' => 'required|in:' . implode(',', Issue::PRIORITIES), - 'type' => 'required|in:' . implode(',', Issue::TYPES), - 'assignedTo' => 'nullable|exists:users,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'required|in:'.implode(',', Issue::STATUSES), + 'priority' => 'required|in:'.implode(',', Issue::PRIORITIES), + 'type' => 'required|in:'.implode(',', Issue::TYPES), + 'assignedTo' => 'nullable|exists:users,id', 'resolutionNotes' => 'nullable|string', ]; } @@ -91,14 +102,14 @@ class IssueForm extends Component $this->validate(); $payload = [ - 'title' => $this->title, - 'description' => $this->description, - 'status' => $this->status, - 'priority' => $this->priority, - 'type' => $this->type, - 'feature_id' => $this->featureId, - 'inspection_id' => $this->inspectionId, - 'assigned_to' => $this->assignedTo ?: null, + 'title' => $this->title, + 'description' => $this->description, + 'status' => $this->status, + 'priority' => $this->priority, + 'type' => $this->type, + 'feature_id' => $this->featureId, + 'inspection_id' => $this->inspectionId, + 'assigned_to' => $this->assignedTo ?: null, 'resolution_notes' => $this->resolutionNotes ?: null, ]; @@ -120,7 +131,7 @@ class IssueForm extends Component } } else { $issue = Issue::create(array_merge($payload, [ - 'project_id' => $this->project->id, + 'project_id' => $this->project->id, 'reported_by' => Auth::id(), ])); diff --git a/app/Livewire/Issues/IssueManager.php b/app/Livewire/Issues/IssueManager.php index 9d60694..31a14fa 100644 --- a/app/Livewire/Issues/IssueManager.php +++ b/app/Livewire/Issues/IssueManager.php @@ -2,12 +2,12 @@ namespace App\Livewire\Issues; -use Livewire\Component; +use App\Models\Issue; +use App\Models\Project; +use Illuminate\Support\Facades\Auth; use Livewire\Attributes\Layout; use Livewire\Attributes\On; -use Illuminate\Support\Facades\Auth; -use App\Models\Project; -use App\Models\Issue; +use Livewire\Component; #[Layout('layouts.app')] class IssueManager extends Component @@ -24,6 +24,7 @@ class IssueManager extends Component private function canAccessProject(): bool { $user = Auth::user(); + return $user->can('manage all') || $this->project->users()->where('user_id', $user->id)->exists(); } @@ -44,11 +45,11 @@ class IssueManager extends Component ->pluck('c', 'status'); return view('livewire.issues.issue-manager', [ - 'countOpen' => (int) ($counts['open'] ?? 0), + 'countOpen' => (int) ($counts['open'] ?? 0), 'countInReview' => (int) ($counts['in_review'] ?? 0), 'countResolved' => (int) ($counts['resolved'] ?? 0), - 'countClosed' => (int) ($counts['closed'] ?? 0), - 'countTotal' => (int) $counts->sum(), + 'countClosed' => (int) ($counts['closed'] ?? 0), + 'countTotal' => (int) $counts->sum(), ]); } } diff --git a/app/Livewire/Issues/IssueTable.php b/app/Livewire/Issues/IssueTable.php index 1706fe2..51faa1a 100644 --- a/app/Livewire/Issues/IssueTable.php +++ b/app/Livewire/Issues/IssueTable.php @@ -3,6 +3,7 @@ namespace App\Livewire\Issues; use App\Models\Issue; +use App\Models\Project; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; @@ -19,9 +20,9 @@ class IssueTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('created_at', 'desc') - ->setSortingPillsEnabled(false) - ->setAdditionalSelects(['issues.id as id', 'issues.created_at as created_at']); + ->setDefaultSort('created_at', 'desc') + ->setSortingPillsEnabled(false) + ->setAdditionalSelects(['issues.id as id', 'issues.created_at as created_at']); } public function builder(): Builder @@ -31,7 +32,7 @@ class IssueTable extends DataTableComponent abort_unless( $user->can('view issues') && ( $user->can('manage all') - || \App\Models\Project::whereKey($this->projectId)->whereHas('users', fn ($q) => $q->where('user_id', $user->id))->exists() + || Project::whereKey($this->projectId)->whereHas('users', fn ($q) => $q->where('user_id', $user->id))->exists() ), 403 ); @@ -53,119 +54,129 @@ class IssueTable extends DataTableComponent { return [ Column::make('Prioridad', 'priority') - ->sortable() - ->format(function ($value, $row) { - $label = ['low' => 'Bajo', 'medium' => 'Medio', 'high' => 'Alto', 'critical' => 'Crítico'][$value] ?? ucfirst($value); - $textColor = in_array($value, ['critical', 'high']) ? '#fff' : '#1f2937'; - return ''.$label.''; - }) - ->html(), + ->sortable() + ->format(function ($value, $row) { + $label = ['low' => 'Bajo', 'medium' => 'Medio', 'high' => 'Alto', 'critical' => 'Crítico'][$value] ?? ucfirst($value); + $textColor = in_array($value, ['critical', 'high']) ? '#fff' : '#1f2937'; + + return ''.$label.''; + }) + ->html(), Column::make('Título', 'title') - ->sortable() - ->searchable() - ->format(function ($value, $row) { - $url = route('projects.issues.show', [$this->projectId, $row->id]); - $html = ''.e($value).''; - if ($row->description) { - $html .= '
'.e(Str::limit($row->description, 60)).'
'; - } - $meta = []; - if ($row->reporter) $meta[] = 'Reportado por '.e($row->reporter->name); - if ($row->comments_count) $meta[] = '💬 '.$row->comments_count; - if ($row->media_count) $meta[] = '📷 '.$row->media_count; - if ($meta) { - $html .= '
'.implode(' · ', $meta).'
'; - } - if ($row->tasks_count) { - $pct = (int) round($row->tasks_done_count / $row->tasks_count * 100); - $html .= '
+ ->sortable() + ->searchable() + ->format(function ($value, $row) { + $url = route('projects.issues.show', [$this->projectId, $row->id]); + $html = ''.e($value).''; + if ($row->description) { + $html .= '
'.e(Str::limit($row->description, 60)).'
'; + } + $meta = []; + if ($row->reporter) { + $meta[] = 'Reportado por '.e($row->reporter->name); + } + if ($row->comments_count) { + $meta[] = '💬 '.$row->comments_count; + } + if ($row->media_count) { + $meta[] = '📷 '.$row->media_count; + } + if ($meta) { + $html .= '
'.implode(' · ', $meta).'
'; + } + if ($row->tasks_count) { + $pct = (int) round($row->tasks_done_count / $row->tasks_count * 100); + $html .= '
'.$row->tasks_done_count.'/'.$row->tasks_count.' tareas
'; - } - if ($row->overdue_tasks_count) { - $html .= '
⏰ '.$row->overdue_tasks_count.' vencida'.($row->overdue_tasks_count > 1 ? 's' : '').'
'; - } - return $html; - }) - ->html(), + } + if ($row->overdue_tasks_count) { + $html .= '
⏰ '.$row->overdue_tasks_count.' vencida'.($row->overdue_tasks_count > 1 ? 's' : '').'
'; + } + + return $html; + }) + ->html(), Column::make('Tipo', 'type') - ->sortable() - ->format(fn ($value, $row) => - ''.e($row->type_label).'') - ->html(), + ->sortable() + ->format(fn ($value, $row) => ''.e($row->type_label).'') + ->html(), Column::make('Feature') - ->label(fn ($row) => $row->feature - ? ''.e($row->feature->name).'' - : '') - ->html(), + ->label(fn ($row) => $row->feature + ? ''.e($row->feature->name).'' + : '') + ->html(), Column::make('Estado', 'status') - ->sortable() - ->format(function ($value, $row) { - $label = ['open' => 'Abierto', 'in_review' => 'En revisión', 'resolved' => 'Resuelto', 'closed' => 'Cerrado'][$value] ?? ucfirst($value); - return ''.$label.''; - }) - ->html(), + ->sortable() + ->format(function ($value, $row) { + $label = ['open' => 'Abierto', 'in_review' => 'En revisión', 'resolved' => 'Resuelto', 'closed' => 'Cerrado'][$value] ?? ucfirst($value); + + return ''.$label.''; + }) + ->html(), Column::make('Asignado a') - ->label(fn ($row) => $row->assignee - ? ''.e($row->assignee->name).'' - : 'Sin asignar') - ->html(), + ->label(fn ($row) => $row->assignee + ? ''.e($row->assignee->name).'' + : 'Sin asignar') + ->html(), Column::make('Fecha', 'created_at') - ->sortable() - ->format(function ($value, $row) { - $html = $row->created_at->format('d/m/Y'); - if ($row->resolved_at) { - $html .= '
Res. '.$row->resolved_at->format('d/m/Y').'
'; - } - return $html; - }) - ->html(), + ->sortable() + ->format(function ($value, $row) { + $html = $row->created_at->format('d/m/Y'); + if ($row->resolved_at) { + $html .= '
Res. '.$row->resolved_at->format('d/m/Y').'
'; + } + + return $html; + }) + ->html(), Column::make('Acciones') - ->label(function ($row) { - $user = Auth::user(); - $detail = route('projects.issues.show', [$this->projectId, $row->id]); - $edit = route('projects.issues.edit', [$this->projectId, $row->id]); + ->label(function ($row) { + $user = Auth::user(); + $detail = route('projects.issues.show', [$this->projectId, $row->id]); + $edit = route('projects.issues.edit', [$this->projectId, $row->id]); - $html = '
'; - $html .= ' + $html = '
'; + $html .= ' '; - if ($user->can('edit issues')) { - $html .= ' + if ($user->can('edit issues')) { + $html .= ' '; - if (in_array($row->status, ['open', 'in_review'])) { - $html .= ''; - } - if ($row->status !== 'closed') { - $html .= ''; - } - } + } + } - if ($user->can('delete issues')) { - $html .= ''; - } + } - $html .= '
'; - return $html; - }) - ->html(), + $html .= '
'; + + return $html; + }) + ->html(), ]; } @@ -174,26 +185,26 @@ class IssueTable extends DataTableComponent return [ SelectFilter::make('Estado', 'status') ->options([ - '' => 'Estado: todos', - 'open' => 'Abierto', + '' => 'Estado: todos', + 'open' => 'Abierto', 'in_review' => 'En revisión', - 'resolved' => 'Resuelto', - 'closed' => 'Cerrado', + 'resolved' => 'Resuelto', + 'closed' => 'Cerrado', ]) ->filter(fn (Builder $query, string $value) => $query->where('issues.status', $value)), SelectFilter::make('Prioridad', 'priority') ->options([ - '' => 'Prioridad: todas', + '' => 'Prioridad: todas', 'critical' => 'Crítica', - 'high' => 'Alta', - 'medium' => 'Media', - 'low' => 'Baja', + 'high' => 'Alta', + 'medium' => 'Media', + 'low' => 'Baja', ]) ->filter(fn (Builder $query, string $value) => $query->where('issues.priority', $value)), SelectFilter::make('Tipo', 'type') - ->options(['' => 'Tipo: todos'] + \App\Models\Issue::typeLabels()) + ->options(['' => 'Tipo: todos'] + Issue::typeLabels()) ->filter(fn (Builder $query, string $value) => $query->where('issues.type', $value)), ]; } diff --git a/app/Livewire/Layers/LayerManager.php b/app/Livewire/Layers/LayerManager.php index 29920f0..ae070d9 100644 --- a/app/Livewire/Layers/LayerManager.php +++ b/app/Livewire/Layers/LayerManager.php @@ -2,19 +2,19 @@ namespace App\Livewire\Layers; -use Livewire\Component; -use Livewire\WithFileUploads; -use Livewire\Attributes\Layout; -use Livewire\Attributes\On; -use App\Models\Project; -use App\Models\Phase; -use App\Models\Layer; use App\Models\Feature; use App\Models\InspectionTemplate; +use App\Models\Layer; +use App\Models\Phase; +use App\Models\Project; use App\Services\SpatialFileConverter; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; +use Livewire\Attributes\Layout; +use Livewire\Attributes\On; +use Livewire\Component; +use Livewire\WithFileUploads; #[Layout('layouts.app')] class LayerManager extends Component @@ -22,29 +22,39 @@ class LayerManager extends Component use WithFileUploads; public Project $project; - public Phase $phase; + + public Phase $phase; + public $layers; + public $selectedLayer = null; + public $visibleLayers = []; - public $uploadFile = null; - public $layerName = ''; - public $layerColor = '#3b82f6'; + public $uploadFile = null; + + public $layerName = ''; + + public $layerColor = '#3b82f6'; // Batch assign - public $templates = []; + public $templates = []; + public $batchTemplateId = null; - public $batchStatus = ''; + + public $batchStatus = ''; public function mount(Project $project, Phase $phase) { $this->project = $project; - $this->phase = $phase; + $this->phase = $phase; - if ($this->phase->project_id !== $this->project->id) abort(404); + if ($this->phase->project_id !== $this->project->id) { + abort(404); + } $user = Auth::user(); - if (!$user->can('manage all') && !$project->users()->where('user_id', $user->id)->exists()) { + if (! $user->can('manage all') && ! $project->users()->where('user_id', $user->id)->exists()) { abort(403); } @@ -71,28 +81,28 @@ class LayerManager extends Component private function buildLayerPayload(Layer $layer): array { - $color = $layer->color ?: '#3b82f6'; + $color = $layer->color ?: '#3b82f6'; $features = ($layer->relationLoaded('features') ? $layer->features : $layer->features()->get()) - ->map(fn($f) => [ - 'type' => 'Feature', - 'id' => $f->id, - 'geometry' => $f->geometry, + ->map(fn ($f) => [ + 'type' => 'Feature', + 'id' => $f->id, + 'geometry' => $f->geometry, 'properties' => [ - 'name' => $f->name ?? 'Elemento', - 'progress' => $f->progress, - 'status' => $f->status ?? 'planned', + 'name' => $f->name ?? 'Elemento', + 'progress' => $f->progress, + 'status' => $f->status ?? 'planned', 'responsible' => $f->responsible, 'template_id' => $f->template_id, ], ])->values()->toArray(); return [ - 'id' => $layer->id, - 'color' => $color, + 'id' => $layer->id, + 'color' => $color, 'geojson' => [ - 'type' => 'FeatureCollection', + 'type' => 'FeatureCollection', 'features' => $features, - 'style' => ['color' => $color], + 'style' => ['color' => $color], ], ]; } @@ -101,8 +111,8 @@ class LayerManager extends Component { $this->layers->loadMissing('features'); $this->dispatch('initialLayersData', [ - 'layers' => $this->layers->map(fn($l) => $this->buildLayerPayload($l)), - 'visibleLayers' => $this->visibleLayers, + 'layers' => $this->layers->map(fn ($l) => $this->buildLayerPayload($l)), + 'visibleLayers' => $this->visibleLayers, 'selectedLayerId' => $this->selectedLayer?->id, ]); } @@ -113,6 +123,7 @@ class LayerManager extends Component { if ($this->selectedLayer && $this->selectedLayer->id == $layerId) { $this->dispatch('notify', 'No puedes ocultar la capa que estás editando'); + return; } if (in_array($layerId, $this->visibleLayers)) { @@ -128,9 +139,11 @@ class LayerManager extends Component public function selectLayer($layerId) { $this->selectedLayer = Layer::with('features')->find($layerId); - if (!$this->selectedLayer) return; + if (! $this->selectedLayer) { + return; + } - if (!in_array($layerId, $this->visibleLayers)) { + if (! in_array($layerId, $this->visibleLayers)) { $this->visibleLayers[] = $layerId; $this->dispatch('visibilityChanged', $this->visibleLayers); } @@ -139,9 +152,9 @@ class LayerManager extends Component $this->dispatch('layerSelectedForEdit', [ 'layerId' => $layerId, 'geojson' => $payload['geojson'], - 'color' => $payload['color'], + 'color' => $payload['color'], ]); - $this->dispatch('notify', 'Editando: ' . $this->selectedLayer->name); + $this->dispatch('notify', 'Editando: '.$this->selectedLayer->name); } // ── Import file ─────────────────────────────────────────────────────────── @@ -149,32 +162,35 @@ class LayerManager extends Component public function importFile() { $user = Auth::user(); - if (!$user->can('upload layers')) { + if (! $user->can('upload layers')) { $this->dispatch('notify', 'Sin permisos para subir capas'); + return; } $this->validate([ 'uploadFile' => 'required|file|max:51200', - 'layerName' => 'required|string|max:255', + 'layerName' => 'required|string|max:255', 'layerColor' => 'nullable|string|size:7', ]); - $ext = strtolower($this->uploadFile->getClientOriginalExtension()); + $ext = strtolower($this->uploadFile->getClientOriginalExtension()); $allowed = ['geojson', 'json', 'kmz', 'kml', 'shp', 'dwg', 'zip']; - if (!in_array($ext, $allowed)) { - $this->dispatch('notify', 'Extensión no permitida. Válidas: ' . implode(', ', $allowed)); + if (! in_array($ext, $allowed)) { + $this->dispatch('notify', 'Extensión no permitida. Válidas: '.implode(', ', $allowed)); + return; } $geojson = SpatialFileConverter::convertToGeoJson($this->uploadFile); - if (!$geojson) { + if (! $geojson) { $this->dispatch('notify', 'No se pudo convertir el archivo. Comprueba que sea GeoJSON, KML o Shapefile válido.'); + return; } $layerColor = $this->layerColor ?: '#3b82f6'; - $layerName = $this->layerName; + $layerName = $this->layerName; try { DB::transaction(function () use ($geojson, $layerColor, $layerName, $user) { @@ -183,28 +199,30 @@ class LayerManager extends Component ); $layer = Layer::create([ - 'project_id' => $this->project->id, - 'phase_id' => $this->phase->id, - 'name' => $layerName, - 'color' => $layerColor, + 'project_id' => $this->project->id, + 'phase_id' => $this->phase->id, + 'name' => $layerName, + 'color' => $layerColor, 'original_file' => $path, - 'uploaded_by' => $user->id, + 'uploaded_by' => $user->id, ]); $idx = 0; foreach ($geojson['features'] ?? [] as $fd) { $idx++; $name = trim($fd['properties']['name'] ?? ''); - if ($name === '') $name = $layerName . ' — Elemento ' . $idx; + if ($name === '') { + $name = $layerName.' — Elemento '.$idx; + } Feature::create([ - 'layer_id' => $layer->id, - 'name' => $name, - 'geometry' => $fd['geometry'], - 'properties' => $fd['properties'] ?? [], + 'layer_id' => $layer->id, + 'name' => $name, + 'geometry' => $fd['geometry'], + 'properties' => $fd['properties'] ?? [], 'template_id' => $fd['properties']['template_id'] ?? null, - 'progress' => $fd['properties']['progress'] ?? 0, - 'status' => in_array($fd['properties']['status'] ?? '', Feature::STATUSES) + 'progress' => $fd['properties']['progress'] ?? 0, + 'status' => in_array($fd['properties']['status'] ?? '', Feature::STATUSES) ? $fd['properties']['status'] : 'planned', 'responsible' => $fd['properties']['responsible'] ?? null, @@ -214,7 +232,8 @@ class LayerManager extends Component $this->visibleLayers[] = $layer->id; }); } catch (\Throwable $e) { - $this->dispatch('notify', 'Error al importar: ' . $e->getMessage()); + $this->dispatch('notify', 'Error al importar: '.$e->getMessage()); + return; } @@ -229,18 +248,19 @@ class LayerManager extends Component public function createEmptyLayer() { $user = Auth::user(); - if (!$user->can('upload layers')) { + if (! $user->can('upload layers')) { $this->dispatch('notify', 'Sin permisos para crear capas'); + return; } $layer = Layer::create([ - 'project_id' => $this->project->id, - 'phase_id' => $this->phase->id, - 'name' => $this->layerName ?: 'Nueva capa', - 'color' => $this->layerColor ?: '#3b82f6', + 'project_id' => $this->project->id, + 'phase_id' => $this->phase->id, + 'name' => $this->layerName ?: 'Nueva capa', + 'color' => $this->layerColor ?: '#3b82f6', 'original_file' => null, - 'uploaded_by' => $user->id, + 'uploaded_by' => $user->id, ]); $this->loadLayers(); @@ -255,18 +275,20 @@ class LayerManager extends Component #[On('save-manual-geojson')] public function saveManualGeojson($geojsonString) { - if (!$this->selectedLayer) { + if (! $this->selectedLayer) { $this->dispatch('notify', 'No hay capa seleccionada'); + return; } $geojson = json_decode($geojsonString, true); - if (json_last_error() !== JSON_ERROR_NONE || !isset($geojson['features'])) { + if (json_last_error() !== JSON_ERROR_NONE || ! isset($geojson['features'])) { $this->dispatch('notify', 'GeoJSON inválido'); + return; } - $layerId = $this->selectedLayer->id; + $layerId = $this->selectedLayer->id; $layerName = $this->selectedLayer->name; try { @@ -278,16 +300,18 @@ class LayerManager extends Component foreach ($geojson['features'] as $fd) { $idx++; $name = trim($fd['properties']['name'] ?? ''); - if ($name === '') $name = $layerName . ' — Elemento ' . $idx; + if ($name === '') { + $name = $layerName.' — Elemento '.$idx; + } Feature::create([ - 'layer_id' => $layerId, - 'name' => $name, - 'geometry' => $fd['geometry'], - 'properties' => $fd['properties'] ?? [], + 'layer_id' => $layerId, + 'name' => $name, + 'geometry' => $fd['geometry'], + 'properties' => $fd['properties'] ?? [], 'template_id' => $fd['properties']['template_id'] ?? null, - 'progress' => $fd['properties']['progress'] ?? 0, - 'status' => in_array($fd['properties']['status'] ?? '', Feature::STATUSES) + 'progress' => $fd['properties']['progress'] ?? 0, + 'status' => in_array($fd['properties']['status'] ?? '', Feature::STATUSES) ? $fd['properties']['status'] : 'planned', 'responsible' => $fd['properties']['responsible'] ?? null, @@ -295,14 +319,15 @@ class LayerManager extends Component } }); } catch (\Throwable $e) { - $this->dispatch('notify', 'Error al guardar: ' . $e->getMessage()); + $this->dispatch('notify', 'Error al guardar: '.$e->getMessage()); + return; } $this->loadLayers(); $this->selectLayer($this->selectedLayer->id); $this->emitInitialLayersData(); - $this->dispatch('notify', count($geojson['features']) . ' elementos guardados'); + $this->dispatch('notify', count($geojson['features']).' elementos guardados'); } // ── Delete layer ────────────────────────────────────────────────────────── @@ -310,13 +335,19 @@ class LayerManager extends Component public function deleteLayer($layerId) { $user = Auth::user(); - if (!$user->can('delete layers')) abort(403); + if (! $user->can('delete layers')) { + abort(403); + } // Verify it belongs to this phase (prevents cross-project deletion) $layer = Layer::where('id', $layerId)->where('phase_id', $this->phase->id)->first(); - if (!$layer) return; + if (! $layer) { + return; + } - if ($layer->original_file) Storage::disk('public')->delete($layer->original_file); + if ($layer->original_file) { + Storage::disk('public')->delete($layer->original_file); + } $layer->features()->delete(); $layer->delete(); @@ -337,25 +368,27 @@ class LayerManager extends Component ->where('id', $layerId) ->where('phase_id', $this->phase->id) ->first(); - if (!$layer) return; + if (! $layer) { + return; + } $fc = [ - 'type' => 'FeatureCollection', - 'name' => $layer->name, - 'features' => $layer->features->map(fn($f) => [ - 'type' => 'Feature', - 'geometry' => $f->geometry, + 'type' => 'FeatureCollection', + 'name' => $layer->name, + 'features' => $layer->features->map(fn ($f) => [ + 'type' => 'Feature', + 'geometry' => $f->geometry, 'properties' => array_merge($f->properties ?? [], [ - 'name' => $f->name, - 'progress' => $f->progress, - 'status' => $f->status, + 'name' => $f->name, + 'progress' => $f->progress, + 'status' => $f->status, 'responsible' => $f->responsible, 'template_id' => $f->template_id, ]), ])->values()->toArray(), ]; - $filename = preg_replace('/[^a-z0-9_\-]/i', '_', $layer->name) . '.geojson'; + $filename = preg_replace('/[^a-z0-9_\-]/i', '_', $layer->name).'.geojson'; return response()->streamDownload(function () use ($fc) { echo json_encode($fc, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); @@ -367,7 +400,9 @@ class LayerManager extends Component public function batchAssign($layerId) { $layer = Layer::where('id', $layerId)->where('phase_id', $this->phase->id)->first(); - if (!$layer) return; + if (! $layer) { + return; + } $data = []; if ($this->batchStatus && in_array($this->batchStatus, Feature::STATUSES)) { @@ -378,6 +413,7 @@ class LayerManager extends Component } if (empty($data)) { $this->dispatch('notify', 'Selecciona un estado o template para asignar'); + return; } diff --git a/app/Livewire/Media/MediaManager.php b/app/Livewire/Media/MediaManager.php index 22c92c7..9747d57 100644 --- a/app/Livewire/Media/MediaManager.php +++ b/app/Livewire/Media/MediaManager.php @@ -2,17 +2,10 @@ namespace App\Livewire\Media; +use App\Models\Media; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Livewire\WithFileUploads; -use Livewire\Attributes\On; -use App\Models\Media; -use App\Models\Project; -use App\Models\Phase; -use App\Models\Layer; -use App\Models\Feature; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; class MediaManager extends Component { @@ -20,18 +13,23 @@ class MediaManager extends Component // Polimórfico: a qué entidad pertenece public $mediableType; + public $mediableId; public $entity; // instancia cargada + public $mediaItems = []; // Subida public $uploadFiles = []; + public $uploadDescription = ''; + public $uploadCategory = 'image'; // Modal visor public $showViewer = false; + public $viewingMedia = null; protected $rules = [ @@ -65,8 +63,9 @@ class MediaManager extends Component public function upload() { $user = Auth::user(); - if (!$user->can('upload layers')) { + if (! $user->can('upload layers')) { session()->flash('error', 'Sin permisos.'); + return; } @@ -74,6 +73,7 @@ class MediaManager extends Component if (empty($this->uploadFiles)) { session()->flash('error', 'Selecciona al menos un archivo.'); + return; } @@ -130,8 +130,9 @@ class MediaManager extends Component $media = Media::findOrFail($mediaId); $user = Auth::user(); - if (!$user->can('delete media') && $media->uploaded_by !== $user->id) { + if (! $user->can('delete media') && $media->uploaded_by !== $user->id) { session()->flash('error', 'No puedes borrar archivos de otro usuario.'); + return; } @@ -143,9 +144,10 @@ class MediaManager extends Component public function viewMedia($mediaId) { $media = Media::findOrFail($mediaId); - if (!$media->is_image) { + if (! $media->is_image) { // Si no es imagen, abrir en nueva pestaña $this->dispatch('openUrl', $media->url); + return; } $this->viewingMedia = $media; @@ -161,9 +163,9 @@ class MediaManager extends Component public function render() { return view('livewire.media.media-manager', [ - 'entityName' => class_basename($this->entity) . ': ' . ($this->entity->name ?? $this->entity->id), - 'images' => $this->mediaItems->filter(fn($m) => $m->is_image), - 'documents' => $this->mediaItems->filter(fn($m) => !$m->is_image), + 'entityName' => class_basename($this->entity).': '.($this->entity->name ?? $this->entity->id), + 'images' => $this->mediaItems->filter(fn ($m) => $m->is_image), + 'documents' => $this->mediaItems->filter(fn ($m) => ! $m->is_image), ]); } -} \ No newline at end of file +} diff --git a/app/Livewire/Phases/PhaseGantt.php b/app/Livewire/Phases/PhaseGantt.php index 5580d60..a36098e 100644 --- a/app/Livewire/Phases/PhaseGantt.php +++ b/app/Livewire/Phases/PhaseGantt.php @@ -1,20 +1,23 @@ can('manage all') && !$project->users()->where('user_id', $user->id)->exists()) { + if (! $user->can('manage all') && ! $project->users()->where('user_id', $user->id)->exists()) { abort(403); } $this->project = $project; @@ -25,47 +28,48 @@ class PhaseGantt extends Component { $phases = $this->project->phases()->with(['layers.features'])->orderBy('order')->get(); $projectStart = $this->project->start_date ?? now()->startOfMonth(); - $projectEnd = $this->project->end_date_estimated ?? now()->addMonths(6); + $projectEnd = $this->project->end_date_estimated ?? now()->addMonths(6); - $this->ganttData = $phases->map(function($phase) use ($projectStart, $projectEnd) { + $this->ganttData = $phases->map(function ($phase) use ($projectStart, $projectEnd) { $planned_start = $phase->planned_start ?? $projectStart; - $planned_end = $phase->planned_end ?? $projectEnd; - $actual_start = $phase->actual_start; - $actual_end = $phase->actual_end; + $planned_end = $phase->planned_end ?? $projectEnd; + $actual_start = $phase->actual_start; + $actual_end = $phase->actual_end; $totalDays = max(1, $projectStart->diffInDays($projectEnd)); $pStartOffset = max(0, $projectStart->diffInDays($planned_start)); - $pDuration = max(1, $planned_start->diffInDays($planned_end)); - $pStartPct = round(($pStartOffset / $totalDays) * 100, 2); - $pWidthPct = round(($pDuration / $totalDays) * 100, 2); + $pDuration = max(1, $planned_start->diffInDays($planned_end)); + $pStartPct = round(($pStartOffset / $totalDays) * 100, 2); + $pWidthPct = round(($pDuration / $totalDays) * 100, 2); - $aStartPct = null; $aWidthPct = null; + $aStartPct = null; + $aWidthPct = null; if ($actual_start) { - $aStart = max(0, $projectStart->diffInDays($actual_start)); - $aEnd = $actual_end ?? now(); + $aStart = max(0, $projectStart->diffInDays($actual_start)); + $aEnd = $actual_end ?? now(); $aDuration = max(1, $actual_start->diffInDays($aEnd)); - $aStartPct = round(($aStart / $totalDays) * 100, 2); + $aStartPct = round(($aStart / $totalDays) * 100, 2); $aWidthPct = round(($aDuration / $totalDays) * 100, 2); } $isDelayed = $phase->planned_end && $phase->planned_end->isPast() && $phase->progress_percent < 100; return [ - 'id' => $phase->id, - 'name' => $phase->name, - 'color' => $phase->color ?? '#3b82f6', - 'progress' => $phase->progress_percent, - 'planned_start' => $planned_start->format('d/m/Y'), - 'planned_end' => $planned_end->format('d/m/Y'), - 'actual_start' => $actual_start?->format('d/m/Y'), - 'actual_end' => $actual_end?->format('d/m/Y'), - 'p_start_pct' => $pStartPct, - 'p_width_pct' => min($pWidthPct, 100 - $pStartPct), - 'a_start_pct' => $aStartPct, - 'a_width_pct' => $aWidthPct ? min($aWidthPct, 100 - $aStartPct) : null, - 'is_delayed' => $isDelayed, - 'features_count' => $phase->layers->sum(fn($l) => $l->features->count()), + 'id' => $phase->id, + 'name' => $phase->name, + 'color' => $phase->color ?? '#3b82f6', + 'progress' => $phase->progress_percent, + 'planned_start' => $planned_start->format('d/m/Y'), + 'planned_end' => $planned_end->format('d/m/Y'), + 'actual_start' => $actual_start?->format('d/m/Y'), + 'actual_end' => $actual_end?->format('d/m/Y'), + 'p_start_pct' => $pStartPct, + 'p_width_pct' => min($pWidthPct, 100 - $pStartPct), + 'a_start_pct' => $aStartPct, + 'a_width_pct' => $aWidthPct ? min($aWidthPct, 100 - $aStartPct) : null, + 'is_delayed' => $isDelayed, + 'features_count' => $phase->layers->sum(fn ($l) => $l->features->count()), ]; })->toArray(); } @@ -75,9 +79,9 @@ class PhaseGantt extends Component $phase = $this->project->phases()->findOrFail($phaseId); $phase->update([ 'planned_start' => $plannedStart ?: null, - 'planned_end' => $plannedEnd ?: null, - 'actual_start' => $actualStart ?: null, - 'actual_end' => $actualEnd ?: null, + 'planned_end' => $plannedEnd ?: null, + 'actual_start' => $actualStart ?: null, + 'actual_end' => $actualEnd ?: null, ]); $this->loadGanttData(); $this->dispatch('notify', 'Fechas actualizadas'); @@ -87,7 +91,7 @@ class PhaseGantt extends Component { return view('livewire.projects.phase-gantt', [ 'project' => $this->project, - 'phases' => $this->project->phases()->orderBy('order')->get(), + 'phases' => $this->project->phases()->orderBy('order')->get(), ]); } } diff --git a/app/Livewire/Phases/PhaseList.php b/app/Livewire/Phases/PhaseList.php index 6b5bddf..32b34c6 100644 --- a/app/Livewire/Phases/PhaseList.php +++ b/app/Livewire/Phases/PhaseList.php @@ -2,7 +2,6 @@ namespace App\Livewire\Phases; -use App\Models\Phase; use App\Models\Project; use Illuminate\Support\Facades\Auth; use Livewire\Attributes\On; @@ -14,17 +13,26 @@ class PhaseList extends Component // Modal state public bool $showForm = false; + public $editingId = null; // Form fields public string $name = ''; + public string $description = ''; + public string $color = '#3b82f6'; + public int $order = 1; + public int $progressPercent = 0; + public string $plannedStart = ''; + public string $plannedEnd = ''; + public string $actualStart = ''; + public string $actualEnd = ''; public function mount(Project $project) @@ -35,27 +43,27 @@ class PhaseList extends Component protected function rules(): array { return [ - 'name' => 'required|string|max:255', - 'description' => 'nullable|string', - 'color' => 'required|string|max:7', - 'order' => 'required|integer|min:0', + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'color' => 'required|string|max:7', + 'order' => 'required|integer|min:0', 'progressPercent' => 'required|integer|min:0|max:100', - 'plannedStart' => 'nullable|date', - 'plannedEnd' => 'nullable|date|after_or_equal:plannedStart', - 'actualStart' => 'nullable|date', - 'actualEnd' => 'nullable|date|after_or_equal:actualStart', + 'plannedStart' => 'nullable|date', + 'plannedEnd' => 'nullable|date|after_or_equal:plannedStart', + 'actualStart' => 'nullable|date', + 'actualEnd' => 'nullable|date|after_or_equal:actualStart', ]; } protected $validationAttributes = [ - 'name' => 'nombre', - 'color' => 'color', - 'order' => 'orden', + 'name' => 'nombre', + 'color' => 'color', + 'order' => 'orden', 'progressPercent' => 'progreso', - 'plannedStart' => 'inicio previsto', - 'plannedEnd' => 'fin previsto', - 'actualStart' => 'inicio real', - 'actualEnd' => 'fin real', + 'plannedStart' => 'inicio previsto', + 'plannedEnd' => 'fin previsto', + 'actualStart' => 'inicio real', + 'actualEnd' => 'fin real', ]; public function openForm($phaseId = null): void @@ -65,19 +73,19 @@ class PhaseList extends Component if ($phaseId) { $phase = $this->project->phases()->findOrFail($phaseId); - $this->editingId = $phase->id; - $this->name = $phase->name; - $this->description = $phase->description ?? ''; - $this->color = $phase->color ?? '#3b82f6'; - $this->order = (int) $phase->order; + $this->editingId = $phase->id; + $this->name = $phase->name; + $this->description = $phase->description ?? ''; + $this->color = $phase->color ?? '#3b82f6'; + $this->order = (int) $phase->order; $this->progressPercent = (int) $phase->progress_percent; - $this->plannedStart = $phase->planned_start?->format('Y-m-d') ?? ''; - $this->plannedEnd = $phase->planned_end?->format('Y-m-d') ?? ''; - $this->actualStart = $phase->actual_start?->format('Y-m-d') ?? ''; - $this->actualEnd = $phase->actual_end?->format('Y-m-d') ?? ''; + $this->plannedStart = $phase->planned_start?->format('Y-m-d') ?? ''; + $this->plannedEnd = $phase->planned_end?->format('Y-m-d') ?? ''; + $this->actualStart = $phase->actual_start?->format('Y-m-d') ?? ''; + $this->actualEnd = $phase->actual_end?->format('Y-m-d') ?? ''; } else { $this->order = (int) $this->project->phases()->max('order') + 1; - $this->color = '#' . substr(md5((string) rand()), 0, 6); + $this->color = '#'.substr(md5((string) rand()), 0, 6); } $this->showForm = true; @@ -113,15 +121,15 @@ class PhaseList extends Component $this->validate(); $data = [ - 'name' => $this->name, - 'description' => $this->description ?: null, - 'color' => $this->color, - 'order' => $this->order, + 'name' => $this->name, + 'description' => $this->description ?: null, + 'color' => $this->color, + 'order' => $this->order, 'progress_percent' => $this->progressPercent, - 'planned_start' => $this->plannedStart ?: null, - 'planned_end' => $this->plannedEnd ?: null, - 'actual_start' => $this->actualStart ?: null, - 'actual_end' => $this->actualEnd ?: null, + 'planned_start' => $this->plannedStart ?: null, + 'planned_end' => $this->plannedEnd ?: null, + 'actual_start' => $this->actualStart ?: null, + 'actual_end' => $this->actualEnd ?: null, ]; if ($this->editingId) { diff --git a/app/Livewire/Phases/PhaseProgress.php b/app/Livewire/Phases/PhaseProgress.php index c1c9b2e..0397e2c 100644 --- a/app/Livewire/Phases/PhaseProgress.php +++ b/app/Livewire/Phases/PhaseProgress.php @@ -2,15 +2,17 @@ namespace App\Livewire\Phases; -use Livewire\Component; -use Livewire\Attributes\Layout; use App\Models\Phase; +use Livewire\Attributes\Layout; +use Livewire\Component; #[Layout('layouts.app')] class PhaseProgress extends Component { public Phase $phase; + public $progress; + public $comment = ''; public function mount(Phase $phase) @@ -39,4 +41,4 @@ class PhaseProgress extends Component { return view('livewire.phases.phase-progress'); } -} \ No newline at end of file +} diff --git a/app/Livewire/Phases/PhaseTable.php b/app/Livewire/Phases/PhaseTable.php index 80e6d2f..fc5104d 100644 --- a/app/Livewire/Phases/PhaseTable.php +++ b/app/Livewire/Phases/PhaseTable.php @@ -6,6 +6,7 @@ use App\Models\Phase; use App\Models\Project; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Str; use Livewire\Attributes\On; use Rappasoft\LaravelLivewireTables\DataTableComponent; use Rappasoft\LaravelLivewireTables\Views\Column; @@ -19,9 +20,9 @@ class PhaseTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('order', 'asc') - ->setSortingPillsEnabled(false) - ->setAdditionalSelects(['phases.id as id', 'phases.order as order']); + ->setDefaultSort('order', 'asc') + ->setSortingPillsEnabled(false) + ->setAdditionalSelects(['phases.id as id', 'phases.order as order']); } /** Re-render when the parent (PhaseList) creates/edits a phase. */ @@ -49,64 +50,65 @@ class PhaseTable extends DataTableComponent Column::make('Orden', 'order')->sortable(), Column::make('Nombre', 'name') - ->sortable() - ->searchable() - ->format(function ($value, $row) { - $html = ''.e($value).''; - if ($row->description) { - $html .= '
'.e(\Illuminate\Support\Str::limit($row->description, 60)).'
'; - } - return $html; - }) - ->html(), + ->sortable() + ->searchable() + ->format(function ($value, $row) { + $html = ''.e($value).''; + if ($row->description) { + $html .= '
'.e(Str::limit($row->description, 60)).'
'; + } + + return $html; + }) + ->html(), Column::make('Progreso', 'progress_percent') - ->sortable() - ->format(fn ($value) => - '
+ ->sortable() + ->format(fn ($value) => '
'.(int) $value.'%
') - ->html(), + ->html(), Column::make('Fechas') - ->label(function ($row) { - $ps = $row->planned_start?->format('d/m/Y'); - $pe = $row->planned_end?->format('d/m/Y'); - if (! $ps && ! $pe) { - return ''; - } - return ''.($ps ?: '?').' → '.($pe ?: '?').''; - }) - ->html(), + ->label(function ($row) { + $ps = $row->planned_start?->format('d/m/Y'); + $pe = $row->planned_end?->format('d/m/Y'); + if (! $ps && ! $pe) { + return ''; + } + + return ''.($ps ?: '?').' → '.($pe ?: '?').''; + }) + ->html(), Column::make('Color', 'color') - ->format(fn ($value) => - '
') - ->html(), + ->format(fn ($value) => '
') + ->html(), Column::make('Acciones') - ->label(function ($row) { - $user = Auth::user(); - $progress = route('phases.progress', $row->id); + ->label(function ($row) { + $user = Auth::user(); + $progress = route('phases.progress', $row->id); - $html = '
'; - $html .= ' + $html = '
'; + $html .= ' '; - if ($user->can('manage phases')) { - $html .= ''; - $html .= ''; - } - $html .= '
'; - return $html; - }) - ->html(), + } + $html .= '
'; + + return $html; + }) + ->html(), ]; } diff --git a/app/Livewire/Projects/FeatureManager.php b/app/Livewire/Projects/FeatureManager.php index 3136ac4..24c6237 100644 --- a/app/Livewire/Projects/FeatureManager.php +++ b/app/Livewire/Projects/FeatureManager.php @@ -14,13 +14,18 @@ use Livewire\Component; class FeatureManager extends Component { public Project $project; + public $featureTypes = []; // Edit modal public bool $showForm = false; + public $editingId = null; + public string $name = ''; + public $featureTypeId = ''; + public bool $isActive = true; public function mount(Project $project) @@ -33,6 +38,7 @@ class FeatureManager extends Component private function canManage(): bool { $user = Auth::user(); + return $user->can('manage all') || ($user->can('edit layers') && $this->project->users()->where('user_id', $user->id)->exists()); } @@ -42,10 +48,10 @@ class FeatureManager extends Component { abort_unless($this->canManage(), 403); $feature = $this->findFeature($id); - $this->editingId = $feature->id; - $this->name = $feature->name ?? ''; + $this->editingId = $feature->id; + $this->name = $feature->name ?? ''; $this->featureTypeId = $feature->feature_type_id ?? ''; - $this->isActive = (bool) $feature->is_active; + $this->isActive = (bool) $feature->is_active; $this->resetErrorBag(); $this->showForm = true; } @@ -54,14 +60,14 @@ class FeatureManager extends Component { abort_unless($this->canManage(), 403); $this->validate([ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'featureTypeId' => 'nullable|exists:feature_types,id', ]); $this->findFeature($this->editingId)->update([ - 'name' => $this->name, + 'name' => $this->name, 'feature_type_id' => $this->featureTypeId ?: null, - 'is_active' => $this->isActive, + 'is_active' => $this->isActive, ]); $this->showForm = false; diff --git a/app/Livewire/Projects/FeatureTable.php b/app/Livewire/Projects/FeatureTable.php index d0336a6..5de20eb 100644 --- a/app/Livewire/Projects/FeatureTable.php +++ b/app/Livewire/Projects/FeatureTable.php @@ -22,10 +22,10 @@ class FeatureTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('name', 'asc') - ->setSortingPillsEnabled(false) - ->setSecondaryHeaderEnabled() - ->setAdditionalSelects(['features.id as id', 'features.layer_id as layer_id']); + ->setDefaultSort('name', 'asc') + ->setSortingPillsEnabled(false) + ->setSecondaryHeaderEnabled() + ->setAdditionalSelects(['features.id as id', 'features.layer_id as layer_id']); } public function builder(): Builder @@ -46,40 +46,42 @@ class FeatureTable extends DataTableComponent { return [ Column::make('Elemento', 'name') - ->sortable() - ->searchable() - ->secondaryHeaderFilter('name') - ->format(fn ($value) => '' . e($value) . '') - ->html(), + ->sortable() + ->searchable() + ->secondaryHeaderFilter('name') + ->format(fn ($value) => ''.e($value).'') + ->html(), Column::make('Capa') - ->secondaryHeaderFilter('layer') - ->label(fn ($row) => e($row->layer?->name ?? '—')), + ->secondaryHeaderFilter('layer') + ->label(fn ($row) => e($row->layer?->name ?? '—')), Column::make('Fase') - ->secondaryHeaderFilter('phase') - ->label(fn ($row) => e($row->layer?->phase?->name ?? '—')), + ->secondaryHeaderFilter('phase') + ->label(fn ($row) => e($row->layer?->phase?->name ?? '—')), Column::make('Progreso', 'progress') - ->sortable() - ->format(function ($value) { - $cls = $value >= 100 ? 'badge-success' : ($value > 0 ? 'badge-warning' : 'badge-ghost'); - return '' . (int) $value . '%'; - }) - ->html(), + ->sortable() + ->format(function ($value) { + $cls = $value >= 100 ? 'badge-success' : ($value > 0 ? 'badge-warning' : 'badge-ghost'); + + return ''.(int) $value.'%'; + }) + ->html(), Column::make('Acciones') - ->label(function ($row) { - $id = $row->id; - return '
' - . '' - . '
'; - }) - ->html(), + ->label(function ($row) { + $id = $row->id; + + return '
' + .'' + .'
'; + }) + ->html(), ]; } @@ -93,7 +95,7 @@ class FeatureTable extends DataTableComponent return [ TextFilter::make('Elemento', 'name') ->config(['placeholder' => 'Buscar elemento…']) - ->filter(fn (Builder $query, string $value) => $query->where('features.name', 'like', '%' . $value . '%')), + ->filter(fn (Builder $query, string $value) => $query->where('features.name', 'like', '%'.$value.'%')), SelectFilter::make('Capa', 'layer') ->options(['' => 'Todas'] + $layers) @@ -104,4 +106,4 @@ class FeatureTable extends DataTableComponent ->filter(fn (Builder $query, string $value) => $query->whereHas('layer', fn ($l) => $l->where('phase_id', $value))), ]; } -} \ No newline at end of file +} diff --git a/app/Livewire/Projects/FeatureTypeManager.php b/app/Livewire/Projects/FeatureTypeManager.php index f4ac37e..f24fa00 100644 --- a/app/Livewire/Projects/FeatureTypeManager.php +++ b/app/Livewire/Projects/FeatureTypeManager.php @@ -13,9 +13,13 @@ class FeatureTypeManager extends Component public $types = []; public bool $showForm = false; + public $editingId = null; + public string $name = ''; + public string $description = ''; + public string $color = '#6b7280'; public function mount() @@ -32,9 +36,9 @@ class FeatureTypeManager extends Component protected function rules(): array { return [ - 'name' => 'required|string|max:255|unique:feature_types,name,' . ($this->editingId ?? 'NULL'), + 'name' => 'required|string|max:255|unique:feature_types,name,'.($this->editingId ?? 'NULL'), 'description' => 'nullable|string|max:255', - 'color' => 'required|string|max:7', + 'color' => 'required|string|max:7', ]; } @@ -49,10 +53,10 @@ class FeatureTypeManager extends Component public function edit($id): void { $t = FeatureType::findOrFail($id); - $this->editingId = $t->id; - $this->name = $t->name; + $this->editingId = $t->id; + $this->name = $t->name; $this->description = $t->description ?? ''; - $this->color = $t->color ?? '#6b7280'; + $this->color = $t->color ?? '#6b7280'; $this->resetErrorBag(); $this->showForm = true; } diff --git a/app/Livewire/Projects/InspectionTable.php b/app/Livewire/Projects/InspectionTable.php index 84cf183..9b3c543 100644 --- a/app/Livewire/Projects/InspectionTable.php +++ b/app/Livewire/Projects/InspectionTable.php @@ -22,16 +22,16 @@ class InspectionTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('inspections.created_at', 'desc') - ->setSortingPillsEnabled(false) - ->setSecondaryHeaderEnabled() - ->setAdditionalSelects([ - 'inspections.id as id', - 'inspections.created_at as created_at', - 'inspections.feature_id as feature_id', - 'inspections.template_id as template_id', - 'inspections.user_id as user_id', - ]); + ->setDefaultSort('inspections.created_at', 'desc') + ->setSortingPillsEnabled(false) + ->setSecondaryHeaderEnabled() + ->setAdditionalSelects([ + 'inspections.id as id', + 'inspections.created_at as created_at', + 'inspections.feature_id as feature_id', + 'inspections.template_id as template_id', + 'inspections.user_id as user_id', + ]); } public function builder(): Builder @@ -52,58 +52,57 @@ class InspectionTable extends DataTableComponent { return [ Column::make('Fecha', 'created_at') - ->sortable() - ->secondaryHeaderFilter('fecha') - ->format(fn ($value, $row) => $row->created_at?->format('d/m/Y') ?? '—'), + ->sortable() + ->secondaryHeaderFilter('fecha') + ->format(fn ($value, $row) => $row->created_at?->format('d/m/Y') ?? '—'), Column::make('Elemento') - ->secondaryHeaderFilter('elemento') - ->label(fn ($row) => $row->feature?->name - ? '' . e($row->feature->name) . '' - : '') - ->html(), + ->secondaryHeaderFilter('elemento') + ->label(fn ($row) => $row->feature?->name + ? ''.e($row->feature->name).'' + : '') + ->html(), Column::make('Plantilla') - ->label(fn ($row) => e($row->template?->name ?? '—')), + ->label(fn ($row) => e($row->template?->name ?? '—')), Column::make('Resultado', 'result') - ->sortable() - ->secondaryHeaderFilter('resultado') - ->format(fn ($value) => $value - ? '' . e($value) . '' - : '') - ->html(), + ->sortable() + ->secondaryHeaderFilter('resultado') + ->format(fn ($value) => $value + ? ''.e($value).'' + : '') + ->html(), Column::make('Usuario') - ->secondaryHeaderFilter('usuario') - ->label(fn ($row) => e($row->user?->name ?? '—')), + ->secondaryHeaderFilter('usuario') + ->label(fn ($row) => e($row->user?->name ?? '—')), Column::make('Fotos') - ->label(fn ($row) => $this->renderPhotosColumn($row)) - ->html(), + ->label(fn ($row) => $this->renderPhotosColumn($row)) + ->html(), Column::make('Acciones') - ->label(fn ($row) => - '
' - . '' - . '@can("edit inspections")' - . '' - . '@endcan' - . '@can("delete inspections")' - . '' - . '@endcan' - . '
') - ->html(), + ->label(fn ($row) => '
' + .'' + .'@can("edit inspections")' + .'' + .'@endcan' + .'@can("delete inspections")' + .'' + .'@endcan' + .'
') + ->html(), ]; } @@ -116,15 +115,14 @@ class InspectionTable extends DataTableComponent return ''; } - $thumbnails = $images->take(3)->map(fn ($m) => - '' - . '' . e($m->name) . '' - . '' + $thumbnails = $images->take(3)->map(fn ($m) => '' + .''.e($m->name).'' + .'' )->implode(''); - $more = $count > 3 ? '+' . ($count - 3) . '' : ''; + $more = $count > 3 ? '+'.($count - 3).'' : ''; - return '
' . $thumbnails . $more . '
'; + return '
'.$thumbnails.$more.'
'; } public function filters(): array @@ -134,7 +132,7 @@ class InspectionTable extends DataTableComponent ->pluck('result', 'result')->toArray(); $users = User::whereIn('id', Inspection::where('project_id', $this->projectId) - ->whereNotNull('user_id')->distinct()->pluck('user_id')) + ->whereNotNull('user_id')->distinct()->pluck('user_id')) ->orderBy('name')->pluck('name', 'id')->toArray(); return [ @@ -143,7 +141,7 @@ class InspectionTable extends DataTableComponent TextFilter::make('Elemento', 'elemento') ->config(['placeholder' => 'Buscar elemento…']) - ->filter(fn (Builder $query, string $value) => $query->whereHas('feature', fn ($f) => $f->where('name', 'like', '%' . $value . '%'))), + ->filter(fn (Builder $query, string $value) => $query->whereHas('feature', fn ($f) => $f->where('name', 'like', '%'.$value.'%'))), SelectFilter::make('Resultado', 'resultado') ->options(['' => 'Todos'] + $results) @@ -154,4 +152,4 @@ class InspectionTable extends DataTableComponent ->filter(fn (Builder $query, string $value) => $query->where('inspections.user_id', $value)), ]; } -} \ No newline at end of file +} diff --git a/app/Livewire/Projects/ProjectCompanies.php b/app/Livewire/Projects/ProjectCompanies.php index 5262449..8a7e664 100644 --- a/app/Livewire/Projects/ProjectCompanies.php +++ b/app/Livewire/Projects/ProjectCompanies.php @@ -11,8 +11,11 @@ use Livewire\Component; class ProjectCompanies extends Component { public Project $project; + public $allCompanies = []; + public $selectedCompanyId = ''; + public $selectedRole = 'other'; public function mount(Project $project) @@ -41,7 +44,7 @@ class ProjectCompanies extends Component $this->validate([ 'selectedCompanyId' => 'required|exists:companies,id', - 'selectedRole' => 'required|in:' . implode(',', array_keys(ProjectCompaniesTable::ROLES)), + 'selectedRole' => 'required|in:'.implode(',', array_keys(ProjectCompaniesTable::ROLES)), ]); $this->project->companies()->attach($this->selectedCompanyId, [ diff --git a/app/Livewire/Projects/ProjectCompaniesTable.php b/app/Livewire/Projects/ProjectCompaniesTable.php index 716320f..ccce191 100644 --- a/app/Livewire/Projects/ProjectCompaniesTable.php +++ b/app/Livewire/Projects/ProjectCompaniesTable.php @@ -3,6 +3,7 @@ namespace App\Livewire\Projects; use App\Models\Company; +use App\Models\Project; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; use Livewire\Attributes\On; @@ -18,20 +19,20 @@ class ProjectCompaniesTable extends DataTableComponent /** role_in_project => label */ public const ROLES = [ - 'owner' => 'Promotor', - 'constructor' => 'Constructor', + 'owner' => 'Promotor', + 'constructor' => 'Constructor', 'subcontractor' => 'Subcontratista', - 'consultant' => 'Consultor', - 'supplier' => 'Proveedor', - 'other' => 'Otro', + 'consultant' => 'Consultor', + 'supplier' => 'Proveedor', + 'other' => 'Otro', ]; public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('companies.name', 'asc') - ->setSortingPillsEnabled(false) - ->setAdditionalSelects(['companies.id as id', 'company_project.role_in_project as role_in_project']); + ->setDefaultSort('companies.name', 'asc') + ->setSortingPillsEnabled(false) + ->setAdditionalSelects(['companies.id as id', 'company_project.role_in_project as role_in_project']); } #[On('project-companies-changed')] @@ -51,48 +52,51 @@ class ProjectCompaniesTable extends DataTableComponent { return [ Column::make('Empresa', 'name') - ->sortable() - ->searchable() - ->format(function ($value, $row) { - $initial = strtoupper(mb_substr($value ?? '?', 0, 1)); - $html = '
+ ->sortable() + ->searchable() + ->format(function ($value, $row) { + $initial = strtoupper(mb_substr($value ?? '?', 0, 1)); + $html = '
'.$initial.'
'.e($value).''; - if ($row->tax_id) { - $html .= '
'.e($row->tax_id).'
'; - } - $html .= '
'; - return $html; - }) - ->html(), + if ($row->tax_id) { + $html .= '
'.e($row->tax_id).'
'; + } + $html .= '
'; + + return $html; + }) + ->html(), Column::make('Rol', 'role_in_project') - ->label(function ($row) { - $current = $row->role_in_project; - if (! Auth::user()->can('assign companies')) { - return ''.(self::ROLES[$current] ?? ucfirst((string) $current)).''; - } - $opts = ''; - foreach (self::ROLES as $val => $label) { - $opts .= ''; - } - return ''; - }) - ->html(), + ->label(function ($row) { + $current = $row->role_in_project; + if (! Auth::user()->can('assign companies')) { + return ''.(self::ROLES[$current] ?? ucfirst((string) $current)).''; + } + $opts = ''; + foreach (self::ROLES as $val => $label) { + $opts .= ''; + } + + return ''; + }) + ->html(), Column::make('Acciones') - ->label(function ($row) { - if (! Auth::user()->can('assign companies')) { - return ''; - } - return '
+ ->label(function ($row) { + if (! Auth::user()->can('assign companies')) { + return ''; + } + + return '
'; - }) - ->html(), + }) + ->html(), ]; } @@ -111,7 +115,7 @@ class ProjectCompaniesTable extends DataTableComponent if (! array_key_exists($role, self::ROLES)) { return; } - \App\Models\Project::findOrFail($this->projectId) + Project::findOrFail($this->projectId) ->companies()->updateExistingPivot($companyId, ['role_in_project' => $role]); $this->dispatch('project-companies-changed'); $this->dispatch('notify', 'Rol actualizado.'); @@ -120,7 +124,7 @@ class ProjectCompaniesTable extends DataTableComponent public function removeCompany($companyId): void { abort_unless(Auth::user()->can('assign companies'), 403); - \App\Models\Project::findOrFail($this->projectId)->companies()->detach($companyId); + Project::findOrFail($this->projectId)->companies()->detach($companyId); $this->dispatch('project-companies-changed'); $this->dispatch('notify', 'Empresa eliminada del proyecto.'); } diff --git a/app/Livewire/Projects/ProjectDashboard.php b/app/Livewire/Projects/ProjectDashboard.php index 6b73f07..5500cae 100644 --- a/app/Livewire/Projects/ProjectDashboard.php +++ b/app/Livewire/Projects/ProjectDashboard.php @@ -2,14 +2,14 @@ namespace App\Livewire\Projects; -use Livewire\Component; -use Livewire\Attributes\Layout; -use App\Models\Project; -use App\Models\Phase; use App\Models\Feature; use App\Models\Inspection; use App\Models\Issue; +use App\Models\Phase; +use App\Models\Project; use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Layout; +use Livewire\Component; #[Layout('layouts.app')] class ProjectDashboard extends Component @@ -17,11 +17,16 @@ class ProjectDashboard extends Component public Project $project; // Computed stats (cached as properties after mount) - public array $stats = []; + public array $stats = []; + public $phases; + public $recentInspections; + public $recentIssues; + public $teamMembers; + public $companies; public function mount(Project $project): void @@ -34,8 +39,12 @@ class ProjectDashboard extends Component private function checkAccess(): void { $user = Auth::user(); - if ($user->can('manage all')) return; - if (!$this->project->users()->where('user_id', $user->id)->exists()) abort(403); + if ($user->can('manage all')) { + return; + } + if (! $this->project->users()->where('user_id', $user->id)->exists()) { + abort(403); + } } private function loadData(): void @@ -44,43 +53,42 @@ class ProjectDashboard extends Component $this->phases = Phase::where('project_id', $pid) ->withCount('layers') - ->with(['layers' => fn($q) => $q->withCount('features')]) + ->with(['layers' => fn ($q) => $q->withCount('features')]) ->orderBy('order') ->get(); - $totalFeatures = Feature::whereHas('layer.phase', fn($q) => $q->where('project_id', $pid))->count(); - $completedFeatures = Feature::whereHas('layer.phase', fn($q) => $q->where('project_id', $pid)) + $totalFeatures = Feature::whereHas('layer.phase', fn ($q) => $q->where('project_id', $pid))->count(); + $completedFeatures = Feature::whereHas('layer.phase', fn ($q) => $q->where('project_id', $pid)) ->where('status', 'completed')->count(); - $verifiedFeatures = Feature::whereHas('layer.phase', fn($q) => $q->where('project_id', $pid)) + $verifiedFeatures = Feature::whereHas('layer.phase', fn ($q) => $q->where('project_id', $pid)) ->where('status', 'verified')->count(); - $openIssues = Issue::where('project_id', $pid)->where('status', 'open')->count(); - $closedIssues = Issue::where('project_id', $pid)->where('status', 'closed')->count(); + $openIssues = Issue::where('project_id', $pid)->where('status', 'open')->count(); + $closedIssues = Issue::where('project_id', $pid)->where('status', 'closed')->count(); $criticalIssues = Issue::where('project_id', $pid)->where('status', 'open')->where('priority', 'critical')->count(); - $totalInspections = Inspection::where('project_id', $pid)->count(); - $passedInspections = Inspection::where('project_id', $pid)->where('result', 'pass')->count(); - $failedInspections = Inspection::where('project_id', $pid)->where('result', 'fail')->count(); + $totalInspections = Inspection::where('project_id', $pid)->count(); + $passedInspections = Inspection::where('project_id', $pid)->where('result', 'pass')->count(); + $failedInspections = Inspection::where('project_id', $pid)->where('result', 'fail')->count(); $globalProgress = $this->phases->avg('progress_percent') ?? 0; - $delayedPhases = $this->phases->filter(fn($p) => - $p->planned_end && $p->planned_end < now() && $p->progress_percent < 100 + $delayedPhases = $this->phases->filter(fn ($p) => $p->planned_end && $p->planned_end < now() && $p->progress_percent < 100 )->count(); $this->stats = [ - 'global_progress' => round($globalProgress), - 'total_phases' => $this->phases->count(), - 'delayed_phases' => $delayedPhases, - 'total_features' => $totalFeatures, - 'completed_features' => $completedFeatures, - 'verified_features' => $verifiedFeatures, - 'open_issues' => $openIssues, - 'closed_issues' => $closedIssues, - 'critical_issues' => $criticalIssues, - 'total_inspections' => $totalInspections, - 'passed_inspections' => $passedInspections, - 'failed_inspections' => $failedInspections, + 'global_progress' => round($globalProgress), + 'total_phases' => $this->phases->count(), + 'delayed_phases' => $delayedPhases, + 'total_features' => $totalFeatures, + 'completed_features' => $completedFeatures, + 'verified_features' => $verifiedFeatures, + 'open_issues' => $openIssues, + 'closed_issues' => $closedIssues, + 'critical_issues' => $criticalIssues, + 'total_inspections' => $totalInspections, + 'passed_inspections' => $passedInspections, + 'failed_inspections' => $failedInspections, ]; $this->recentInspections = Inspection::where('project_id', $pid) diff --git a/app/Livewire/Projects/ProjectFeaturesTable.php b/app/Livewire/Projects/ProjectFeaturesTable.php index 61ddf4a..ff1e9fe 100644 --- a/app/Livewire/Projects/ProjectFeaturesTable.php +++ b/app/Livewire/Projects/ProjectFeaturesTable.php @@ -4,6 +4,7 @@ namespace App\Livewire\Projects; use App\Models\Feature; use App\Models\FeatureType; +use App\Models\Layer; use App\Models\Project; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; @@ -22,10 +23,10 @@ class ProjectFeaturesTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('name', 'asc') - ->setSortingPillsEnabled(false) - ->setSecondaryHeaderEnabled() - ->setAdditionalSelects(['features.id as id', 'features.layer_id as layer_id', 'features.feature_type_id as feature_type_id']); + ->setDefaultSort('name', 'asc') + ->setSortingPillsEnabled(false) + ->setSecondaryHeaderEnabled() + ->setAdditionalSelects(['features.id as id', 'features.layer_id as layer_id', 'features.feature_type_id as feature_type_id']); } #[On('features-changed')] @@ -37,6 +38,7 @@ class ProjectFeaturesTable extends DataTableComponent private function canManage(): bool { $user = Auth::user(); + return $user->can('manage all') || ($user->can('edit layers') && Project::whereKey($this->projectId)->whereHas('users', fn ($q) => $q->where('user_id', $user->id))->exists()); @@ -55,47 +57,47 @@ class ProjectFeaturesTable extends DataTableComponent { return [ Column::make('Elemento', 'name') - ->sortable()->searchable() - ->secondaryHeaderFilter('name') - ->format(fn ($value) => '' . e($value) . '') - ->html(), + ->sortable()->searchable() + ->secondaryHeaderFilter('name') + ->format(fn ($value) => ''.e($value).'') + ->html(), Column::make('Capa') - ->secondaryHeaderFilter('layer') - ->label(fn ($row) => e($row->layer?->name ?? '—')), + ->secondaryHeaderFilter('layer') + ->label(fn ($row) => e($row->layer?->name ?? '—')), Column::make('Fase') - ->label(fn ($row) => e($row->layer?->phase?->name ?? '—')), + ->label(fn ($row) => e($row->layer?->phase?->name ?? '—')), Column::make('Tipo') - ->secondaryHeaderFilter('type') - ->label(fn ($row) => $row->featureType - ? '' . e($row->featureType->name) . '' - : '') - ->html(), + ->secondaryHeaderFilter('type') + ->label(fn ($row) => $row->featureType + ? ''.e($row->featureType->name).'' + : '') + ->html(), Column::make('Activo', 'is_active') - ->sortable() - ->label(function ($row) { - if ($row->is_active) { - return ''; - } - return ''; - }) - ->html(), + ->sortable() + ->label(function ($row) { + if ($row->is_active) { + return ''; + } + + return ''; + }) + ->html(), Column::make('Acciones') - ->label(fn ($row) => - '
- -
') - ->html(), + ->html(), ]; } @@ -105,10 +107,10 @@ class ProjectFeaturesTable extends DataTableComponent return [ TextFilter::make('Elemento', 'name') - ->filter(fn (Builder $q, string $v) => $q->where('features.name', 'like', '%' . $v . '%')), + ->filter(fn (Builder $q, string $v) => $q->where('features.name', 'like', '%'.$v.'%')), SelectFilter::make('Capa', 'layer') - ->options(['' => 'Todas'] + \App\Models\Layer::whereHas('phase', fn ($q) => $q->where('project_id', $this->projectId))->orderBy('name')->pluck('name', 'id')->toArray()) + ->options(['' => 'Todas'] + Layer::whereHas('phase', fn ($q) => $q->where('project_id', $this->projectId))->orderBy('name')->pluck('name', 'id')->toArray()) ->filter(fn (Builder $q, string $v) => $q->where('features.layer_id', $v)), SelectFilter::make('Tipo', 'type') diff --git a/app/Livewire/Projects/ProjectForm.php b/app/Livewire/Projects/ProjectForm.php index 5214f9a..8d91bff 100644 --- a/app/Livewire/Projects/ProjectForm.php +++ b/app/Livewire/Projects/ProjectForm.php @@ -2,12 +2,12 @@ namespace App\Livewire\Projects; -use Livewire\Component; -use Livewire\Attributes\Layout; -use Livewire\Attributes\On; use App\Models\Project; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; +use Livewire\Attributes\Layout; +use Livewire\Attributes\On; +use Livewire\Component; #[Layout('layouts.app')] class ProjectForm extends Component @@ -15,33 +15,39 @@ class ProjectForm extends Component public ?Project $project = null; // Identification - public string $name = ''; + public string $name = ''; + public string $reference = ''; - public string $status = 'planning'; + + public string $status = 'planning'; // Location public string $address = ''; + public string $country = ''; - public string $lat = ''; - public string $lng = ''; + + public string $lat = ''; + + public string $lng = ''; // Planning - public string $startDate = ''; - public string $endDateEstimated = ''; + public string $startDate = ''; + + public string $endDateEstimated = ''; public function mount(?Project $project = null): void { if ($project && $project->exists) { Gate::authorize('edit projects', $project); - $this->project = $project; - $this->name = $project->name; - $this->reference = $project->reference ?? ''; - $this->status = $project->status; - $this->address = $project->address; - $this->country = $project->country ?? ''; - $this->lat = (string) ($project->lat ?? ''); - $this->lng = (string) ($project->lng ?? ''); - $this->startDate = $project->start_date->format('Y-m-d'); + $this->project = $project; + $this->name = $project->name; + $this->reference = $project->reference ?? ''; + $this->status = $project->status; + $this->address = $project->address; + $this->country = $project->country ?? ''; + $this->lat = (string) ($project->lat ?? ''); + $this->lng = (string) ($project->lng ?? ''); + $this->startDate = $project->start_date->format('Y-m-d'); $this->endDateEstimated = $project->end_date_estimated?->format('Y-m-d') ?? ''; } else { Gate::authorize('create projects'); @@ -56,34 +62,38 @@ class ProjectForm extends Component { $this->lat = $lat; $this->lng = $lng; - if ($address) $this->address = $address; - if ($country) $this->country = strtolower($country); + if ($address) { + $this->address = $address; + } + if ($country) { + $this->country = strtolower($country); + } } protected function rules(): array { return [ - 'name' => 'required|string|max:255', - 'reference' => 'nullable|string|max:100', - 'status' => 'required|in:planning,in_progress,paused,completed', - 'address' => 'required|string', - 'country' => 'nullable|string|size:2', - 'lat' => 'nullable|numeric|between:-90,90', - 'lng' => 'nullable|numeric|between:-180,180', - 'startDate' => 'required|date', + 'name' => 'required|string|max:255', + 'reference' => 'nullable|string|max:100', + 'status' => 'required|in:planning,in_progress,paused,completed', + 'address' => 'required|string', + 'country' => 'nullable|string|size:2', + 'lat' => 'nullable|numeric|between:-90,90', + 'lng' => 'nullable|numeric|between:-180,180', + 'startDate' => 'required|date', 'endDateEstimated' => 'nullable|date|after_or_equal:startDate', ]; } protected $validationAttributes = [ - 'name' => 'nombre', - 'reference' => 'referencia', - 'status' => 'estado', - 'address' => 'dirección', - 'country' => 'país', - 'lat' => 'latitud', - 'lng' => 'longitud', - 'startDate' => 'fecha de inicio', + 'name' => 'nombre', + 'reference' => 'referencia', + 'status' => 'estado', + 'address' => 'dirección', + 'country' => 'país', + 'lat' => 'latitud', + 'lng' => 'longitud', + 'startDate' => 'fecha de inicio', 'endDateEstimated' => 'fecha de fin estimada', ]; @@ -92,14 +102,14 @@ class ProjectForm extends Component $this->validate(); $data = [ - 'name' => $this->name, - 'reference' => $this->reference ?: null, - 'status' => $this->status, - 'address' => $this->address, - 'country' => $this->country ?: null, - 'lat' => $this->lat ?: null, - 'lng' => $this->lng ?: null, - 'start_date' => $this->startDate, + 'name' => $this->name, + 'reference' => $this->reference ?: null, + 'status' => $this->status, + 'address' => $this->address, + 'country' => $this->country ?: null, + 'lat' => $this->lat ?: null, + 'lng' => $this->lng ?: null, + 'start_date' => $this->startDate, 'end_date_estimated' => $this->endDateEstimated ?: null, ]; diff --git a/app/Livewire/Projects/ProjectList.php b/app/Livewire/Projects/ProjectList.php index 5cf3620..41e9c67 100644 --- a/app/Livewire/Projects/ProjectList.php +++ b/app/Livewire/Projects/ProjectList.php @@ -2,11 +2,11 @@ namespace App\Livewire\Projects; -use Livewire\Component; -use Livewire\WithPagination; -use Livewire\Attributes\Layout; use App\Models\Project; use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Layout; +use Livewire\Component; +use Livewire\WithPagination; #[Layout('layouts.app')] class ProjectList extends Component @@ -14,6 +14,7 @@ class ProjectList extends Component use WithPagination; public $search = ''; + public $statusFilter = ''; public function deleteProject($id) @@ -29,12 +30,13 @@ class ProjectList extends Component { $query = Project::accessibleBy(Auth::user()); if ($this->search) { - $query->where('name', 'like', '%' . $this->search . '%'); + $query->where('name', 'like', '%'.$this->search.'%'); } if ($this->statusFilter) { $query->where('status', $this->statusFilter); } $projects = $query->with('phases')->latest()->paginate(10); + return view('livewire.projects.project-list', ['projects' => $projects]); } -} \ No newline at end of file +} diff --git a/app/Livewire/Projects/ProjectMap.php b/app/Livewire/Projects/ProjectMap.php index 8f17082..943d6f3 100644 --- a/app/Livewire/Projects/ProjectMap.php +++ b/app/Livewire/Projects/ProjectMap.php @@ -2,63 +2,86 @@ namespace App\Livewire\Projects; -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; use App\Models\Feature; use App\Models\Inspection; use App\Models\InspectionTemplate; use App\Models\Issue; +use App\Models\Layer; use App\Models\Media; +use App\Models\Phase; +use App\Models\Project; +use App\Notifications\InspectionCompletedNotification; +use App\Notifications\InspectionDeletedNotification; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Str; +use Livewire\Attributes\On; +use Livewire\Component; +use Livewire\WithFileUploads; class ProjectMap extends Component { use WithFileUploads; public Project $project; + public $phases; + public $activeLayers = []; // Now stores Layer IDs (not Phase IDs) + public $showLayerModal = false; // Editor properties public $selectedFeature = null; + public $selectedPhaseId = null; + public $editProgress = 0; + public $editComment = ''; + public $editResponsible = ''; + public $editPhotos = []; + public $formFullscreen = false; // Tab management public $activeTab = 'edit'; + public $allFeatures; + public $allInspections; // Templates e inspecciones public $templates = []; + public $selectedTemplateId = null; + public $inspectionFormData = []; + public $inspectionHistory = []; // Imágenes en mapa public $showFeatureImages = false; + public $featureImageMarkers = []; // Filters public $filterStatus = ''; + public $filterResponsible = ''; + public $filterProgressMin = 0; + public $filterProgressMax = 100; + public $showFilters = false; // Inspection workflow public $inspectionResult = ''; + public $inspectionNotes = ''; + public $inspectionPhotos = []; // Issues @@ -69,10 +92,15 @@ class ProjectMap extends Component // Inspection editor (para editar inspecciones existentes) public $editingInspection = null; + public $editInspectionFormData = []; + public $editInspectionResult = ''; + public $editInspectionNotes = ''; + public $editInspectionPhotos = []; + public $editInspectionPhotosToDelete = []; public function mount(Project $project) @@ -81,20 +109,20 @@ class ProjectMap extends Component $this->authorizeProjectAccess(); $this->phases = $project->phases()->with([ - 'layers' => fn($q) => $q->withCount('features'), + 'layers' => fn ($q) => $q->withCount('features'), 'layers.features', 'layers.features.images', ])->get(); // Initialize activeLayers with ALL layer IDs (not phase IDs) $this->activeLayers = $this->phases - ->flatMap(fn($p) => $p->layers->pluck('id')) - ->map(fn($id) => (int) $id) + ->flatMap(fn ($p) => $p->layers->pluck('id')) + ->map(fn ($id) => (int) $id) ->toArray(); $this->loadTemplates(); - $this->allFeatures = Feature::whereHas('layer.phase', function($q) use ($project) { + $this->allFeatures = Feature::whereHas('layer.phase', function ($q) use ($project) { $q->where('project_id', $project->id); })->with(['layer.phase', 'template'])->get(); @@ -111,8 +139,12 @@ class ProjectMap extends Component private function authorizeProjectAccess(): void { $user = Auth::user(); - if ($user->can('manage all')) return; - if (!$this->project->users()->where('user_id', $user->id)->exists()) abort(403); + if ($user->can('manage all')) { + return; + } + if (! $this->project->users()->where('user_id', $user->id)->exists()) { + abort(403); + } } public function loadTemplates() @@ -139,9 +171,11 @@ class ProjectMap extends Component public function togglePhase($phaseId) { $phase = $this->phases->find($phaseId); - if (!$phase) return; - $layerIds = $phase->layers->pluck('id')->map(fn($id) => (int) $id)->toArray(); - $allActive = !empty($layerIds) && collect($layerIds)->every(fn($id) => in_array($id, $this->activeLayers)); + if (! $phase) { + return; + } + $layerIds = $phase->layers->pluck('id')->map(fn ($id) => (int) $id)->toArray(); + $allActive = ! empty($layerIds) && collect($layerIds)->every(fn ($id) => in_array($id, $this->activeLayers)); if ($allActive) { $this->activeLayers = array_values(array_diff($this->activeLayers, $layerIds)); } else { @@ -150,22 +184,51 @@ class ProjectMap extends Component $this->dispatch('layersUpdated', $this->activeLayers); } - public function openLayerModal() { $this->showLayerModal = true; } - public function closeLayerModal() { $this->showLayerModal = false; } + public function openLayerModal() + { + $this->showLayerModal = true; + } + + public function closeLayerModal() + { + $this->showLayerModal = false; + } // ─── Filters ──────────────────────────────────────────────────────────────── - public function updatedFilterStatus() { $this->applyFilters(); } - public function updatedFilterResponsible() { $this->applyFilters(); } - public function updatedFilterProgressMin() { $this->applyFilters(); } - public function updatedFilterProgressMax() { $this->applyFilters(); } + public function updatedFilterStatus() + { + $this->applyFilters(); + } + + public function updatedFilterResponsible() + { + $this->applyFilters(); + } + + public function updatedFilterProgressMin() + { + $this->applyFilters(); + } + + public function updatedFilterProgressMax() + { + $this->applyFilters(); + } public function applyFilters() { - $filtered = $this->allFeatures->filter(function($f) { - if ($this->filterStatus && $f->status !== $this->filterStatus) return false; - if ($this->filterResponsible && !str_contains(strtolower($f->responsible ?? ''), strtolower($this->filterResponsible))) return false; - if ($f->progress < $this->filterProgressMin || $f->progress > $this->filterProgressMax) return false; + $filtered = $this->allFeatures->filter(function ($f) { + if ($this->filterStatus && $f->status !== $this->filterStatus) { + return false; + } + if ($this->filterResponsible && ! str_contains(strtolower($f->responsible ?? ''), strtolower($this->filterResponsible))) { + return false; + } + if ($f->progress < $this->filterProgressMin || $f->progress > $this->filterProgressMax) { + return false; + } + return true; }); $this->dispatch('filtersChanged', $filtered->pluck('id')->values()->toArray()); @@ -184,16 +247,24 @@ class ProjectMap extends Component public function editFeatureStatus($status) { - if (!$this->selectedFeature) return; + if (! $this->selectedFeature) { + return; + } $feature = Feature::with('layer.phase')->findOrFail($this->selectedFeature->id); - if ($feature->layer->phase->project_id !== $this->project->id) abort(403); + if ($feature->layer->phase->project_id !== $this->project->id) { + abort(403); + } $feature->status = $status; - if ($status === 'completed') $feature->progress = 100; - if ($status === 'planned') $feature->progress = 0; + if ($status === 'completed') { + $feature->progress = 100; + } + if ($status === 'planned') { + $feature->progress = 0; + } $feature->save(); $this->selectedFeature = $feature; $this->editProgress = $feature->progress; - $this->allFeatures = $this->allFeatures->map(fn($f) => $f->id === $feature->id ? $feature : $f); + $this->allFeatures = $this->allFeatures->map(fn ($f) => $f->id === $feature->id ? $feature : $f); $this->dispatch('featureStatusChanged', $feature->id, $feature->status, $feature->status_color); $this->dispatch('notify', 'Estado actualizado'); } @@ -202,20 +273,23 @@ class ProjectMap extends Component { $feature = Feature::with('layer.phase')->findOrFail($featureId); $user = Auth::user(); - if (!$user->can('update progress')) { + if (! $user->can('update progress')) { $this->dispatch('notify', 'Sin permisos'); + return; } - if ($feature->layer->phase->project_id !== $this->project->id) abort(403); + if ($feature->layer->phase->project_id !== $this->project->id) { + abort(403); + } $feature->progress = min(100, max(0, $newProgress)); $feature->save(); $phase = $feature->layer->phase; $phase->progress_percent = $phase->features()->avg('progress') ?: 0; $phase->save(); $phase->progressUpdates()->create([ - 'user_id' => $user->id, + 'user_id' => $user->id, 'progress_percent' => $phase->progress_percent, - 'comment' => $comment, + 'comment' => $comment, ]); $this->dispatch('progressUpdated', $featureId, $feature->progress); $this->dispatch('notify', 'Progreso actualizado'); @@ -228,25 +302,33 @@ class ProjectMap extends Component #[On('map-select-feature')] public function selectFeature($featureId) { - \Log::info('map-select-feature received', ['payload' => $featureId]); - + \Log::info('[ProjectMap] map-select-feature received', ['payload' => $featureId, 'type' => gettype($featureId)]); + // Handle both formats: direct ID or { featureId: X } if (is_array($featureId) && isset($featureId['featureId'])) { $featureId = $featureId['featureId']; + \Log::info('[ProjectMap] Extracted featureId from array', ['featureId' => $featureId]); } - + $this->selectedFeature = null; $feature = Feature::with(['template', 'layer.phase'])->find($featureId); - if (!$feature) return; - if ($feature->layer->phase->project_id !== $this->project->id) abort(403); + if (! $feature) { + \Log::warning('[ProjectMap] Feature not found', ['featureId' => $featureId]); - $this->selectedFeature = $feature; - $this->selectedPhaseId = $feature->layer->phase_id; - $this->editProgress = $feature->progress; - $this->editResponsible = $feature->responsible ?? ''; - $this->editPhotos = $feature->properties['photos'] ?? []; + return; + } + if ($feature->layer->phase->project_id !== $this->project->id) { + \Log::warning('[ProjectMap] Feature not in project', ['featureId' => $featureId, 'projectId' => $this->project->id]); + abort(403); + } + + $this->selectedFeature = $feature; + $this->selectedPhaseId = $feature->layer->phase_id; + $this->editProgress = $feature->progress; + $this->editResponsible = $feature->responsible ?? ''; + $this->editPhotos = $feature->properties['photos'] ?? []; $this->selectedTemplateId = $feature->template_id; - $this->activeTab = 'edit'; + $this->activeTab = 'edit'; $this->loadInspectionHistory(); $this->resetInspectionForm(); @@ -256,8 +338,9 @@ class ProjectMap extends Component public function loadInspectionHistory() { - if (!$this->selectedFeature) { + if (! $this->selectedFeature) { $this->inspectionHistory = []; + return; } $this->inspectionHistory = Inspection::where('feature_id', $this->selectedFeature->id) @@ -269,9 +352,9 @@ class ProjectMap extends Component public function resetInspectionForm() { $this->inspectionFormData = []; - $this->inspectionResult = ''; - $this->inspectionNotes = ''; - $this->inspectionPhotos = []; + $this->inspectionResult = ''; + $this->inspectionNotes = ''; + $this->inspectionPhotos = []; if ($this->selectedTemplateId) { $template = InspectionTemplate::find($this->selectedTemplateId); if ($template) { @@ -284,45 +367,50 @@ class ProjectMap extends Component public function saveInspection() { - if (!$this->selectedFeature || !$this->selectedTemplateId) { + if (! $this->selectedFeature || ! $this->selectedTemplateId) { $this->dispatch('notify', 'Selecciona un elemento y un template.'); + return; } - + // Verificar permiso - if (!auth()->user()->can('create inspections')) { + if (! auth()->user()->can('create inspections')) { $this->dispatch('notify', 'Sin permisos para crear inspecciones.'); + return; } $feature = Feature::with('layer.phase')->find($this->selectedFeature->id); - if (!$feature || $feature->layer->phase->project_id !== $this->project->id) abort(403); + if (! $feature || $feature->layer->phase->project_id !== $this->project->id) { + abort(403); + } $this->validate([ - 'selectedTemplateId' => 'required|exists:inspection_templates,id', - 'inspectionPhotos.*' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:10240', + 'selectedTemplateId' => 'required|exists:inspection_templates,id', + 'inspectionPhotos.*' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:10240', ]); $template = InspectionTemplate::find($this->selectedTemplateId); foreach ($template->fields as $field) { if (($field['required'] ?? false) && empty($this->inspectionFormData[$field['name']])) { $this->dispatch('notify', "El campo {$field['label']} es obligatorio."); + return; } } $inspection = Inspection::create([ - 'project_id' => $this->project->id, - 'layer_id' => $this->selectedFeature->layer_id, - 'feature_id' => $this->selectedFeature->id, - 'template_id' => $this->selectedTemplateId, - 'user_id' => auth()->id(), + 'project_id' => $this->project->id, + 'layer_id' => $this->selectedFeature->layer_id, + 'feature_id' => $this->selectedFeature->id, + 'template_id' => $this->selectedTemplateId, + 'user_id' => auth()->id(), 'inspector_user_id' => auth()->id(), - 'status' => 'completed', - 'completed_at' => now(), - 'result' => $this->inspectionResult ?: null, - 'notes' => $this->inspectionNotes ?: null, - 'data' => $this->inspectionFormData, + 'status' => 'completed', + 'completed_at' => now(), + 'result' => $this->inspectionResult ?: null, + 'notes' => $this->inspectionNotes ?: null, + 'data' => $this->inspectionFormData, ]); // Fotos adjuntas a la inspección @@ -330,34 +418,34 @@ class ProjectMap extends Component $mime = $photo->getMimeType(); $path = $photo->store("uploads/inspections/{$inspection->id}", 'public'); $inspection->media()->create([ - 'name' => $photo->getClientOriginalName(), - 'file_path' => $path, - 'file_type' => $mime, + 'name' => $photo->getClientOriginalName(), + 'file_path' => $path, + 'file_type' => $mime, 'file_extension' => $photo->getClientOriginalExtension(), - 'file_size' => $photo->getSize(), - 'category' => str_starts_with($mime, 'image/') ? 'image' : 'document', - 'uploaded_by' => auth()->id(), - 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'file_size' => $photo->getSize(), + 'category' => str_starts_with($mime, 'image/') ? 'image' : 'document', + 'uploaded_by' => auth()->id(), + 'uuid' => (string) Str::uuid(), ]); } if ($this->inspectionResult === 'fail') { Issue::create([ - 'project_id' => $this->project->id, - 'feature_id' => $this->selectedFeature->id, + 'project_id' => $this->project->id, + 'feature_id' => $this->selectedFeature->id, 'inspection_id' => $inspection->id, - 'title' => 'Fallo en inspección: ' . ($template->name ?? 'Sin nombre'), - 'description' => $this->inspectionNotes, - 'priority' => 'high', - 'status' => 'open', - 'reported_by' => auth()->id(), + 'title' => 'Fallo en inspección: '.($template->name ?? 'Sin nombre'), + 'description' => $this->inspectionNotes, + 'priority' => 'high', + 'status' => 'open', + 'reported_by' => auth()->id(), ]); $this->openIssuesCount = Issue::where('project_id', $this->project->id) ->where('status', 'open')->count(); $this->dispatch('notify', 'Inspección fallida — Issue creado automáticamente'); } else { if (isset($this->inspectionFormData['progress'])) { - $this->updateProgress($this->selectedFeature->id, (int)$this->inspectionFormData['progress'], 'Inspección registrada'); + $this->updateProgress($this->selectedFeature->id, (int) $this->inspectionFormData['progress'], 'Inspección registrada'); } $this->dispatch('notify', 'Inspección guardada correctamente'); } @@ -367,7 +455,7 @@ class ProjectMap extends Component ->where('user_id', '!=', auth()->id()) ->get(); foreach ($usersToNotify as $user) { - $user->notify(new \App\Notifications\InspectionCompletedNotification($inspection)); + $user->notify(new InspectionCompletedNotification($inspection)); } // Reload global list @@ -382,14 +470,18 @@ class ProjectMap extends Component public function assignTemplateToFeature($templateId) { - if (!$this->selectedFeature) return; + if (! $this->selectedFeature) { + return; + } $template = InspectionTemplate::where('id', $templateId) ->where('project_id', $this->project->id)->first(); - if (!$template) abort(403); + if (! $template) { + abort(403); + } $feature = Feature::findOrFail($this->selectedFeature->id); $feature->template_id = $templateId; $feature->save(); - $this->selectedFeature = $feature; + $this->selectedFeature = $feature; $this->selectedTemplateId = $templateId; $this->resetInspectionForm(); $this->dispatch('notify', 'Template asignado al elemento'); @@ -397,10 +489,14 @@ class ProjectMap extends Component public function saveFeatureProgress() { - if (!$this->selectedFeature) return; + if (! $this->selectedFeature) { + return; + } $feature = Feature::with('layer.phase')->findOrFail($this->selectedFeature->id); - if ($feature->layer->phase->project_id !== $this->project->id) abort(403); - $feature->progress = min(100, max(0, (int)$this->editProgress)); + if ($feature->layer->phase->project_id !== $this->project->id) { + abort(403); + } + $feature->progress = min(100, max(0, (int) $this->editProgress)); $feature->responsible = $this->editResponsible; $feature->save(); $this->selectedFeature = $feature; @@ -424,21 +520,23 @@ class ProjectMap extends Component $ins = Inspection::where('project_id', $this->project->id) ->with(['feature.layer.phase', 'template', 'user', 'media']) ->find($id); - if (!$ins) return; + if (! $ins) { + return; + } $this->viewingInspection = [ - 'id' => $ins->id, - 'feature_name' => $ins->feature?->name ?? '—', - 'layer_name' => $ins->feature?->layer?->name ?? '—', - 'phase_name' => $ins->feature?->layer?->phase?->name ?? '—', + 'id' => $ins->id, + 'feature_name' => $ins->feature?->name ?? '—', + 'layer_name' => $ins->feature?->layer?->name ?? '—', + 'phase_name' => $ins->feature?->layer?->phase?->name ?? '—', 'template_name' => $ins->template?->name ?? '—', - 'user_name' => $ins->user?->name ?? '—', - 'date' => $ins->created_at->format('d/m/Y H:i'), - 'status' => $ins->status, - 'result' => $ins->result, - 'notes' => $ins->notes, - 'data' => $ins->data ?? [], - 'fields' => $ins->template?->fields ?? [], - 'photos' => $ins->media->map(fn ($m) => ['url' => $m->url, 'name' => $m->name, 'id' => $m->id])->values()->all(), + 'user_name' => $ins->user?->name ?? '—', + 'date' => $ins->created_at->format('d/m/Y H:i'), + 'status' => $ins->status, + 'result' => $ins->result, + 'notes' => $ins->notes, + 'data' => $ins->data ?? [], + 'fields' => $ins->template?->fields ?? [], + 'photos' => $ins->media->map(fn ($m) => ['url' => $m->url, 'name' => $m->name, 'id' => $m->id])->values()->all(), ]; } @@ -455,7 +553,9 @@ class ProjectMap extends Component $ins = Inspection::where('project_id', $this->project->id) ->with(['feature.layer.phase', 'template', 'user', 'media']) ->find($id); - if (!$ins) return; + if (! $ins) { + return; + } $this->editingInspection = $ins; $this->editInspectionFormData = $ins->data ?? []; @@ -480,11 +580,13 @@ class ProjectMap extends Component public function deleteEditPhoto($mediaIndex) { - if (!$this->editingInspection) return; + if (! $this->editingInspection) { + return; + } $media = $this->editingInspection->media; if (isset($media[$mediaIndex])) { $m = $media[$mediaIndex]; - if (!in_array($m->id, $this->editInspectionPhotosToDelete)) { + if (! in_array($m->id, $this->editInspectionPhotosToDelete)) { $this->editInspectionPhotosToDelete[] = $m->id; } } @@ -492,34 +594,40 @@ class ProjectMap extends Component public function saveEditInspection() { - if (!$this->editingInspection) return; - + if (! $this->editingInspection) { + return; + } + // Verificar permiso - if (!auth()->user()->can('edit inspections')) { + if (! auth()->user()->can('edit inspections')) { $this->dispatch('notify', 'Sin permisos para editar inspecciones.'); + return; } $this->validate([ - 'selectedTemplateId' => 'required|exists:inspection_templates,id', - 'editInspectionPhotos.*' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:10240', + 'selectedTemplateId' => 'required|exists:inspection_templates,id', + 'editInspectionPhotos.*' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:10240', 'editInspectionPhotosToDelete' => 'array', 'editInspectionPhotosToDelete.*' => 'exists:media,id', ]); $ins = $this->editingInspection; - if ($ins->project_id !== $this->project->id) abort(403); + if ($ins->project_id !== $this->project->id) { + abort(403); + } $template = InspectionTemplate::find($this->selectedTemplateId); foreach ($template->fields as $field) { if (($field['required'] ?? false) && empty($this->editInspectionFormData[$field['name']])) { $this->dispatch('notify', "El campo {$field['label']} es obligatorio."); + return; } } // Eliminar fotos marcadas - if (!empty($this->editInspectionPhotosToDelete)) { + if (! empty($this->editInspectionPhotosToDelete)) { $mediaToDelete = Media::whereIn('id', $this->editInspectionPhotosToDelete) ->where('mediable_type', Inspection::class) ->where('mediable_id', $ins->id) @@ -532,9 +640,9 @@ class ProjectMap extends Component // Actualizar datos $ins->update([ 'template_id' => $this->selectedTemplateId, - 'result' => $this->editInspectionResult ?: null, - 'notes' => $this->editInspectionNotes ?: null, - 'data' => $this->editInspectionFormData, + 'result' => $this->editInspectionResult ?: null, + 'notes' => $this->editInspectionNotes ?: null, + 'data' => $this->editInspectionFormData, ]); // Añadir nuevas fotos @@ -542,14 +650,14 @@ class ProjectMap extends Component $mime = $photo->getMimeType(); $path = $photo->store("uploads/inspections/{$ins->id}", 'public'); $ins->media()->create([ - 'name' => $photo->getClientOriginalName(), - 'file_path' => $path, - 'file_type' => $mime, + 'name' => $photo->getClientOriginalName(), + 'file_path' => $path, + 'file_type' => $mime, 'file_extension' => $photo->getClientOriginalExtension(), - 'file_size' => $photo->getSize(), - 'category' => str_starts_with($mime, 'image/') ? 'image' : 'document', - 'uploaded_by' => auth()->id(), - 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'file_size' => $photo->getSize(), + 'category' => str_starts_with($mime, 'image/') ? 'image' : 'document', + 'uploaded_by' => auth()->id(), + 'uuid' => (string) Str::uuid(), ]); } @@ -570,19 +678,21 @@ class ProjectMap extends Component public function deleteInspection($id) { \Log::info('deleteInspection: START', ['id' => $id]); - - if (!auth()->user()->can('delete inspections')) { + + 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) { + if (! $ins) { \Log::info('deleteInspection: inspection not found', ['id' => $id]); $this->dispatch('notify', 'Inspección no encontrada'); + return; } @@ -612,7 +722,7 @@ class ProjectMap extends Component ->where('user_id', '!=', auth()->id()) ->get(); foreach ($usersToNotify as $user) { - $user->notify(new \App\Notifications\InspectionDeletedNotification($ins)); + $user->notify(new InspectionDeletedNotification($ins)); } } @@ -620,21 +730,25 @@ class ProjectMap extends Component public function toggleFeatureImages() { - $this->showFeatureImages = !$this->showFeatureImages; + $this->showFeatureImages = ! $this->showFeatureImages; $this->loadFeatureImageMarkers(); $this->dispatch('featureImagesToggled', $this->showFeatureImages, $this->featureImageMarkers); } public function loadFeatureImageMarkers() { - if (!$this->showFeatureImages) { $this->featureImageMarkers = []; return; } + if (! $this->showFeatureImages) { + $this->featureImageMarkers = []; + + return; + } $markers = []; foreach ($this->phases as $phase) { foreach ($phase->layers as $layer) { foreach ($layer->features as $feature) { $image = $feature->images->first(); if ($image) { - $geo = $feature->geometry; + $geo = $feature->geometry; $coords = null; if ($geo && isset($geo['coordinates'])) { if ($geo['type'] === 'Point') { @@ -646,10 +760,10 @@ class ProjectMap extends Component if ($coords && $coords['lat'] && $coords['lng']) { $markers[] = [ 'feature_id' => $feature->id, - 'name' => $feature->name, - 'lat' => $coords['lat'], - 'lng' => $coords['lng'], - 'image_url' => $image->url, + 'name' => $feature->name, + 'lat' => $coords['lat'], + 'lng' => $coords['lng'], + 'image_url' => $image->url, 'image_name' => $image->name, ]; } @@ -662,8 +776,10 @@ class ProjectMap extends Component public function toggleFullscreen() { - $this->formFullscreen = !$this->formFullscreen; - if (!$this->formFullscreen) $this->dispatch('mapResize'); + $this->formFullscreen = ! $this->formFullscreen; + if (! $this->formFullscreen) { + $this->dispatch('mapResize'); + } } public function setActiveTab($tab) @@ -675,7 +791,7 @@ class ProjectMap extends Component { return view('livewire.projects.project-map', [ 'project' => $this->project, - 'phases' => $this->phases, + 'phases' => $this->phases, ]); } } diff --git a/app/Livewire/Projects/ProjectTable.php b/app/Livewire/Projects/ProjectTable.php index e96d539..bade725 100644 --- a/app/Livewire/Projects/ProjectTable.php +++ b/app/Livewire/Projects/ProjectTable.php @@ -2,11 +2,11 @@ namespace App\Livewire\Projects; -use Rappasoft\LaravelLivewireTables\DataTableComponent; -use Rappasoft\LaravelLivewireTables\Views\Column; +use App\Models\Project; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; -use App\Models\Project; +use Rappasoft\LaravelLivewireTables\DataTableComponent; +use Rappasoft\LaravelLivewireTables\Views\Column; class ProjectTable extends DataTableComponent { @@ -15,9 +15,9 @@ class ProjectTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('created_at', 'desc') - ->setSortingPillsEnabled(false) - ->setAdditionalSelects(['projects.id as id', 'projects.created_at as created_at']); + ->setDefaultSort('created_at', 'desc') + ->setSortingPillsEnabled(false) + ->setAdditionalSelects(['projects.id as id', 'projects.created_at as created_at']); } public function builder(): Builder @@ -30,88 +30,92 @@ class ProjectTable extends DataTableComponent { return [ Column::make('Referencia', 'reference') - ->sortable() - ->searchable() - ->format(function ($value, $row) { - $url = route('projects.dashboard', $row->id); - return $value - ? ''.e($value).'' - : ''; - }) - ->html(), + ->sortable() + ->searchable() + ->format(function ($value, $row) { + $url = route('projects.dashboard', $row->id); + + return $value + ? ''.e($value).'' + : ''; + }) + ->html(), Column::make(__('Name'), 'name') - ->sortable() - ->searchable(), + ->sortable() + ->searchable(), Column::make(__('Address'), 'address') - ->sortable() - ->searchable() - ->format(fn ($value) => $value - ? ''.e($value).'' - : '') - ->html(), + ->sortable() + ->searchable() + ->format(fn ($value) => $value + ? ''.e($value).'' + : '') + ->html(), Column::make(__('Status'), 'status') - ->sortable() - ->format(function ($value) { - $map = [ - 'planning' => ['badge-ghost', 'Planificación'], - 'in_progress' => ['badge-primary', 'En progreso'], - 'paused' => ['badge-warning', 'Pausado'], - 'completed' => ['badge-success', 'Completado'], - ]; - [$cls, $label] = $map[$value] ?? ['badge-ghost', ucfirst($value)]; - return ''.$label.''; - }) - ->html(), + ->sortable() + ->format(function ($value) { + $map = [ + 'planning' => ['badge-ghost', 'Planificación'], + 'in_progress' => ['badge-primary', 'En progreso'], + 'paused' => ['badge-warning', 'Pausado'], + 'completed' => ['badge-success', 'Completado'], + ]; + [$cls, $label] = $map[$value] ?? ['badge-ghost', ucfirst($value)]; + + return ''.$label.''; + }) + ->html(), Column::make(__('Progress')) - ->label(function ($row) { - $avg = $row->phases->avg('progress_percent') ?? 0; - $pct = round($avg); - return ' + ->label(function ($row) { + $avg = $row->phases->avg('progress_percent') ?? 0; + $pct = round($avg); + + return '
'.$pct.'%
'; - }) - ->html(), + }) + ->html(), Column::make(__('Start Date'), 'start_date') - ->sortable() - ->format(fn ($value) => $value ? $value->format('d/m/Y') : '—'), + ->sortable() + ->format(fn ($value) => $value ? $value->format('d/m/Y') : '—'), Column::make(__('Est. End'), 'end_date_estimated') - ->sortable() - ->format(fn ($value) => $value ? $value->format('d/m/Y') : '—'), + ->sortable() + ->format(fn ($value) => $value ? $value->format('d/m/Y') : '—'), Column::make(__('Actions')) - ->label(function ($row) { - $dashboard = route('projects.dashboard', $row->id); - $map = route('projects.map', $row->id); - $edit = route('projects.edit', $row->id); + ->label(function ($row) { + $dashboard = route('projects.dashboard', $row->id); + $map = route('projects.map', $row->id); + $edit = route('projects.edit', $row->id); - $canEdit = Auth::user()->can('edit projects'); + $canEdit = Auth::user()->can('edit projects'); - $html = '
'; - $html .= ' + $html = '
'; + $html .= ' '; - $html .= ' + $html .= ' '; - if ($canEdit) { - $html .= ' + if ($canEdit) { + $html .= ' '; - } - $html .= '
'; - return $html; - }) - ->html(), + } + $html .= '
'; + + return $html; + }) + ->html(), ]; } diff --git a/app/Livewire/Projects/ProjectTemplatesPicker.php b/app/Livewire/Projects/ProjectTemplatesPicker.php index 4fe05fa..3ed9623 100644 --- a/app/Livewire/Projects/ProjectTemplatesPicker.php +++ b/app/Livewire/Projects/ProjectTemplatesPicker.php @@ -12,7 +12,9 @@ use Livewire\Component; class ProjectTemplatesPicker extends Component { public Project $project; + public array $assignedIds = []; + public string $search = ''; public function mount(Project $project) @@ -41,7 +43,7 @@ class ProjectTemplatesPicker extends Component public function render() { $templates = InspectionTemplate::query() - ->when($this->search !== '', fn ($q) => $q->where('name', 'like', '%' . $this->search . '%')) + ->when($this->search !== '', fn ($q) => $q->where('name', 'like', '%'.$this->search.'%')) ->orderBy('name')->get(); return view('livewire.projects.project-templates-picker', [ diff --git a/app/Livewire/Projects/ProjectUsers.php b/app/Livewire/Projects/ProjectUsers.php index 18f09df..0f52500 100644 --- a/app/Livewire/Projects/ProjectUsers.php +++ b/app/Livewire/Projects/ProjectUsers.php @@ -11,8 +11,11 @@ use Livewire\Component; class ProjectUsers extends Component { public Project $project; + public $allUsers = []; + public $selectedUserId = ''; + public $selectedRole = 'viewer'; public function mount(Project $project) @@ -41,7 +44,7 @@ class ProjectUsers extends Component $this->validate([ 'selectedUserId' => 'required|exists:users,id', - 'selectedRole' => 'required|in:' . implode(',', array_keys(ProjectUsersTable::ROLES)), + 'selectedRole' => 'required|in:'.implode(',', array_keys(ProjectUsersTable::ROLES)), ]); $this->project->users()->attach($this->selectedUserId, [ diff --git a/app/Livewire/Projects/ProjectUsersTable.php b/app/Livewire/Projects/ProjectUsersTable.php index 37f6def..f5a670e 100644 --- a/app/Livewire/Projects/ProjectUsersTable.php +++ b/app/Livewire/Projects/ProjectUsersTable.php @@ -2,6 +2,7 @@ namespace App\Livewire\Projects; +use App\Models\Project; use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; @@ -20,16 +21,16 @@ class ProjectUsersTable extends DataTableComponent public const ROLES = [ 'supervisor' => 'Supervisor', 'consultant' => 'Consultor', - 'client' => 'Cliente', - 'viewer' => 'Observador', + 'client' => 'Cliente', + 'viewer' => 'Observador', ]; public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('users.name', 'asc') - ->setSortingPillsEnabled(false) - ->setAdditionalSelects(['users.id as id', 'project_user.role_in_project as role_in_project']); + ->setDefaultSort('users.name', 'asc') + ->setSortingPillsEnabled(false) + ->setAdditionalSelects(['users.id as id', 'project_user.role_in_project as role_in_project']); } #[On('project-users-changed')] @@ -49,48 +50,51 @@ class ProjectUsersTable extends DataTableComponent { return [ Column::make('Nombre', 'name') - ->sortable() - ->searchable() - ->format(function ($value, $row) { - $initial = strtoupper(mb_substr($value ?? '?', 0, 1)); - return '
+ ->sortable() + ->searchable() + ->format(function ($value, $row) { + $initial = strtoupper(mb_substr($value ?? '?', 0, 1)); + + return '
'.$initial.' '.e($value).'
'; - }) - ->html(), + }) + ->html(), Column::make('Email', 'email') - ->sortable() - ->searchable(), + ->sortable() + ->searchable(), Column::make('Rol', 'role_in_project') - ->label(function ($row) { - $current = $row->role_in_project; - if (! Auth::user()->can('assign users')) { - return ''.(self::ROLES[$current] ?? ucfirst((string) $current)).''; - } - $opts = ''; - foreach (self::ROLES as $val => $label) { - $opts .= ''; - } - return ''; - }) - ->html(), + ->label(function ($row) { + $current = $row->role_in_project; + if (! Auth::user()->can('assign users')) { + return ''.(self::ROLES[$current] ?? ucfirst((string) $current)).''; + } + $opts = ''; + foreach (self::ROLES as $val => $label) { + $opts .= ''; + } + + return ''; + }) + ->html(), Column::make('Acciones') - ->label(function ($row) { - if (! Auth::user()->can('assign users')) { - return ''; - } - return '
+ ->label(function ($row) { + if (! Auth::user()->can('assign users')) { + return ''; + } + + return '
'; - }) - ->html(), + }) + ->html(), ]; } @@ -109,7 +113,7 @@ class ProjectUsersTable extends DataTableComponent if (! array_key_exists($role, self::ROLES)) { return; } - \App\Models\Project::findOrFail($this->projectId) + Project::findOrFail($this->projectId) ->users()->updateExistingPivot($userId, ['role_in_project' => $role]); $this->dispatch('project-users-changed'); $this->dispatch('notify', 'Rol actualizado.'); @@ -118,7 +122,7 @@ class ProjectUsersTable extends DataTableComponent public function removeUser($userId): void { abort_unless(Auth::user()->can('assign users'), 403); - \App\Models\Project::findOrFail($this->projectId)->users()->detach($userId); + Project::findOrFail($this->projectId)->users()->detach($userId); $this->dispatch('project-users-changed'); $this->dispatch('notify', 'Usuario eliminado del proyecto.'); } diff --git a/app/Livewire/Reports/ReportBuilder.php b/app/Livewire/Reports/ReportBuilder.php index 66d3ee0..de84c43 100644 --- a/app/Livewire/Reports/ReportBuilder.php +++ b/app/Livewire/Reports/ReportBuilder.php @@ -10,7 +10,7 @@ use Livewire\Component; class ReportBuilder extends Component { public Project $project; - + public array $filters = [ 'date_from' => null, 'date_to' => null, @@ -19,8 +19,9 @@ class ReportBuilder extends Component 'include_charts' => false, 'format' => 'html', ]; - + public bool $showPreview = false; + public ?array $previewData = null; public function mount(Project $project) @@ -32,7 +33,7 @@ class ReportBuilder extends Component public function authorizeAccess(): void { $user = auth()->guard()->user(); - if (!$user->can('manage all') && !$this->project->users()->where('user_id', $user->id)->exists()) { + if (! $user->can('manage all') && ! $this->project->users()->where('user_id', $user->id)->exists()) { abort(403); } } @@ -122,4 +123,4 @@ class ReportBuilder extends Component 'availableEntities' => (new ReportFilters)->getAvailableEntities(), ]); } -} \ No newline at end of file +} diff --git a/app/Livewire/Reports/ReportsDashboard.php b/app/Livewire/Reports/ReportsDashboard.php index f6bcb9d..414db92 100644 --- a/app/Livewire/Reports/ReportsDashboard.php +++ b/app/Livewire/Reports/ReportsDashboard.php @@ -2,31 +2,32 @@ namespace App\Livewire\Reports; -use Livewire\Component; -use Livewire\Attributes\Layout; -use App\Models\Project; -use App\Models\Phase; use App\Models\Inspection; +use App\Models\Phase; +use App\Models\Project; use Carbon\Carbon; +use Livewire\Attributes\Layout; +use Livewire\Component; #[Layout('layouts.app')] class ReportsDashboard extends Component { public $dateRange = 'month'; // week, month, quarter, year + public $chartData = []; - + public function mount() { $this->loadChartData(); } - + public function loadChartData() { // Project progress over time (last 6 months) - $projects = Project::with(['phases' => function($query) { + $projects = Project::with(['phases' => function ($query) { $query->select('project_id', 'progress_percent', 'updated_at'); }])->get(); - + // Simulate monthly progress data (since we don't have historical stored) // In a real app, we'd have a progress_history table or similar $months = []; @@ -35,7 +36,7 @@ class ReportsDashboard extends Component $month = $current->copy()->subMonths($i); $months[] = $month->format('M Y'); } - + $projectProgress = []; foreach ($projects as $project) { $progressData = []; @@ -49,54 +50,54 @@ class ReportsDashboard extends Component } $projectProgress[] = [ 'name' => $project->name, - 'data' => $progressData + 'data' => $progressData, ]; } - + // Inspections by type (last 6 months) $inspections = Inspection::with(['template', 'feature']) ->whereDate('created_at', '>=', Carbon::now()->subMonths(6)) ->get(); - - $inspectionTypes = $inspections->groupBy(function($inspection) { + + $inspectionTypes = $inspections->groupBy(function ($inspection) { return $inspection->template ? $inspection->template->name : 'Sin plantilla'; - })->map(function($group) { + })->map(function ($group) { return $group->count(); }); - + // Projects by status $projectsByStatus = Project::selectRaw('status, count(*) as count') ->groupBy('status') ->pluck('count', 'status') ->toArray(); - + // Average phase progress by project $projectPhaseProgress = Project::with(['phases']) ->get() - ->map(function($project) { + ->map(function ($project) { return [ 'name' => $project->name, - 'progress' => $project->phases->avg('progress_percent') ?? 0 + 'progress' => $project->phases->avg('progress_percent') ?? 0, ]; }); - + $this->chartData = [ 'months' => $months, 'projectProgress' => $projectProgress, 'inspectionTypes' => [ 'labels' => $inspectionTypes->keys()->toArray(), - 'data' => $inspectionTypes->values()->toArray() + 'data' => $inspectionTypes->values()->toArray(), ], 'projectsByStatus' => [ - 'labels' => array_map(function($status) { + 'labels' => array_map(function ($status) { return ucfirst(str_replace('_', ' ', $status)); }, array_keys($projectsByStatus)), - 'data' => array_values($projectsByStatus) + 'data' => array_values($projectsByStatus), ], - 'projectPhaseProgress' => $projectPhaseProgress + 'projectPhaseProgress' => $projectPhaseProgress, ]; } - + public function render() { return view('livewire.reports.reports-dashboard'); diff --git a/app/Livewire/Users/AdminUsers.php b/app/Livewire/Users/AdminUsers.php index 5e0f0d0..54f3e26 100644 --- a/app/Livewire/Users/AdminUsers.php +++ b/app/Livewire/Users/AdminUsers.php @@ -2,14 +2,15 @@ namespace App\Livewire\Users; -use Livewire\Component; use App\Models\User; -use Spatie\Permission\Models\Role; use Illuminate\Support\Facades\Auth; +use Livewire\Component; +use Spatie\Permission\Models\Role; class AdminUsers extends Component { public string $search = ''; + public $roles; public function mount(): void @@ -21,10 +22,9 @@ class AdminUsers extends Component public function getUsersProperty() { return User::with('roles') - ->when($this->search, fn($q) => - $q->where(fn($q2) => $q2 - ->where('name', 'like', '%' . $this->search . '%') - ->orWhere('email', 'like', '%' . $this->search . '%'))) + ->when($this->search, fn ($q) => $q->where(fn ($q2) => $q2 + ->where('name', 'like', '%'.$this->search.'%') + ->orWhere('email', 'like', '%'.$this->search.'%'))) ->orderBy('name') ->get(); } @@ -33,6 +33,7 @@ class AdminUsers extends Component { if ($userId === Auth::id()) { $this->dispatch('notify', 'No puedes eliminarte a ti mismo.'); + return; } User::findOrFail($userId)->delete(); diff --git a/app/Livewire/Users/UserForm.php b/app/Livewire/Users/UserForm.php index 4f13e76..aba54b7 100644 --- a/app/Livewire/Users/UserForm.php +++ b/app/Livewire/Users/UserForm.php @@ -2,14 +2,14 @@ namespace App\Livewire\Users; -use Livewire\Component; -use Livewire\Attributes\Layout; -use App\Models\User; use App\Models\Company; -use Spatie\Permission\Models\Role; +use App\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules\Password; +use Livewire\Attributes\Layout; +use Livewire\Component; +use Spatie\Permission\Models\Role; #[Layout('layouts.app')] class UserForm extends Component @@ -17,21 +17,29 @@ class UserForm extends Component public ?User $user = null; // Información personal - public string $title = ''; - public string $lastName = ''; + public string $title = ''; + + public string $lastName = ''; + public string $firstName = ''; // Validación public string $userStatus = 'active'; - public string $validFrom = ''; + + public string $validFrom = ''; + public string $validUntil = ''; + public string $formPassword = ''; // Contacto - public ?int $companyId = null; - public string $address = ''; - public string $phone = ''; - public string $email = ''; + public ?int $companyId = null; + + public string $address = ''; + + public string $phone = ''; + + public string $email = ''; // Permisos public string $formRole = ''; @@ -44,6 +52,7 @@ class UserForm extends Component // Catálogos public $roles; + public $companies; /** Idiomas disponibles (código => nombre + archivo de bandera). */ @@ -58,47 +67,47 @@ class UserForm extends Component { abort_unless(Auth::user()->can('create users') || Auth::user()->can('edit users'), 403); - $this->roles = Role::orderBy('name')->get(); + $this->roles = Role::orderBy('name')->get(); $this->companies = Company::where('estado', 'activo')->orderBy('name')->get(); - $this->formRole = $this->roles->first()?->name ?? ''; + $this->formRole = $this->roles->first()?->name ?? ''; if ($user && $user->exists) { - $this->user = $user; - $this->title = $user->title ?? ''; - $this->lastName = $user->last_name ?? ''; - $this->firstName = $user->first_name ?? ''; - $this->userStatus = $user->status ?? 'active'; - $this->validFrom = $user->valid_from?->format('Y-m-d') ?? ''; + $this->user = $user; + $this->title = $user->title ?? ''; + $this->lastName = $user->last_name ?? ''; + $this->firstName = $user->first_name ?? ''; + $this->userStatus = $user->status ?? 'active'; + $this->validFrom = $user->valid_from?->format('Y-m-d') ?? ''; $this->validUntil = $user->valid_until?->format('Y-m-d') ?? ''; - $this->companyId = $user->company_id; - $this->address = $user->address ?? ''; - $this->phone = $user->phone ?? ''; - $this->email = $user->email; - $this->notes = $user->notes ?? ''; - $this->formRole = $user->roles->first()?->name ?? $this->formRole; - $this->locale = $user->locale ?? $this->locale; + $this->companyId = $user->company_id; + $this->address = $user->address ?? ''; + $this->phone = $user->phone ?? ''; + $this->email = $user->email; + $this->notes = $user->notes ?? ''; + $this->formRole = $user->roles->first()?->name ?? $this->formRole; + $this->locale = $user->locale ?? $this->locale; } } protected function rules(): array { - $id = $this->user?->id ?? 'NULL'; + $id = $this->user?->id ?? 'NULL'; $rules = [ - 'lastName' => 'required|string|max:100', - 'firstName' => 'required|string|max:100', - 'title' => 'nullable|string|max:20', + 'lastName' => 'required|string|max:100', + 'firstName' => 'required|string|max:100', + 'title' => 'nullable|string|max:20', 'userStatus' => 'required|in:active,inactive,suspended', - 'validFrom' => 'nullable|date', + 'validFrom' => 'nullable|date', 'validUntil' => 'nullable|date|after_or_equal:validFrom', - 'companyId' => 'required|exists:companies,id', - 'address' => 'nullable|string', - 'phone' => 'nullable|string|max:30', - 'email' => "required|email|max:255|unique:users,email,{$id}", - 'formRole' => 'required|exists:roles,name', - 'locale' => 'required|in:' . implode(',', array_keys($this->languages)), + 'companyId' => 'required|exists:companies,id', + 'address' => 'nullable|string', + 'phone' => 'nullable|string|max:30', + 'email' => "required|email|max:255|unique:users,email,{$id}", + 'formRole' => 'required|exists:roles,name', + 'locale' => 'required|in:'.implode(',', array_keys($this->languages)), ]; - if (!$this->user) { + if (! $this->user) { $rules['formPassword'] = ['required', Password::min(8)->letters()->mixedCase()->numbers()]; } elseif ($this->formPassword !== '') { $rules['formPassword'] = [Password::min(8)->letters()->mixedCase()->numbers()]; @@ -108,20 +117,22 @@ class UserForm extends Component } protected $validationAttributes = [ - 'lastName' => 'apellidos', - 'firstName' => 'nombre', - 'userStatus' => 'estado', - 'validFrom' => 'fecha de inicio', - 'validUntil' => 'fecha de fin', - 'companyId' => 'empresa', - 'formPassword'=> 'contraseña', - 'formRole' => 'rol', - 'locale' => 'idioma', + 'lastName' => 'apellidos', + 'firstName' => 'nombre', + 'userStatus' => 'estado', + 'validFrom' => 'fecha de inicio', + 'validUntil' => 'fecha de fin', + 'companyId' => 'empresa', + 'formPassword' => 'contraseña', + 'formRole' => 'rol', + 'locale' => 'idioma', ]; public function copyCompanyAddress(): void { - if (!$this->companyId) return; + if (! $this->companyId) { + return; + } $company = Company::find($this->companyId); if ($company?->address) { $this->address = $company->address; @@ -135,25 +146,26 @@ class UserForm extends Component if ($this->user && $this->user->id === Auth::id() && $this->user->hasRole('Admin') && $this->formRole !== 'Admin') { $this->addError('formRole', 'No puedes quitarte el rol Admin a ti mismo.'); + return; } - $fullName = trim($this->firstName . ' ' . $this->lastName); + $fullName = trim($this->firstName.' '.$this->lastName); $data = [ - 'name' => $fullName, - 'title' => $this->title ?: null, + 'name' => $fullName, + 'title' => $this->title ?: null, 'first_name' => $this->firstName, - 'last_name' => $this->lastName, - 'status' => $this->userStatus, - 'valid_from' => $this->validFrom ?: null, - 'valid_until'=> $this->validUntil ?: null, + 'last_name' => $this->lastName, + 'status' => $this->userStatus, + 'valid_from' => $this->validFrom ?: null, + 'valid_until' => $this->validUntil ?: null, 'company_id' => $this->companyId, - 'address' => $this->address ?: null, - 'phone' => $this->phone ?: null, - 'email' => $this->email, - 'notes' => $this->notes ?: null, - 'locale' => $this->locale, + 'address' => $this->address ?: null, + 'phone' => $this->phone ?: null, + 'email' => $this->email, + 'notes' => $this->notes ?: null, + 'locale' => $this->locale, ]; if ($this->formPassword !== '') { diff --git a/app/Livewire/Users/UserTable.php b/app/Livewire/Users/UserTable.php index 4d2b4bd..9ac9e68 100644 --- a/app/Livewire/Users/UserTable.php +++ b/app/Livewire/Users/UserTable.php @@ -2,13 +2,13 @@ namespace App\Livewire\Users; +use App\Models\User; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Auth; use Rappasoft\LaravelLivewireTables\DataTableComponent; use Rappasoft\LaravelLivewireTables\Views\Column; use Rappasoft\LaravelLivewireTables\Views\Filters\SelectFilter; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\Auth; use Spatie\Permission\Models\Role; -use App\Models\User; class UserTable extends DataTableComponent { @@ -17,17 +17,17 @@ class UserTable extends DataTableComponent public function configure(): void { $this->setPrimaryKey('id') - ->setDefaultSort('name', 'asc') - ->setSortingPillsEnabled(false) - ->setAdditionalSelects([ - 'users.id as id', - 'users.email as email', - 'users.email_verified_at as email_verified_at', - 'users.status as status', - 'users.phone as phone', - 'users.company_id as company_id', - 'users.created_at as created_at', - ]); + ->setDefaultSort('name', 'asc') + ->setSortingPillsEnabled(false) + ->setAdditionalSelects([ + 'users.id as id', + 'users.email as email', + 'users.email_verified_at as email_verified_at', + 'users.status as status', + 'users.phone as phone', + 'users.company_id as company_id', + 'users.created_at as created_at', + ]); } public function builder(): Builder @@ -39,90 +39,91 @@ class UserTable extends DataTableComponent { return [ Column::make('Usuario', 'name') - ->sortable() - ->searchable() - ->format(function ($value, $row) { - $initial = strtoupper(mb_substr($value, 0, 1)); - $html = '
'; - $html .= '
+ ->sortable() + ->searchable() + ->format(function ($value, $row) { + $initial = strtoupper(mb_substr($value, 0, 1)); + $html = '
'; + $html .= '
'.$initial.'
'; - $html .= '
'; - $html .= '

'.e($value).'

'; - $html .= '

'.e($row->email).'

'; - $html .= '
'; - return $html; - }) - ->html(), + $html .= '
'; + $html .= '

'.e($value).'

'; + $html .= '

'.e($row->email).'

'; + $html .= '
'; + + return $html; + }) + ->html(), Column::make('Empresa') - ->label(fn ($row) => - $row->company - ? ''.e($row->company->name).'' - : '' - ) - ->html(), + ->label(fn ($row) => $row->company + ? ''.e($row->company->name).'' + : '' + ) + ->html(), Column::make('Rol') - ->label(function ($row) { - if ($row->roles->isEmpty()) { - return 'Sin rol'; - } - return $row->roles->map(fn ($role) => - ''.e($role->name).'' - )->implode(' '); - }) - ->html(), + ->label(function ($row) { + if ($row->roles->isEmpty()) { + return 'Sin rol'; + } + + return $row->roles->map(fn ($role) => ''.e($role->name).'' + )->implode(' '); + }) + ->html(), Column::make('Estado', 'status') - ->sortable() - ->format(function ($value) { - $map = [ - 'active' => ['badge-success', 'Activo'], - 'inactive' => ['badge-ghost', 'Inactivo'], - 'suspended' => ['badge-error', 'Suspendido'], - ]; - [$cls, $label] = $map[$value ?? 'active'] ?? ['badge-ghost', ucfirst($value ?? '')]; - return ''.$label.''; - }) - ->html(), + ->sortable() + ->format(function ($value) { + $map = [ + 'active' => ['badge-success', 'Activo'], + 'inactive' => ['badge-ghost', 'Inactivo'], + 'suspended' => ['badge-error', 'Suspendido'], + ]; + [$cls, $label] = $map[$value ?? 'active'] ?? ['badge-ghost', ucfirst($value ?? '')]; + + return ''.$label.''; + }) + ->html(), Column::make('Verificado', 'email_verified_at') - ->sortable() - ->format(fn ($value) => - $value - ? '' - : '' - ) - ->html(), + ->sortable() + ->format(fn ($value) => $value + ? '' + : '' + ) + ->html(), Column::make('Acciones') - ->label(function ($row) { - $ver = route('admin.users.show', $row->id); - $editar = route('admin.users.edit', $row->id); - $name = addslashes($row->name); - $isSelf = $row->id === Auth::id(); + ->label(function ($row) { + $ver = route('admin.users.show', $row->id); + $editar = route('admin.users.edit', $row->id); + $name = addslashes($row->name); + $isSelf = $row->id === Auth::id(); - $html = '
'; - $html .= ' + $html = '
'; + $html .= ' '; - $html .= ' + $html .= ' '; - if (! $isSelf) { - $html .= ''; - } - $html .= '
'; - return $html; - }) - ->html(), + } + $html .= '
'; + + return $html; + }) + ->html(), ]; } @@ -133,16 +134,15 @@ class UserTable extends DataTableComponent return [ SelectFilter::make('Rol') ->options($roleOptions) - ->filter(fn (Builder $query, string $value) => - $query->whereHas('roles', fn ($q) => $q->where('name', $value)) + ->filter(fn (Builder $query, string $value) => $query->whereHas('roles', fn ($q) => $q->where('name', $value)) ), SelectFilter::make('Estado', 'status') ->options([ - '' => 'Estado: todos', - 'active' => 'Activo', - 'inactive' => 'Inactivo', - 'suspended' => 'Suspendido', + '' => 'Estado: todos', + 'active' => 'Activo', + 'inactive' => 'Inactivo', + 'suspended' => 'Suspendido', ]) ->filter(fn (Builder $query, string $value) => $query->where('status', $value)), ]; @@ -150,7 +150,9 @@ class UserTable extends DataTableComponent public function deleteUser(int $id): void { - if ($id === Auth::id()) return; + if ($id === Auth::id()) { + return; + } User::findOrFail($id)->delete(); } } diff --git a/app/Livewire/Users/UserView.php b/app/Livewire/Users/UserView.php index 5624507..a15c2f2 100644 --- a/app/Livewire/Users/UserView.php +++ b/app/Livewire/Users/UserView.php @@ -2,40 +2,45 @@ namespace App\Livewire\Users; -use Livewire\Component; -use Livewire\Attributes\Layout; -use App\Models\User; -use App\Models\Project; use App\Models\Inspection; use App\Models\Issue; +use App\Models\Project; +use App\Models\User; use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Layout; +use Livewire\Component; use Spatie\Permission\Models\Permission; use Spatie\Permission\PermissionRegistrar; #[Layout('layouts.app')] class UserView extends Component { - public User $user; + public User $user; + public string $activeTab = 'ficha'; // Projects tab - public ?int $addProjectId = null; + public ?int $addProjectId = null; + public string $addProjectRole = ''; + public $availableProjects; // Notes tab - public string $notes = ''; - public bool $editingNotes = false; + public string $notes = ''; + + public bool $editingNotes = false; // Recent activity (loaded once) public $recentInspections; + public $recentIssues; public function mount(User $user): void { abort_unless(Auth::user()->can('view users'), 403); - $this->user = $user->load(['roles', 'company', 'projects.phases']); + $this->user = $user->load(['roles', 'company', 'projects.phases']); $this->notes = $user->notes ?? ''; $this->loadAvailableProjects(); @@ -72,7 +77,7 @@ class UserView extends Component public function assignProject(): void { $this->validate([ - 'addProjectId' => 'required|exists:projects,id', + 'addProjectId' => 'required|exists:projects,id', 'addProjectRole' => 'nullable|string|max:100', ], [], ['addProjectId' => 'proyecto', 'addProjectRole' => 'rol en proyecto']); @@ -81,7 +86,7 @@ class UserView extends Component ]); $this->user->load('projects.phases'); - $this->addProjectId = null; + $this->addProjectId = null; $this->addProjectRole = ''; $this->loadAvailableProjects(); $this->dispatch('notify', 'Proyecto asignado.'); @@ -146,13 +151,14 @@ class UserView extends Component ->groupBy(fn ($perm) => $perm->group ?: 'General') ->sortBy(function ($perms, $section) use ($order) { $i = array_search($section, $order, true); + return $i === false ? 999 : $i; }); return view('livewire.users.user-view', [ - 'grouped' => $grouped, + 'grouped' => $grouped, 'directPerms' => $this->user->getDirectPermissions()->pluck('name')->toArray(), - 'rolePerms' => $this->user->getPermissionsViaRoles()->pluck('name')->toArray(), + 'rolePerms' => $this->user->getPermissionsViaRoles()->pluck('name')->toArray(), ]); } } diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php index 7d82abb..f1d0e48 100644 --- a/app/Models/ActivityLog.php +++ b/app/Models/ActivityLog.php @@ -12,18 +12,18 @@ class ActivityLog extends Model protected $fillable = ['action', 'model_type', 'model_id', 'user_id', 'changes', 'created_at']; protected $casts = [ - 'changes' => 'array', + 'changes' => 'array', 'created_at' => 'datetime', ]; public static function record(string $action, Model $model, array $changes = []): void { static::create([ - 'action' => $action, + 'action' => $action, 'model_type' => class_basename($model), - 'model_id' => $model->getKey(), - 'user_id' => Auth::id(), - 'changes' => empty($changes) ? null : $changes, + 'model_id' => $model->getKey(), + 'user_id' => Auth::id(), + 'changes' => empty($changes) ? null : $changes, 'created_at' => now(), ]); } diff --git a/app/Models/Company.php b/app/Models/Company.php index 3344810..442f93e 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -2,7 +2,6 @@ namespace App\Models; -use App\Models\User; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -35,7 +34,7 @@ class Company extends Model public function projects() { return $this->belongsToMany(Project::class, 'company_project') - ->withPivot('role_in_project') - ->withTimestamps(); + ->withPivot('role_in_project') + ->withTimestamps(); } } diff --git a/app/Models/Feature.php b/app/Models/Feature.php index 34c89d0..25edad5 100644 --- a/app/Models/Feature.php +++ b/app/Models/Feature.php @@ -2,14 +2,15 @@ namespace App\Models; +use App\Traits\LogsActivity; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Traits\LogsActivity; class Feature extends Model { - use SoftDeletes, LogsActivity; + use LogsActivity, SoftDeletes; const STATUSES = ['planned', 'started', 'in_progress', 'completed', 'verified']; @@ -22,15 +23,15 @@ class Feature extends Model ]; protected $casts = [ - 'geometry' => 'array', - 'properties' => 'array', - 'is_active' => 'boolean', - 'planned_start' => 'date', - 'planned_end' => 'date', - 'actual_start' => 'date', - 'actual_end' => 'date', - 'baseline_start' => 'date', - 'baseline_end' => 'date', + 'geometry' => 'array', + 'properties' => 'array', + 'is_active' => 'boolean', + 'planned_start' => 'date', + 'planned_end' => 'date', + 'actual_start' => 'date', + 'actual_end' => 'date', + 'baseline_start' => 'date', + 'baseline_end' => 'date', ]; public function featureType() @@ -80,13 +81,13 @@ class Feature extends Model public function getStatusColorAttribute(): string { - return match($this->status) { - 'planned' => '#6b7280', - 'started' => '#3b82f6', + return match ($this->status) { + 'planned' => '#6b7280', + 'started' => '#3b82f6', 'in_progress' => '#f59e0b', - 'completed' => '#10b981', - 'verified' => '#8b5cf6', - default => '#6b7280', + 'completed' => '#10b981', + 'verified' => '#8b5cf6', + default => '#6b7280', }; } @@ -99,7 +100,7 @@ class Feature extends Model return null; } $end = $this->actual_end ?? now()->toDateString(); - + return $this->planned_end->diffInDays($end, false); } @@ -109,28 +110,28 @@ class Feature extends Model return null; } $start = $this->actual_start ?? now()->toDateString(); - + return $this->planned_start->diffInDays($start, false); } - public function getPlannedProgressAtAttribute(?\Carbon\Carbon $date = null): float + public function getPlannedProgressAtAttribute(?Carbon $date = null): float { $date = $date ?? now(); - + if (! $this->planned_start || ! $this->planned_end) { return 0; } - + if ($date <= $this->planned_start) { return 0; } if ($date >= $this->planned_end) { return 100; } - + $totalDays = $this->planned_start->diffInDays($this->planned_end); $elapsedDays = $this->planned_start->diffInDays($date); - + return round(($elapsedDays / $totalDays) * 100, 2); } @@ -138,11 +139,11 @@ class Feature extends Model { $pv = $this->planned_progress_at; $ev = $this->progress; - + if ($pv <= 0) { return null; } - + return round($ev / $pv, 2); } @@ -152,6 +153,7 @@ class Feature extends Model if ($spi === null) { return null; } + return $spi >= 0.95; } } diff --git a/app/Models/Inspection.php b/app/Models/Inspection.php index fdc3d2b..e230126 100644 --- a/app/Models/Inspection.php +++ b/app/Models/Inspection.php @@ -2,13 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Traits\LogsActivity; class Inspection extends Model { - use SoftDeletes, LogsActivity; + use LogsActivity, SoftDeletes; protected static function booted(): void { @@ -19,7 +19,8 @@ class Inspection extends Model } const STATUSES = ['pending', 'in_progress', 'completed', 'approved', 'rejected']; - const RESULTS = ['pass', 'fail', 'conditional']; + + const RESULTS = ['pass', 'fail', 'conditional']; protected $fillable = [ 'project_id', 'layer_id', 'feature_id', 'template_id', 'user_id', @@ -28,7 +29,7 @@ class Inspection extends Model ]; protected $casts = [ - 'data' => 'array', + 'data' => 'array', 'completed_at' => 'datetime', ]; @@ -72,7 +73,18 @@ class Inspection extends Model return $this->hasMany(Issue::class); } - public function scopePending($q) { return $q->where('status', 'pending'); } - public function scopeCompleted($q) { return $q->where('status', 'completed'); } - public function scopeRejected($q) { return $q->where('status', 'rejected'); } + public function scopePending($q) + { + return $q->where('status', 'pending'); + } + + public function scopeCompleted($q) + { + return $q->where('status', 'completed'); + } + + public function scopeRejected($q) + { + return $q->where('status', 'rejected'); + } } diff --git a/app/Models/Issue.php b/app/Models/Issue.php index 70f9fff..825f7c6 100644 --- a/app/Models/Issue.php +++ b/app/Models/Issue.php @@ -2,17 +2,19 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Traits\LogsActivity; class Issue extends Model { - use SoftDeletes, LogsActivity; + use LogsActivity, SoftDeletes; + + const STATUSES = ['open', 'in_review', 'resolved', 'closed']; - const STATUSES = ['open', 'in_review', 'resolved', 'closed']; const PRIORITIES = ['low', 'medium', 'high', 'critical']; - const TYPES = ['defect', 'safety', 'quality', 'documentation', 'other']; + + const TYPES = ['defect', 'safety', 'quality', 'documentation', 'other']; protected $fillable = [ 'project_id', 'feature_id', 'inspection_id', @@ -23,17 +25,55 @@ class Issue extends Model protected $casts = ['resolved_at' => 'datetime']; - public function project() { return $this->belongsTo(Project::class); } - public function feature() { return $this->belongsTo(Feature::class); } - public function inspection() { return $this->belongsTo(Inspection::class); } - public function reporter() { return $this->belongsTo(User::class, 'reported_by'); } - public function assignee() { return $this->belongsTo(User::class, 'assigned_to'); } - public function media() { return $this->morphMany(Media::class, 'mediable'); } - public function tasks() { return $this->hasMany(IssueTask::class)->orderBy('order')->orderBy('id'); } - public function comments() { return $this->hasMany(IssueComment::class)->orderBy('created_at'); } + public function project() + { + return $this->belongsTo(Project::class); + } - public function scopeOpen($q) { return $q->where('status', 'open'); } - public function scopeCritical($q) { return $q->where('priority', 'critical'); } + public function feature() + { + return $this->belongsTo(Feature::class); + } + + public function inspection() + { + return $this->belongsTo(Inspection::class); + } + + public function reporter() + { + return $this->belongsTo(User::class, 'reported_by'); + } + + public function assignee() + { + return $this->belongsTo(User::class, 'assigned_to'); + } + + public function media() + { + return $this->morphMany(Media::class, 'mediable'); + } + + public function tasks() + { + return $this->hasMany(IssueTask::class)->orderBy('order')->orderBy('id'); + } + + public function comments() + { + return $this->hasMany(IssueComment::class)->orderBy('created_at'); + } + + public function scopeOpen($q) + { + return $q->where('status', 'open'); + } + + public function scopeCritical($q) + { + return $q->where('priority', 'critical'); + } /** Resolution progress derived from the checklist: done tasks / total. */ public function getProgressAttribute(): int @@ -42,6 +82,7 @@ class Issue extends Model if ($total === 0) { return in_array($this->status, ['resolved', 'closed'], true) ? 100 : 0; } + return (int) round($this->tasks->where('is_done', true)->count() / $total * 100); } @@ -53,23 +94,23 @@ class Issue extends Model public function getPriorityColorAttribute(): string { - return match($this->priority) { - 'low' => '#6b7280', - 'medium' => '#f59e0b', - 'high' => '#ef4444', + return match ($this->priority) { + 'low' => '#6b7280', + 'medium' => '#f59e0b', + 'high' => '#ef4444', 'critical' => '#7c3aed', - default => '#6b7280', + default => '#6b7280', }; } public function getStatusColorAttribute(): string { - return match($this->status) { - 'open' => '#ef4444', + return match ($this->status) { + 'open' => '#ef4444', 'in_review' => '#f59e0b', - 'resolved' => '#10b981', - 'closed' => '#6b7280', - default => '#6b7280', + 'resolved' => '#10b981', + 'closed' => '#6b7280', + default => '#6b7280', }; } @@ -77,11 +118,11 @@ class Issue extends Model public static function typeLabels(): array { return [ - 'defect' => 'Defecto', - 'safety' => 'Seguridad', - 'quality' => 'Calidad', + 'defect' => 'Defecto', + 'safety' => 'Seguridad', + 'quality' => 'Calidad', 'documentation' => 'Documentación', - 'other' => 'Otro', + 'other' => 'Otro', ]; } @@ -92,12 +133,12 @@ class Issue extends Model public function getTypeColorAttribute(): string { - return match($this->type) { - 'defect' => '#ef4444', - 'safety' => '#f97316', - 'quality' => '#0ea5e9', + return match ($this->type) { + 'defect' => '#ef4444', + 'safety' => '#f97316', + 'quality' => '#0ea5e9', 'documentation' => '#8b5cf6', - default => '#6b7280', + default => '#6b7280', }; } } diff --git a/app/Models/IssueComment.php b/app/Models/IssueComment.php index 0cb50eb..9a4ebf2 100644 --- a/app/Models/IssueComment.php +++ b/app/Models/IssueComment.php @@ -14,7 +14,18 @@ class IssueComment extends Model 'uuid', 'client_updated_at', ]; - public function issue() { return $this->belongsTo(Issue::class); } - public function user() { return $this->belongsTo(User::class); } - public function media() { return $this->morphMany(Media::class, 'mediable'); } + public function issue() + { + return $this->belongsTo(Issue::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function media() + { + return $this->morphMany(Media::class, 'mediable'); + } } diff --git a/app/Models/IssueTask.php b/app/Models/IssueTask.php index 34cbbd7..442335f 100644 --- a/app/Models/IssueTask.php +++ b/app/Models/IssueTask.php @@ -16,9 +16,9 @@ class IssueTask extends Model ]; protected $casts = [ - 'is_done' => 'boolean', - 'done_at' => 'datetime', - 'due_date' => 'date', + 'is_done' => 'boolean', + 'done_at' => 'datetime', + 'due_date' => 'date', 'overdue_notified_at' => 'datetime', ]; @@ -26,14 +26,29 @@ class IssueTask extends Model public function scopeOverdue($q) { return $q->where('is_done', false) - ->whereNotNull('due_date') - ->whereDate('due_date', '<', now()->toDateString()); + ->whereNotNull('due_date') + ->whereDate('due_date', '<', now()->toDateString()); } - public function issue() { return $this->belongsTo(Issue::class); } - public function assignee() { return $this->belongsTo(User::class, 'assigned_to'); } - public function completer() { return $this->belongsTo(User::class, 'done_by'); } - public function media() { return $this->morphMany(Media::class, 'mediable'); } + public function issue() + { + return $this->belongsTo(Issue::class); + } + + public function assignee() + { + return $this->belongsTo(User::class, 'assigned_to'); + } + + public function completer() + { + return $this->belongsTo(User::class, 'done_by'); + } + + public function media() + { + return $this->morphMany(Media::class, 'mediable'); + } /** Overdue = has a due date in the past and not yet done. */ public function getIsOverdueAttribute(): bool diff --git a/app/Models/Layer.php b/app/Models/Layer.php index 087cbb8..32ba570 100644 --- a/app/Models/Layer.php +++ b/app/Models/Layer.php @@ -5,13 +5,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; - class Layer extends Model { use SoftDeletes; protected $fillable = [ - 'project_id', 'phase_id', 'name', 'color', 'geojson_data', 'original_file', 'uploaded_by' + 'project_id', 'phase_id', 'name', 'color', 'geojson_data', 'original_file', 'uploaded_by', ]; protected $casts = [ @@ -32,6 +31,7 @@ class Layer extends Model { return $this->belongsTo(User::class, 'uploaded_by'); } + public function features() { return $this->hasMany(Feature::class); @@ -46,4 +46,4 @@ class Layer extends Model { return $this->morphMany(Media::class, 'mediable'); } -} \ No newline at end of file +} diff --git a/app/Models/Media.php b/app/Models/Media.php index 02c6cc6..2e0c3d7 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -48,10 +48,17 @@ class Media extends Model public function getFormattedSizeAttribute() { $bytes = $this->file_size; - if ($bytes >= 1073741824) return round($bytes / 1073741824, 2) . ' GB'; - if ($bytes >= 1048576) return round($bytes / 1048576, 1) . ' MB'; - if ($bytes >= 1024) return round($bytes / 1024) . ' KB'; - return $bytes . ' B'; + if ($bytes >= 1073741824) { + return round($bytes / 1073741824, 2).' GB'; + } + if ($bytes >= 1048576) { + return round($bytes / 1048576, 1).' MB'; + } + if ($bytes >= 1024) { + return round($bytes / 1024).' KB'; + } + + return $bytes.' B'; } // Scopes @@ -72,4 +79,4 @@ class Media extends Model Storage::delete($media->file_path); }); } -} \ No newline at end of file +} diff --git a/app/Models/PendingSync.php b/app/Models/PendingSync.php index 015dd48..10155f7 100644 --- a/app/Models/PendingSync.php +++ b/app/Models/PendingSync.php @@ -12,4 +12,4 @@ class PendingSync extends Model 'payload' => 'array', 'synced_at' => 'datetime', ]; -} \ No newline at end of file +} diff --git a/app/Models/Phase.php b/app/Models/Phase.php index 5f608ef..3168f25 100644 --- a/app/Models/Phase.php +++ b/app/Models/Phase.php @@ -2,6 +2,7 @@ namespace App\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -79,7 +80,7 @@ class Phase extends Model return null; } $end = $this->actual_end ?? now()->toDateString(); - + return $this->planned_end->diffInDays($end, false); } @@ -92,31 +93,31 @@ class Phase extends Model return null; } $start = $this->actual_start ?? now()->toDateString(); - + return $this->planned_start->diffInDays($start, false); } /** * Planned progress at a given date (linear interpolation) */ - public function getPlannedProgressAtAttribute(?\Carbon\Carbon $date = null): float + public function getPlannedProgressAtAttribute(?Carbon $date = null): float { $date = $date ?? now(); - + if (! $this->planned_start || ! $this->planned_end) { return 0; } - + if ($date <= $this->planned_start) { return 0; } if ($date >= $this->planned_end) { return 100; } - + $totalDays = $this->planned_start->diffInDays($this->planned_end); $elapsedDays = $this->planned_start->diffInDays($date); - + return round(($elapsedDays / $totalDays) * 100, 2); } @@ -127,11 +128,11 @@ class Phase extends Model { $pv = $this->planned_progress_at; // Planned Value $ev = $this->progress_percent; // Earned Value (current progress) - + if ($pv <= 0) { return null; } - + return round($ev / $pv, 2); } @@ -144,6 +145,7 @@ class Phase extends Model if ($spi === null) { return null; } + return $spi >= 0.95; } } diff --git a/app/Models/ProgressSnapshot.php b/app/Models/ProgressSnapshot.php index 3d91b3f..267d6e6 100644 --- a/app/Models/ProgressSnapshot.php +++ b/app/Models/ProgressSnapshot.php @@ -52,6 +52,7 @@ class ProgressSnapshot extends Model if ($id) { $query->where('trackable_id', $id); } + return $query; } @@ -76,4 +77,4 @@ class ProgressSnapshot extends Model ->get(['snapshot_date', 'progress', 'method']) ->toArray(); } -} \ No newline at end of file +} diff --git a/app/Models/ProgressUpdate.php b/app/Models/ProgressUpdate.php index 972745e..1832666 100644 --- a/app/Models/ProgressUpdate.php +++ b/app/Models/ProgressUpdate.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model; class ProgressUpdate extends Model { protected $fillable = [ - 'uuid', 'phase_id', 'user_id', 'progress_percent', 'comment', 'location', 'client_updated_at' + 'uuid', 'phase_id', 'user_id', 'progress_percent', 'comment', 'location', 'client_updated_at', ]; protected $casts = [ @@ -24,4 +24,4 @@ class ProgressUpdate extends Model { return $this->belongsTo(User::class); } -} \ No newline at end of file +} diff --git a/app/Notifications/FeatureCompletedNotification.php b/app/Notifications/FeatureCompletedNotification.php index 51db57b..c7608c2 100644 --- a/app/Notifications/FeatureCompletedNotification.php +++ b/app/Notifications/FeatureCompletedNotification.php @@ -2,9 +2,9 @@ namespace App\Notifications; +use App\Models\Feature; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; -use App\Models\Feature; class FeatureCompletedNotification extends Notification { @@ -20,12 +20,12 @@ class FeatureCompletedNotification extends Notification public function toArray($notifiable): array { return [ - 'type' => 'feature_completed', - 'feature_id' => $this->feature->id, - 'project_id' => $this->feature->layer?->phase?->project_id, + 'type' => 'feature_completed', + 'feature_id' => $this->feature->id, + 'project_id' => $this->feature->layer?->phase?->project_id, 'feature_name' => $this->feature->name, - 'progress' => 100, - 'message' => "Elemento '{$this->feature->name}' marcado como completado", + 'progress' => 100, + 'message' => "Elemento '{$this->feature->name}' marcado como completado", ]; } } diff --git a/app/Notifications/InspectionCompletedNotification.php b/app/Notifications/InspectionCompletedNotification.php index b9fb9ed..52b1e67 100644 --- a/app/Notifications/InspectionCompletedNotification.php +++ b/app/Notifications/InspectionCompletedNotification.php @@ -2,10 +2,9 @@ namespace App\Notifications; +use App\Models\Inspection; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; -use Illuminate\Notifications\Messages\MailMessage; -use App\Models\Inspection; class InspectionCompletedNotification extends Notification { @@ -21,13 +20,13 @@ class InspectionCompletedNotification extends Notification public function toArray($notifiable): array { return [ - 'type' => 'inspection_completed', + 'type' => 'inspection_completed', 'inspection_id' => $this->inspection->id, - 'project_id' => $this->inspection->project_id, - 'feature_name' => $this->inspection->feature?->name ?? '—', + 'project_id' => $this->inspection->project_id, + 'feature_name' => $this->inspection->feature?->name ?? '—', 'template_name' => $this->inspection->template?->name ?? '—', - 'result' => $this->inspection->result, - 'message' => "Inspección completada en '{$this->inspection->feature?->name}': " . ($this->inspection->result ?? 'sin resultado'), + 'result' => $this->inspection->result, + 'message' => "Inspección completada en '{$this->inspection->feature?->name}': ".($this->inspection->result ?? 'sin resultado'), ]; } } diff --git a/app/Notifications/InspectionDeletedNotification.php b/app/Notifications/InspectionDeletedNotification.php index 4710f8c..0515a2c 100644 --- a/app/Notifications/InspectionDeletedNotification.php +++ b/app/Notifications/InspectionDeletedNotification.php @@ -2,9 +2,9 @@ namespace App\Notifications; +use App\Models\Inspection; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; -use App\Models\Inspection; class InspectionDeletedNotification extends Notification { @@ -20,13 +20,13 @@ class InspectionDeletedNotification extends Notification public function toArray($notifiable): array { return [ - 'type' => 'inspection_deleted', + 'type' => 'inspection_deleted', 'inspection_id' => $this->inspection->id, - 'project_id' => $this->inspection->project_id, - 'feature_name' => $this->inspection->feature?->name ?? '—', + 'project_id' => $this->inspection->project_id, + 'feature_name' => $this->inspection->feature?->name ?? '—', 'template_name' => $this->inspection->template?->name ?? '—', - 'deleted_by' => $this->deletedBy, - 'message' => "Inspección eliminada en '{$this->inspection->feature?->name}' por {$this->deletedBy}", + 'deleted_by' => $this->deletedBy, + 'message' => "Inspección eliminada en '{$this->inspection->feature?->name}' por {$this->deletedBy}", ]; } -} \ No newline at end of file +} diff --git a/app/Notifications/InspectionUpdatedNotification.php b/app/Notifications/InspectionUpdatedNotification.php index ffcd9c8..e3b9637 100644 --- a/app/Notifications/InspectionUpdatedNotification.php +++ b/app/Notifications/InspectionUpdatedNotification.php @@ -2,9 +2,9 @@ namespace App\Notifications; +use App\Models\Inspection; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; -use App\Models\Inspection; class InspectionUpdatedNotification extends Notification { @@ -20,14 +20,14 @@ class InspectionUpdatedNotification extends Notification public function toArray($notifiable): array { return [ - 'type' => 'inspection_updated', + 'type' => 'inspection_updated', 'inspection_id' => $this->inspection->id, - 'project_id' => $this->inspection->project_id, - 'feature_name' => $this->inspection->feature?->name ?? '—', + 'project_id' => $this->inspection->project_id, + 'feature_name' => $this->inspection->feature?->name ?? '—', 'template_name' => $this->inspection->template?->name ?? '—', - 'result' => $this->inspection->result, - 'changes' => $this->changes, - 'message' => "Inspección actualizada en '{$this->inspection->feature?->name}'", + 'result' => $this->inspection->result, + 'changes' => $this->changes, + 'message' => "Inspección actualizada en '{$this->inspection->feature?->name}'", ]; } -} \ No newline at end of file +} diff --git a/app/Notifications/IssueAssignedNotification.php b/app/Notifications/IssueAssignedNotification.php index ed26e35..9a6577a 100644 --- a/app/Notifications/IssueAssignedNotification.php +++ b/app/Notifications/IssueAssignedNotification.php @@ -20,11 +20,11 @@ class IssueAssignedNotification extends Notification public function toArray($notifiable): array { return [ - 'type' => 'issue_assigned', - 'issue_id' => $this->issue->id, + 'type' => 'issue_assigned', + 'issue_id' => $this->issue->id, 'project_id' => $this->issue->project_id, - 'priority' => $this->issue->priority, - 'message' => "Se te ha asignado la incidencia '{$this->issue->title}'", + 'priority' => $this->issue->priority, + 'message' => "Se te ha asignado la incidencia '{$this->issue->title}'", ]; } } diff --git a/app/Notifications/IssueCommentedNotification.php b/app/Notifications/IssueCommentedNotification.php index 164772f..f72fd0d 100644 --- a/app/Notifications/IssueCommentedNotification.php +++ b/app/Notifications/IssueCommentedNotification.php @@ -23,11 +23,11 @@ class IssueCommentedNotification extends Notification $issue = $this->comment->issue; return [ - 'type' => 'issue_commented', - 'issue_id' => $this->comment->issue_id, + 'type' => 'issue_commented', + 'issue_id' => $this->comment->issue_id, 'project_id' => $issue?->project_id, - 'author' => $this->comment->user?->name, - 'message' => "{$this->comment->user?->name} comentó en '{$issue?->title}': " . Str::limit($this->comment->body, 60), + 'author' => $this->comment->user?->name, + 'message' => "{$this->comment->user?->name} comentó en '{$issue?->title}': ".Str::limit($this->comment->body, 60), ]; } } diff --git a/app/Notifications/IssueReportedNotification.php b/app/Notifications/IssueReportedNotification.php index 74280ca..1fdc208 100644 --- a/app/Notifications/IssueReportedNotification.php +++ b/app/Notifications/IssueReportedNotification.php @@ -2,9 +2,9 @@ namespace App\Notifications; +use App\Models\Issue; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; -use App\Models\Issue; class IssueReportedNotification extends Notification { @@ -20,12 +20,12 @@ class IssueReportedNotification extends Notification public function toArray($notifiable): array { return [ - 'type' => 'issue_reported', - 'issue_id' => $this->issue->id, - 'project_id' => $this->issue->project_id, + 'type' => 'issue_reported', + 'issue_id' => $this->issue->id, + 'project_id' => $this->issue->project_id, 'feature_name' => $this->issue->feature?->name ?? '—', - 'priority' => $this->issue->priority, - 'message' => "Nuevo issue '{$this->issue->title}' (prioridad: {$this->issue->priority})", + 'priority' => $this->issue->priority, + 'message' => "Nuevo issue '{$this->issue->title}' (prioridad: {$this->issue->priority})", ]; } } diff --git a/app/Notifications/IssueStatusChangedNotification.php b/app/Notifications/IssueStatusChangedNotification.php index 61dc309..74d9fd5 100644 --- a/app/Notifications/IssueStatusChangedNotification.php +++ b/app/Notifications/IssueStatusChangedNotification.php @@ -20,18 +20,18 @@ class IssueStatusChangedNotification extends Notification public function toArray($notifiable): array { $label = [ - 'open' => 'reabierta', + 'open' => 'reabierta', 'in_review' => 'enviada a revisión', - 'resolved' => 'resuelta', - 'closed' => 'cerrada', + 'resolved' => 'resuelta', + 'closed' => 'cerrada', ][$this->status] ?? $this->status; return [ - 'type' => 'issue_status_changed', - 'issue_id' => $this->issue->id, + 'type' => 'issue_status_changed', + 'issue_id' => $this->issue->id, 'project_id' => $this->issue->project_id, - 'status' => $this->status, - 'message' => "La incidencia '{$this->issue->title}' ha sido {$label}", + 'status' => $this->status, + 'message' => "La incidencia '{$this->issue->title}' ha sido {$label}", ]; } } diff --git a/app/Notifications/IssueTaskAssignedNotification.php b/app/Notifications/IssueTaskAssignedNotification.php index 90be82c..43fb98d 100644 --- a/app/Notifications/IssueTaskAssignedNotification.php +++ b/app/Notifications/IssueTaskAssignedNotification.php @@ -20,12 +20,12 @@ class IssueTaskAssignedNotification extends Notification public function toArray($notifiable): array { return [ - 'type' => 'issue_task_assigned', - 'issue_id' => $this->task->issue_id, - 'task_id' => $this->task->id, + 'type' => 'issue_task_assigned', + 'issue_id' => $this->task->issue_id, + 'task_id' => $this->task->id, 'project_id' => $this->task->issue?->project_id, - 'due_date' => $this->task->due_date?->toDateString(), - 'message' => "Se te ha asignado la tarea '{$this->task->title}'", + 'due_date' => $this->task->due_date?->toDateString(), + 'message' => "Se te ha asignado la tarea '{$this->task->title}'", ]; } } diff --git a/app/Notifications/IssueTaskOverdueNotification.php b/app/Notifications/IssueTaskOverdueNotification.php index f89f60e..e2f39b1 100644 --- a/app/Notifications/IssueTaskOverdueNotification.php +++ b/app/Notifications/IssueTaskOverdueNotification.php @@ -20,12 +20,12 @@ class IssueTaskOverdueNotification extends Notification public function toArray($notifiable): array { return [ - 'type' => 'issue_task_overdue', - 'issue_id' => $this->task->issue_id, - 'task_id' => $this->task->id, + 'type' => 'issue_task_overdue', + 'issue_id' => $this->task->issue_id, + 'task_id' => $this->task->id, 'project_id' => $this->task->issue?->project_id, - 'due_date' => $this->task->due_date?->toDateString(), - 'message' => "Tarea vencida: '{$this->task->title}' (venció el {$this->task->due_date?->format('d/m/Y')})", + 'due_date' => $this->task->due_date?->toDateString(), + 'message' => "Tarea vencida: '{$this->task->title}' (venció el {$this->task->due_date?->format('d/m/Y')})", ]; } } diff --git a/app/Services/ReportGenerator.php b/app/Services/ReportGenerator.php index a77eb02..850946a 100644 --- a/app/Services/ReportGenerator.php +++ b/app/Services/ReportGenerator.php @@ -8,16 +8,15 @@ use App\Models\Inspection; use App\Models\Issue; use App\Models\Media; use App\Models\Phase; -use App\Models\Project; use App\Models\ProgressSnapshot; +use App\Models\Project; use App\Models\Task; use Carbon\Carbon; -use Illuminate\Database\Eloquent\Collection; -use Illuminate\Support\Facades\DB; class ReportGenerator { protected Project $project; + protected ReportFilters $filters; public function __construct(Project $project, ReportFilters $filters) @@ -82,7 +81,7 @@ class ReportGenerator ->with(['layers.features']) ->get(); - $allFeatures = $phases->flatMap(fn($p) => $p->layers->flatMap(fn($l) => $l->features)); + $allFeatures = $phases->flatMap(fn ($p) => $p->layers->flatMap(fn ($l) => $l->features)); $completedFeatures = $allFeatures->where('status', 'completed')->count(); $inspectionsQuery = Inspection::where('project_id', $this->project->id); @@ -136,8 +135,8 @@ class ReportGenerator ->get(); return $phases->map(function ($phase) { - $phaseFeatures = $phase->layers->flatMap(fn($l) => $l->features); - + $phaseFeatures = $phase->layers->flatMap(fn ($l) => $l->features); + return [ 'id' => $phase->id, 'name' => $phase->name, @@ -158,7 +157,7 @@ class ReportGenerator 'is_on_track' => $phase->is_on_track, 'features_count' => $phaseFeatures->count(), 'completed_features' => $phaseFeatures->where('status', 'completed')->count(), - 'layers' => $phase->layers->map(fn($l) => [ + 'layers' => $phase->layers->map(fn ($l) => [ 'id' => $l->id, 'name' => $l->name, 'features_count' => $l->features->count(), @@ -169,7 +168,7 @@ class ReportGenerator protected function buildFeaturesData(): array { - $query = Feature::whereHas('layer.phase', fn($q) => $q->where('project_id', $this->project->id)) + $query = Feature::whereHas('layer.phase', fn ($q) => $q->where('project_id', $this->project->id)) ->with(['layer.phase', 'template', 'inspections', 'issues']); // Apply date filter on feature created_at if date range provided @@ -179,7 +178,7 @@ class ReportGenerator return $features->map(function ($feature) { $lastInspection = $feature->inspections->sortByDesc('created_at')->first(); - + return [ 'id' => $feature->id, 'name' => $feature->name, @@ -227,7 +226,7 @@ class ReportGenerator 'date' => $inspection->created_at->format('d/m/Y H:i'), 'status' => $inspection->status, 'result' => $inspection->result, - 'result_label' => match($inspection->result) { + 'result_label' => match ($inspection->result) { 'pass' => 'Aprobada', 'fail' => 'Fallida', 'conditional' => 'Condicional', @@ -256,7 +255,7 @@ class ReportGenerator 'feature' => $issue->feature?->name ?? '—', 'phase' => $issue->feature?->layer?->phase?->name ?? '—', 'priority' => $issue->priority, - 'priority_label' => match($issue->priority) { + 'priority_label' => match ($issue->priority) { 'low' => 'Baja', 'medium' => 'Media', 'high' => 'Alta', @@ -264,7 +263,7 @@ class ReportGenerator default => ucfirst($issue->priority ?? ''), }, 'status' => $issue->status, - 'status_label' => match($issue->status) { + 'status_label' => match ($issue->status) { 'open' => 'Abierta', 'in_review' => 'En revisión', 'closed' => 'Cerrada', @@ -308,7 +307,7 @@ class ReportGenerator 'actual_hours' => $task->actual_hours, 'progress' => $task->progress, 'is_overdue' => $task->is_overdue, - 'subtasks' => $task->subtasks->map(fn($st) => [ + 'subtasks' => $task->subtasks->map(fn ($st) => [ 'id' => $st->id, 'title' => $st->title, 'status' => $st->status, @@ -341,8 +340,10 @@ class ReportGenerator // Filter by project in PHP (polymorphic complexity) $filtered = $media->filter(function ($m) { $mediable = $m->mediable; - if (!$mediable) return false; - + if (! $mediable) { + return false; + } + if ($mediable instanceof Phase) { return $mediable->project_id === $this->project->id; } @@ -358,6 +359,7 @@ class ReportGenerator if ($mediable instanceof Task) { return $mediable->project_id === $this->project->id; } + return false; }); @@ -398,9 +400,9 @@ class ReportGenerator 'spi' => $phase->spi, 'is_on_track' => $phase->is_on_track, ]; - })->filter(fn($p) => $p['planned_start'] || $p['planned_end'])->toArray(); + })->filter(fn ($p) => $p['planned_start'] || $p['planned_end'])->toArray(); - $featureDeviations = Feature::whereHas('layer.phase', fn($q) => $q->where('project_id', $this->project->id)) + $featureDeviations = Feature::whereHas('layer.phase', fn ($q) => $q->where('project_id', $this->project->id)) ->whereNotNull('planned_end') ->with(['layer.phase']) ->get() @@ -428,12 +430,12 @@ class ReportGenerator 'phases' => $phaseDeviations, 'features' => $featureDeviations, 'summary' => [ - 'phases_delayed' => count(array_filter($phaseDeviations, fn($p) => ($p['end_deviation'] ?? 0) > 0)), - 'phases_early' => count(array_filter($phaseDeviations, fn($p) => ($p['end_deviation'] ?? 0) < 0)), - 'phases_on_time' => count(array_filter($phaseDeviations, fn($p) => ($p['end_deviation'] ?? 0) === 0)), - 'features_delayed' => count(array_filter($featureDeviations, fn($f) => ($f['end_deviation'] ?? 0) > 0)), - 'features_early' => count(array_filter($featureDeviations, fn($f) => ($f['end_deviation'] ?? 0) < 0)), - 'features_on_time' => count(array_filter($featureDeviations, fn($f) => ($f['end_deviation'] ?? 0) === 0)), + 'phases_delayed' => count(array_filter($phaseDeviations, fn ($p) => ($p['end_deviation'] ?? 0) > 0)), + 'phases_early' => count(array_filter($phaseDeviations, fn ($p) => ($p['end_deviation'] ?? 0) < 0)), + 'phases_on_time' => count(array_filter($phaseDeviations, fn ($p) => ($p['end_deviation'] ?? 0) === 0)), + 'features_delayed' => count(array_filter($featureDeviations, fn ($f) => ($f['end_deviation'] ?? 0) > 0)), + 'features_early' => count(array_filter($featureDeviations, fn ($f) => ($f['end_deviation'] ?? 0) < 0)), + 'features_on_time' => count(array_filter($featureDeviations, fn ($f) => ($f['end_deviation'] ?? 0) === 0)), ], ]; } @@ -442,7 +444,7 @@ class ReportGenerator { // Get snapshots for this project's phases and features $phaseIds = $this->project->phases->pluck('id')->toArray(); - $featureIds = Feature::whereHas('layer.phase', fn($q) => $q->whereIn('phase_id', $phaseIds)) + $featureIds = Feature::whereHas('layer.phase', fn ($q) => $q->whereIn('phase_id', $phaseIds)) ->pluck('id')->toArray(); $taskIds = Task::where('project_id', $this->project->id)->pluck('id')->toArray(); @@ -475,7 +477,7 @@ class ReportGenerator foreach ($grouped as $date => $snaps) { $dates[] = Carbon::parse($date)->format('d/m/Y'); - + $phaseSnaps = $snaps->where('trackable_type', Phase::class); $featureSnaps = $snaps->where('trackable_type', Feature::class); $taskSnaps = $snaps->where('trackable_type', Task::class); @@ -523,7 +525,7 @@ class ReportGenerator protected function getFeatureStatusLabel(string $status): string { - return match($status) { + return match ($status) { 'planned' => 'Planificado', 'started' => 'Iniciado', 'in_progress' => 'En progreso', @@ -541,6 +543,7 @@ class ReportGenerator $bytes /= 1024; $i++; } - return round($bytes, 1) . ' ' . $units[$i]; + + return round($bytes, 1).' '.$units[$i]; } -} \ No newline at end of file +} diff --git a/app/Services/SpatialFileConverter.php b/app/Services/SpatialFileConverter.php index 6b7d4fe..1699b76 100644 --- a/app/Services/SpatialFileConverter.php +++ b/app/Services/SpatialFileConverter.php @@ -15,14 +15,16 @@ class SpatialFileConverter $geojson = match ($ext) { 'geojson' => self::parseGeoJson($path), - 'kml' => self::kmlToGeoJson($path), - 'kmz' => self::kmzToGeoJson($path), - 'shp' => self::shapefileToGeoJson($path), - 'zip' => self::handleZip($path), - default => null, + 'kml' => self::kmlToGeoJson($path), + 'kmz' => self::kmzToGeoJson($path), + 'shp' => self::shapefileToGeoJson($path), + 'zip' => self::handleZip($path), + default => null, }; - if (!$geojson) return null; + if (! $geojson) { + return null; + } return self::postProcess($geojson); } @@ -37,15 +39,19 @@ class SpatialFileConverter foreach ($geojson['features'] ?? [] as $feature) { - if (!isset($feature['geometry'])) continue; + if (! isset($feature['geometry'])) { + continue; + } $geometry = self::cleanGeometry($feature['geometry']); - if (!$geometry) continue; + if (! $geometry) { + continue; + } $features[] = [ 'type' => 'Feature', 'geometry' => $geometry, - 'properties' => self::normalizeProperties($feature['properties'] ?? []) + 'properties' => self::normalizeProperties($feature['properties'] ?? []), ]; } @@ -53,7 +59,7 @@ class SpatialFileConverter 'type' => 'FeatureCollection', 'features' => $features, 'bbox' => self::calculateBBox($features), - 'centroid' => self::calculateCentroid($features) + 'centroid' => self::calculateCentroid($features), ]; } @@ -75,13 +81,16 @@ class SpatialFileConverter private static function cleanGeometry(array $geom): ?array { - if (!isset($geom['type'], $geom['coordinates'])) return null; + if (! isset($geom['type'], $geom['coordinates'])) { + return null; + } if ($geom['type'] === 'Polygon') { $geom['coordinates'] = array_map(function ($ring) { if ($ring[0] !== end($ring)) { $ring[] = $ring[0]; } + return $ring; }, $geom['coordinates']); } @@ -101,7 +110,9 @@ class SpatialFileConverter $coords = array_merge($coords, self::flattenCoords($f['geometry']['coordinates'])); } - if (empty($coords)) return null; + if (empty($coords)) { + return null; + } $lons = array_column($coords, 0); $lats = array_column($coords, 1); @@ -110,7 +121,7 @@ class SpatialFileConverter min($lons), min($lats), max($lons), - max($lats) + max($lats), ]; } @@ -126,7 +137,9 @@ class SpatialFileConverter $coords = array_merge($coords, self::flattenCoords($f['geometry']['coordinates'])); } - if (empty($coords)) return null; + if (empty($coords)) { + return null; + } $x = array_sum(array_column($coords, 0)) / count($coords); $y = array_sum(array_column($coords, 1)) / count($coords); @@ -139,10 +152,13 @@ class SpatialFileConverter $result = []; $iterator = function ($c) use (&$result, &$iterator) { - if (!is_array($c)) return; + if (! is_array($c)) { + return; + } if (isset($c[0]) && isset($c[1]) && is_numeric($c[0])) { $result[] = [$c[0], $c[1]]; + return; } @@ -163,6 +179,7 @@ class SpatialFileConverter private static function parseGeoJson($path): ?array { $data = json_decode(file_get_contents($path), true); + return json_last_error() === JSON_ERROR_NONE ? $data : null; } @@ -175,18 +192,24 @@ class SpatialFileConverter libxml_use_internal_errors(true); $xml = simplexml_load_file($path); - if (!$xml) return null; + if (! $xml) { + return null; + } // Namespace-agnostic: usamos local-name() en el XPath para aceptar KMLs // con cualquier xmlns (opengis 2.2, earth.google 2.1/2.0, o sin xmlns). $placemarks = $xml->xpath('//*[local-name()="Placemark"]'); - if ($placemarks === false) $placemarks = []; + if ($placemarks === false) { + $placemarks = []; + } $features = []; foreach ($placemarks as $pm) { $geom = self::parseKmlGeometry($pm); - if (!$geom) continue; + if (! $geom) { + continue; + } $features[] = [ 'type' => 'Feature', @@ -204,12 +227,15 @@ class SpatialFileConverter /** Descomprime un KMZ y parsea el .kml interno. */ private static function kmzToGeoJson(string $path): ?array { - $zip = new \ZipArchive(); - if ($zip->open($path) !== true) return null; + $zip = new \ZipArchive; + if ($zip->open($path) !== true) { + return null; + } - $tmp = sys_get_temp_dir() . '/kmz_' . uniqid(); + $tmp = sys_get_temp_dir().'/kmz_'.uniqid(); if (! @mkdir($tmp, 0777, true) && ! is_dir($tmp)) { $zip->close(); + return null; } $zip->extractTo($tmp); @@ -228,15 +254,20 @@ class SpatialFileConverter // Limpieza self::rrmdir($tmp); + return $result; } private static function rrmdir(string $dir): void { - if (! is_dir($dir)) return; + if (! is_dir($dir)) { + return; + } foreach (scandir($dir) ?: [] as $item) { - if ($item === '.' || $item === '..') continue; - $p = $dir . DIRECTORY_SEPARATOR . $item; + if ($item === '.' || $item === '..') { + continue; + } + $p = $dir.DIRECTORY_SEPARATOR.$item; is_dir($p) ? self::rrmdir($p) : @unlink($p); } @rmdir($dir); @@ -245,38 +276,45 @@ class SpatialFileConverter /** Devuelve el primer hijo cuyo local-name coincida (sin depender del prefijo). */ private static function kmlChild(\SimpleXMLElement $node, string $name): \SimpleXMLElement|string { - $matches = $node->xpath('./*[local-name()="' . $name . '"]'); + $matches = $node->xpath('./*[local-name()="'.$name.'"]'); + return $matches ? $matches[0] : ''; } private static function parseKmlGeometry($pm): ?array { // Todos los accesos van por xpath local-name para tolerar cualquier xmlns. - $find = fn (string $tag) => $pm->xpath('./*[local-name()="' . $tag . '"]'); - $findDeep = fn (\SimpleXMLElement $n, string $tag) => $n->xpath('.//*[local-name()="' . $tag . '"]'); + $find = fn (string $tag) => $pm->xpath('./*[local-name()="'.$tag.'"]'); + $findDeep = fn (\SimpleXMLElement $n, string $tag) => $n->xpath('.//*[local-name()="'.$tag.'"]'); if ($multi = $find('MultiGeometry')) { $geoms = []; foreach ($multi[0]->children() as $g) { $parsed = self::parseKmlGeometry($g); - if ($parsed) $geoms[] = $parsed; + if ($parsed) { + $geoms[] = $parsed; + } } + return ['type' => 'GeometryCollection', 'geometries' => $geoms]; } if ($point = $find('Point')) { $coords = self::parseKmlCoords((string) ($findDeep($point[0], 'coordinates')[0] ?? '')); + return $coords ? ['type' => 'Point', 'coordinates' => $coords[0]] : null; } if ($line = $find('LineString')) { $coords = self::parseKmlCoords((string) ($findDeep($line[0], 'coordinates')[0] ?? '')); + return $coords ? ['type' => 'LineString', 'coordinates' => $coords] : null; } if ($poly = $find('Polygon')) { $outer = $findDeep($poly[0], 'coordinates')[0] ?? ''; $coords = self::parseKmlCoords((string) $outer); + return $coords ? ['type' => 'Polygon', 'coordinates' => [$coords]] : null; } @@ -289,9 +327,10 @@ class SpatialFileConverter foreach (preg_split('/\s+/', trim($text)) as $pair) { $p = explode(',', $pair); if (count($p) >= 2) { - $coords[] = [(float)$p[0], (float)$p[1]]; + $coords[] = [(float) $p[0], (float) $p[1]]; } } + return $coords; } @@ -307,16 +346,20 @@ class SpatialFileConverter $features = []; while ($record = $reader->fetchRecord()) { - if ($record->isDeleted()) continue; + if ($record->isDeleted()) { + continue; + } $geom = json_decode($record->getGeometry()->toGeoJSON(), true); - if (!$geom) continue; + if (! $geom) { + continue; + } $features[] = [ 'type' => 'Feature', 'geometry' => $geom, - 'properties' => $record->getDataArray() + 'properties' => $record->getDataArray(), ]; } @@ -324,6 +367,7 @@ class SpatialFileConverter } catch (\Exception $e) { Log::error($e->getMessage()); + return null; } } @@ -334,11 +378,13 @@ class SpatialFileConverter private static function handleZip($zipPath): ?array { - $zip = new \ZipArchive(); + $zip = new \ZipArchive; - if ($zip->open($zipPath) !== true) return null; + if ($zip->open($zipPath) !== true) { + return null; + } - $dir = sys_get_temp_dir() . '/geo_' . uniqid(); + $dir = sys_get_temp_dir().'/geo_'.uniqid(); mkdir($dir); $zip->extractTo($dir); @@ -347,7 +393,7 @@ class SpatialFileConverter $result = null; foreach (scandir($dir) as $file) { - $full = $dir . '/' . $file; + $full = $dir.'/'.$file; $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); if ($ext === 'shp') { @@ -373,4 +419,4 @@ class SpatialFileConverter } rmdir($dir); } -} \ No newline at end of file +} diff --git a/app/Traits/LogsActivity.php b/app/Traits/LogsActivity.php index 6299b08..15f4cf9 100644 --- a/app/Traits/LogsActivity.php +++ b/app/Traits/LogsActivity.php @@ -2,7 +2,6 @@ namespace App\Traits; -use Illuminate\Support\Facades\Auth; use App\Models\ActivityLog; trait LogsActivity diff --git a/bootstrap/app.php b/bootstrap/app.php index 7b8594d..8f63d08 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,8 +1,16 @@ withRouting( @@ -12,32 +20,31 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - $middleware->appendToGroup('web', \App\Http\Middleware\SetLocale::class); + $middleware->appendToGroup('web', SetLocale::class); // Si APP_URL es https, forzar el scheme percibido en TODAS las peticiones // (algunos proxies no reenvían X-Forwarded-Proto en todas las rutas, y las // URLs firmadas —p. ej. /livewire/upload-file— se firman con un scheme y // se validan con otro → 401). Debe ir ANTES de TrustProxies. - $middleware->prepend(\App\Http\Middleware\ForceHttpsScheme::class); + $middleware->prepend(ForceHttpsScheme::class); // Confiar en el proxy inverso (OpenResty/Nginx) para leer X-Forwarded-Proto: // sin esto, tras HTTPS el proxy Laravel genera URLs con http:// y las cookies // "secure" no viajan → 419 en /livewire/update. - $middleware->trustProxies(at: '*', headers: - Illuminate\Http\Request::HEADER_X_FORWARDED_FOR | - Illuminate\Http\Request::HEADER_X_FORWARDED_HOST | - Illuminate\Http\Request::HEADER_X_FORWARDED_PORT | - Illuminate\Http\Request::HEADER_X_FORWARDED_PROTO | - Illuminate\Http\Request::HEADER_X_FORWARDED_AWS_ELB, + $middleware->trustProxies(at: '*', headers: Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB, ); // Spatie permission + Sanctum ability middleware aliases $middleware->alias([ - 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, - 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, - 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, - 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, - 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, + 'role' => RoleMiddleware::class, + 'permission' => PermissionMiddleware::class, + 'role_or_permission' => RoleOrPermissionMiddleware::class, + 'abilities' => CheckAbilities::class, + 'ability' => CheckForAnyAbility::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 7d50f93..c60700c 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,6 +1,9 @@ [ - 'disk' => null, - 'rules' => ['required', 'file', 'max:51200'], // 50 MB - 'directory' => null, - 'middleware' => null, + 'disk' => null, + 'rules' => ['required', 'file', 'max:51200'], // 50 MB + 'directory' => null, + 'middleware' => null, 'preview_mimes' => [ 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', 'mov', 'avi', 'wmv', 'mp3', 'm4a', 'jpg', 'jpeg', 'mpga', 'webp', 'wma', ], 'max_upload_time' => 5, - 'cleanup' => true, + 'cleanup' => true, ], /* @@ -64,7 +64,7 @@ return [ | Navegación SPA (wire:navigate). */ 'navigate' => [ - 'show_progress_bar' => true, + 'show_progress_bar' => true, 'progress_bar_color' => '#2299dd', ], diff --git a/database/migrations/2026_04_27_095918_create_projects_table.php b/database/migrations/2026_04_27_095918_create_projects_table.php index b15d93b..eb413f7 100644 --- a/database/migrations/2026_04_27_095918_create_projects_table.php +++ b/database/migrations/2026_04_27_095918_create_projects_table.php @@ -30,4 +30,4 @@ return new class extends Migration { Schema::dropIfExists('projects'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_27_095919_create_phases_table.php b/database/migrations/2026_04_27_095919_create_phases_table.php index a844d43..3ec2a4f 100644 --- a/database/migrations/2026_04_27_095919_create_phases_table.php +++ b/database/migrations/2026_04_27_095919_create_phases_table.php @@ -29,4 +29,4 @@ return new class extends Migration { Schema::dropIfExists('phases'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_27_095920_create_layers_table.php b/database/migrations/2026_04_27_095920_create_layers_table.php index 4d3f313..02647b4 100644 --- a/database/migrations/2026_04_27_095920_create_layers_table.php +++ b/database/migrations/2026_04_27_095920_create_layers_table.php @@ -25,4 +25,4 @@ return new class extends Migration { Schema::dropIfExists('layers'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_27_095921_create_progress_updates_table.php b/database/migrations/2026_04_27_095921_create_progress_updates_table.php index f0a8aba..0573b2d 100644 --- a/database/migrations/2026_04_27_095921_create_progress_updates_table.php +++ b/database/migrations/2026_04_27_095921_create_progress_updates_table.php @@ -25,4 +25,4 @@ return new class extends Migration { Schema::dropIfExists('progress_updates'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_27_095925_create_pending_syncs_table.php b/database/migrations/2026_04_27_095925_create_pending_syncs_table.php index 0d2ffc1..d9da819 100644 --- a/database/migrations/2026_04_27_095925_create_pending_syncs_table.php +++ b/database/migrations/2026_04_27_095925_create_pending_syncs_table.php @@ -22,4 +22,4 @@ return new class extends Migration { Schema::dropIfExists('pending_syncs'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_27_095925_create_project_user_table.php b/database/migrations/2026_04_27_095925_create_project_user_table.php index 87d8f0d..5fe68a4 100644 --- a/database/migrations/2026_04_27_095925_create_project_user_table.php +++ b/database/migrations/2026_04_27_095925_create_project_user_table.php @@ -21,4 +21,4 @@ return new class extends Migration { Schema::dropIfExists('project_user'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_05_09_210000_create_media_table.php b/database/migrations/2026_05_09_210000_create_media_table.php index 2893ce8..cefea1c 100644 --- a/database/migrations/2026_05_09_210000_create_media_table.php +++ b/database/migrations/2026_05_09_210000_create_media_table.php @@ -30,4 +30,4 @@ return new class extends Migration { Schema::dropIfExists('media'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_05_13_075321_create_companies_table.php b/database/migrations/2026_05_13_075321_create_companies_table.php index 0f9350b..fbf962a 100644 --- a/database/migrations/2026_05_13_075321_create_companies_table.php +++ b/database/migrations/2026_05_13_075321_create_companies_table.php @@ -36,4 +36,4 @@ return new class extends Migration { Schema::dropIfExists('companies'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_05_13_075457_create_company_project_table.php b/database/migrations/2026_05_13_075457_create_company_project_table.php index 91634bf..34b193f 100644 --- a/database/migrations/2026_05_13_075457_create_company_project_table.php +++ b/database/migrations/2026_05_13_075457_create_company_project_table.php @@ -30,4 +30,4 @@ return new class extends Migration { Schema::dropIfExists('company_project'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_06_25_120000_make_inspection_templates_global_with_pivot.php b/database/migrations/2026_06_25_120000_make_inspection_templates_global_with_pivot.php index f4e7e08..b4578dd 100644 --- a/database/migrations/2026_06_25_120000_make_inspection_templates_global_with_pivot.php +++ b/database/migrations/2026_06_25_120000_make_inspection_templates_global_with_pivot.php @@ -32,7 +32,9 @@ return new class extends Migration foreach ($rows as $r) { // Evitar duplicados; si el proyecto ya no existe lo ignoramos $projectExists = DB::table('projects')->where('id', $r->project_id)->exists(); - if (! $projectExists) continue; + if (! $projectExists) { + continue; + } DB::table('inspection_template_project')->updateOrInsert( ['inspection_template_id' => $r->id, 'project_id' => $r->project_id], @@ -46,9 +48,15 @@ return new class extends Migration Schema::table('inspection_templates', function (Blueprint $table) { // 1) FK primero: si no la dropeamos, MySQL rechaza soltar el índice // que la sustenta ("needed in a foreign key constraint"). - try { $table->dropForeign(['phase_id']); } catch (\Throwable $e) { /* sqlite sin FKs / ya inexistente */ } + try { + $table->dropForeign(['phase_id']); + } catch (Throwable $e) { /* sqlite sin FKs / ya inexistente */ + } // 2) Índice después (nombre por convención de Laravel). - try { $table->dropIndex('inspection_templates_phase_id_index'); } catch (\Throwable $e) { /* ya no existe */ } + try { + $table->dropIndex('inspection_templates_phase_id_index'); + } catch (Throwable $e) { /* ya no existe */ + } // 3) Columna al final. $table->dropColumn('phase_id'); }); diff --git a/database/migrations/2026_07_31_114835_create_tasks_table.php b/database/migrations/2026_07_31_114835_create_tasks_table.php index 0c02aa9..f903b0a 100644 --- a/database/migrations/2026_07_31_114835_create_tasks_table.php +++ b/database/migrations/2026_07_31_114835_create_tasks_table.php @@ -48,4 +48,4 @@ return new class extends Migration { Schema::dropIfExists('tasks'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_07_31_114906_create_comments_table.php b/database/migrations/2026_07_31_114906_create_comments_table.php index 456fdaa..7f8f4ab 100644 --- a/database/migrations/2026_07_31_114906_create_comments_table.php +++ b/database/migrations/2026_07_31_114906_create_comments_table.php @@ -30,4 +30,4 @@ return new class extends Migration { Schema::dropIfExists('comments'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_08_24_094858_add_planned_actual_dates_to_phases_table.php b/database/migrations/2026_08_24_094858_add_planned_actual_dates_to_phases_table.php index aa70937..4513649 100644 --- a/database/migrations/2026_08_24_094858_add_planned_actual_dates_to_phases_table.php +++ b/database/migrations/2026_08_24_094858_add_planned_actual_dates_to_phases_table.php @@ -21,4 +21,4 @@ return new class extends Migration $table->dropColumn(['baseline_start', 'baseline_end']); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_08_24_094903_add_planned_actual_dates_to_features_table.php b/database/migrations/2026_08_24_094903_add_planned_actual_dates_to_features_table.php index f012892..2dc5958 100644 --- a/database/migrations/2026_08_24_094903_add_planned_actual_dates_to_features_table.php +++ b/database/migrations/2026_08_24_094903_add_planned_actual_dates_to_features_table.php @@ -24,8 +24,8 @@ return new class extends Migration $table->dropColumn([ 'planned_start', 'planned_end', 'actual_start', 'actual_end', - 'baseline_start', 'baseline_end' + 'baseline_start', 'baseline_end', ]); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_08_24_094906_create_progress_snapshots_table.php b/database/migrations/2026_08_24_094906_create_progress_snapshots_table.php index 71a9b38..fc125e0 100644 --- a/database/migrations/2026_08_24_094906_create_progress_snapshots_table.php +++ b/database/migrations/2026_08_24_094906_create_progress_snapshots_table.php @@ -20,7 +20,7 @@ return new class extends Migration $table->string('method')->default('manual'); // manual|inspection|task|geometry|hybrid $table->json('metadata')->nullable(); // {earned_value, planned_value, actual_cost, spi, cpi, ...} $table->timestamps(); - + // Short index name for MySQL 64-char limit $table->unique(['trackable_type', 'trackable_id', 'snapshot_date'], 'ps_trackable_date_unique'); $table->index(['trackable_type', 'trackable_id', 'snapshot_date'], 'ps_trackable_date_idx'); @@ -32,4 +32,4 @@ return new class extends Migration { Schema::dropIfExists('progress_snapshots'); } -}; \ No newline at end of file +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 9f5e27e..532cbcb 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -22,7 +22,6 @@ class DatabaseSeeder extends Seeder 'email' => 'test@example.com', ]);*/ - $this->call([ RolesAndPermissionsSeeder::class, PermissionCatalogSeeder::class, diff --git a/database/seeders/PermissionCatalogSeeder.php b/database/seeders/PermissionCatalogSeeder.php index 1896bf8..f1d86de 100644 --- a/database/seeders/PermissionCatalogSeeder.php +++ b/database/seeders/PermissionCatalogSeeder.php @@ -19,64 +19,64 @@ class PermissionCatalogSeeder extends Seeder $catalog = [ 'Proyectos' => [ - 'view projects' => 'Ver listado y fichas de proyectos', - 'create projects' => 'Crear proyectos', - 'edit projects' => 'Editar datos del proyecto', - 'delete projects' => 'Eliminar proyectos', - 'export projects' => 'Exportar proyectos (Excel/PDF)', + 'view projects' => 'Ver listado y fichas de proyectos', + 'create projects' => 'Crear proyectos', + 'edit projects' => 'Editar datos del proyecto', + 'delete projects' => 'Eliminar proyectos', + 'export projects' => 'Exportar proyectos (Excel/PDF)', ], 'Fases y progreso' => [ - 'view phases' => 'Ver fases del proyecto', - 'manage phases' => 'Crear, editar, ordenar y eliminar fases', - 'update progress' => 'Actualizar el porcentaje de progreso', + 'view phases' => 'Ver fases del proyecto', + 'manage phases' => 'Crear, editar, ordenar y eliminar fases', + 'update progress' => 'Actualizar el porcentaje de progreso', ], 'Capas y elementos' => [ - 'view layers' => 'Ver capas y elementos en el mapa', - 'upload layers' => 'Subir/importar capas', - 'edit layers' => 'Editar capas y elementos', - 'delete layers' => 'Eliminar capas/elementos', + 'view layers' => 'Ver capas y elementos en el mapa', + 'upload layers' => 'Subir/importar capas', + 'edit layers' => 'Editar capas y elementos', + 'delete layers' => 'Eliminar capas/elementos', ], 'Inspecciones' => [ - 'view inspections' => 'Ver inspecciones e historial', + 'view inspections' => 'Ver inspecciones e historial', 'create inspections' => 'Registrar inspecciones', - 'edit inspections' => 'Editar inspecciones existentes', + 'edit inspections' => 'Editar inspecciones existentes', 'delete inspections' => 'Eliminar inspecciones', - 'manage templates' => 'Gestionar plantillas de inspección', + 'manage templates' => 'Gestionar plantillas de inspección', ], 'Incidencias' => [ - 'view issues' => 'Ver incidencias', - 'create issues' => 'Crear incidencias', - 'edit issues' => 'Editar, resolver y cerrar incidencias', - 'delete issues' => 'Eliminar incidencias', + 'view issues' => 'Ver incidencias', + 'create issues' => 'Crear incidencias', + 'edit issues' => 'Editar, resolver y cerrar incidencias', + 'delete issues' => 'Eliminar incidencias', ], 'Empresas' => [ - 'view companies' => 'Ver empresas', - 'create companies' => 'Crear empresas', - 'edit companies' => 'Editar empresas', - 'delete companies' => 'Eliminar empresas', - 'assign companies' => 'Asignar empresas/roles a proyectos', + 'view companies' => 'Ver empresas', + 'create companies' => 'Crear empresas', + 'edit companies' => 'Editar empresas', + 'delete companies' => 'Eliminar empresas', + 'assign companies' => 'Asignar empresas/roles a proyectos', ], 'Usuarios' => [ - 'view users' => 'Ver usuarios', - 'create users' => 'Crear usuarios', - 'edit users' => 'Editar usuarios', - 'delete users' => 'Eliminar usuarios', - 'assign users' => 'Asignar usuarios/roles a proyectos', + 'view users' => 'Ver usuarios', + 'create users' => 'Crear usuarios', + 'edit users' => 'Editar usuarios', + 'delete users' => 'Eliminar usuarios', + 'assign users' => 'Asignar usuarios/roles a proyectos', ], 'Roles' => [ - 'manage roles' => 'Crear/editar/borrar roles y asignar permisos', + 'manage roles' => 'Crear/editar/borrar roles y asignar permisos', ], 'Informes' => [ - 'view reports' => 'Ver panel de informes', - 'export reports' => 'Exportar informes', + 'view reports' => 'Ver panel de informes', + 'export reports' => 'Exportar informes', ], 'Archivos' => [ - 'view media' => 'Ver archivos/galería', - 'upload media' => 'Subir archivos', - 'delete media' => 'Eliminar archivos', + 'view media' => 'Ver archivos/galería', + 'upload media' => 'Subir archivos', + 'delete media' => 'Eliminar archivos', ], 'General' => [ - 'manage all' => 'Súper-admin: acceso total al sistema', + 'manage all' => 'Súper-admin: acceso total al sistema', ], ]; diff --git a/database/seeders/ProjectExampleSeeder.php b/database/seeders/ProjectExampleSeeder.php index 6bbb7f3..17a7c97 100644 --- a/database/seeders/ProjectExampleSeeder.php +++ b/database/seeders/ProjectExampleSeeder.php @@ -2,13 +2,14 @@ namespace Database\Seeders; -use Illuminate\Database\Seeder; -use App\Models\Project; -use App\Models\Phase; -use App\Models\Layer; use App\Models\Feature; +use App\Models\Inspection; use App\Models\InspectionTemplate; +use App\Models\Layer; +use App\Models\Phase; +use App\Models\Project; use App\Models\User; +use Illuminate\Database\Seeder; class ProjectExampleSeeder extends Seeder { @@ -16,7 +17,7 @@ class ProjectExampleSeeder extends Seeder { // Obtener usuario admin $admin = User::where('email', 'admin@email.com')->first(); - if (!$admin) { + if (! $admin) { $admin = User::first(); } @@ -56,7 +57,7 @@ class ProjectExampleSeeder extends Seeder ['name' => 'progress', 'label' => 'Progreso de vertido', 'type' => 'percentage', 'required' => true], ['name' => 'calidad', 'label' => 'Calidad del hormigón', 'type' => 'select', 'options' => 'Excelente,Bueno,Regular,Deficiente', 'required' => true], ['name' => 'observaciones', 'label' => 'Observaciones', 'type' => 'textarea', 'required' => false], - ] + ], ]); $templateAcero = InspectionTemplate::create([ @@ -65,7 +66,7 @@ class ProjectExampleSeeder extends Seeder 'fields' => [ ['name' => 'progress', 'label' => '% de acero colocado', 'type' => 'percentage', 'required' => true], ['name' => 'diametros', 'label' => 'Diámetros verificados', 'type' => 'text', 'required' => false], - ] + ], ]); // Crear capa (sin geojson_data) @@ -88,8 +89,8 @@ class ProjectExampleSeeder extends Seeder [-3.702, 40.415], [-3.702, 40.418], [-3.705, 40.418], - [-3.705, 40.415] - ]] + [-3.705, 40.415], + ]], ], 'properties' => ['description' => 'Zapata esquina noroeste'], 'template_id' => $templateHormigon->id, @@ -107,8 +108,8 @@ class ProjectExampleSeeder extends Seeder [-3.697, 40.415], [-3.697, 40.418], [-3.700, 40.418], - [-3.700, 40.415] - ]] + [-3.700, 40.415], + ]], ], 'properties' => ['description' => 'Zapata lado este'], 'template_id' => $templateAcero->id, @@ -121,7 +122,7 @@ class ProjectExampleSeeder extends Seeder 'name' => 'Punto de control topográfico', 'geometry' => [ 'type' => 'Point', - 'coordinates' => [-3.703, 40.4165] + 'coordinates' => [-3.703, 40.4165], ], 'properties' => ['tipo' => 'estación total'], 'template_id' => null, @@ -130,7 +131,7 @@ class ProjectExampleSeeder extends Seeder ]); // (Opcional) Crear una inspección de ejemplo - \App\Models\Inspection::create([ + Inspection::create([ 'project_id' => $project->id, 'layer_id' => $layer->id, 'feature_id' => $feature1->id, @@ -139,8 +140,8 @@ class ProjectExampleSeeder extends Seeder 'data' => [ 'progress' => 45, 'calidad' => 'Bueno', - 'observaciones' => 'Vertido completado en un 45%, sin fisuras aparentes.' + 'observaciones' => 'Vertido completado en un 45%, sin fisuras aparentes.', ], ]); } -} \ No newline at end of file +} diff --git a/lang/es/auth.php b/lang/es/auth.php index dc1edad..b59c169 100644 --- a/lang/es/auth.php +++ b/lang/es/auth.php @@ -2,7 +2,7 @@ return [ - 'failed' => 'Las credenciales introducidas no son válidas.', + 'failed' => 'Las credenciales introducidas no son válidas.', 'password' => 'La contraseña indicada es incorrecta.', 'throttle' => 'Demasiados intentos de acceso. Por favor, inténtalo de nuevo en :seconds segundos.', diff --git a/lang/es/pagination.php b/lang/es/pagination.php index 5ad61fc..1058ec8 100644 --- a/lang/es/pagination.php +++ b/lang/es/pagination.php @@ -3,6 +3,6 @@ return [ 'previous' => '« Anterior', - 'next' => 'Siguiente »', + 'next' => 'Siguiente »', ]; diff --git a/lang/es/passwords.php b/lang/es/passwords.php index 471f67e..4fc1451 100644 --- a/lang/es/passwords.php +++ b/lang/es/passwords.php @@ -2,10 +2,10 @@ return [ - 'reset' => 'Tu contraseña ha sido restablecida.', - 'sent' => 'Te hemos enviado un enlace para restablecer tu contraseña.', + 'reset' => 'Tu contraseña ha sido restablecida.', + 'sent' => 'Te hemos enviado un enlace para restablecer tu contraseña.', 'throttled' => 'Por favor, espera antes de volver a intentarlo.', - 'token' => 'Este token de restablecimiento de contraseña no es válido.', - 'user' => 'No encontramos ningún usuario con esa dirección de correo.', + 'token' => 'Este token de restablecimiento de contraseña no es válido.', + 'user' => 'No encontramos ningún usuario con esa dirección de correo.', ]; diff --git a/lang/es/validation.php b/lang/es/validation.php index 00c2fe4..e63768c 100644 --- a/lang/es/validation.php +++ b/lang/es/validation.php @@ -2,159 +2,159 @@ return [ - 'accepted' => 'El campo :attribute debe ser aceptado.', - 'accepted_if' => 'El campo :attribute debe ser aceptado cuando :other es :value.', - 'active_url' => 'El campo :attribute debe ser una URL válida.', - 'after' => 'El campo :attribute debe ser una fecha posterior a :date.', - 'after_or_equal' => 'El campo :attribute debe ser una fecha posterior o igual a :date.', - 'alpha' => 'El campo :attribute solo debe contener letras.', - 'alpha_dash' => 'El campo :attribute solo debe contener letras, números, guiones y guiones bajos.', - 'alpha_num' => 'El campo :attribute solo debe contener letras y números.', - 'any_of' => 'El campo :attribute no es válido.', - 'array' => 'El campo :attribute debe ser un array.', - 'ascii' => 'El campo :attribute solo debe contener caracteres alfanuméricos de un solo byte y símbolos.', - 'before' => 'El campo :attribute debe ser una fecha anterior a :date.', - 'before_or_equal' => 'El campo :attribute debe ser una fecha anterior o igual a :date.', - 'between' => [ - 'array' => 'El campo :attribute debe tener entre :min y :max elementos.', - 'file' => 'El campo :attribute debe estar entre :min y :max kilobytes.', + 'accepted' => 'El campo :attribute debe ser aceptado.', + 'accepted_if' => 'El campo :attribute debe ser aceptado cuando :other es :value.', + 'active_url' => 'El campo :attribute debe ser una URL válida.', + 'after' => 'El campo :attribute debe ser una fecha posterior a :date.', + 'after_or_equal' => 'El campo :attribute debe ser una fecha posterior o igual a :date.', + 'alpha' => 'El campo :attribute solo debe contener letras.', + 'alpha_dash' => 'El campo :attribute solo debe contener letras, números, guiones y guiones bajos.', + 'alpha_num' => 'El campo :attribute solo debe contener letras y números.', + 'any_of' => 'El campo :attribute no es válido.', + 'array' => 'El campo :attribute debe ser un array.', + 'ascii' => 'El campo :attribute solo debe contener caracteres alfanuméricos de un solo byte y símbolos.', + 'before' => 'El campo :attribute debe ser una fecha anterior a :date.', + 'before_or_equal' => 'El campo :attribute debe ser una fecha anterior o igual a :date.', + 'between' => [ + 'array' => 'El campo :attribute debe tener entre :min y :max elementos.', + 'file' => 'El campo :attribute debe estar entre :min y :max kilobytes.', 'numeric' => 'El campo :attribute debe estar entre :min y :max.', - 'string' => 'El campo :attribute debe tener entre :min y :max caracteres.', + 'string' => 'El campo :attribute debe tener entre :min y :max caracteres.', ], - 'boolean' => 'El campo :attribute debe ser verdadero o falso.', - 'can' => 'El campo :attribute contiene un valor no autorizado.', - 'confirmed' => 'La confirmación del campo :attribute no coincide.', - 'contains' => 'Al campo :attribute le falta un valor obligatorio.', - 'current_password' => 'La contraseña es incorrecta.', - 'date' => 'El campo :attribute debe ser una fecha válida.', - 'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.', - 'date_format' => 'El campo :attribute debe coincidir con el formato :format.', - 'decimal' => 'El campo :attribute debe tener :decimal decimales.', - 'declined' => 'El campo :attribute debe ser rechazado.', - 'declined_if' => 'El campo :attribute debe ser rechazado cuando :other es :value.', - 'different' => 'El campo :attribute y :other deben ser diferentes.', - 'digits' => 'El campo :attribute debe tener :digits dígitos.', - 'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.', - 'dimensions' => 'El campo :attribute tiene dimensiones de imagen inválidas.', - 'distinct' => 'El campo :attribute tiene un valor duplicado.', - 'doesnt_contain' => 'El campo :attribute no debe contener ninguno de los siguientes valores: :values.', - 'doesnt_end_with' => 'El campo :attribute no debe terminar con uno de los siguientes valores: :values.', - 'doesnt_start_with' => 'El campo :attribute no debe comenzar con uno de los siguientes valores: :values.', - 'email' => 'El campo :attribute debe ser una dirección de correo válida.', - 'encoding' => 'El campo :attribute debe estar codificado en :encoding.', - 'ends_with' => 'El campo :attribute debe terminar con uno de los siguientes valores: :values.', - 'enum' => 'El :attribute seleccionado no es válido.', - 'exists' => 'El :attribute seleccionado no es válido.', - 'extensions' => 'El campo :attribute debe tener una de las siguientes extensiones: :values.', - 'file' => 'El campo :attribute debe ser un archivo.', - 'filled' => 'El campo :attribute debe tener un valor.', - 'gt' => [ - 'array' => 'El campo :attribute debe tener más de :value elementos.', - 'file' => 'El campo :attribute debe ser mayor que :value kilobytes.', + 'boolean' => 'El campo :attribute debe ser verdadero o falso.', + 'can' => 'El campo :attribute contiene un valor no autorizado.', + 'confirmed' => 'La confirmación del campo :attribute no coincide.', + 'contains' => 'Al campo :attribute le falta un valor obligatorio.', + 'current_password' => 'La contraseña es incorrecta.', + 'date' => 'El campo :attribute debe ser una fecha válida.', + 'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.', + 'date_format' => 'El campo :attribute debe coincidir con el formato :format.', + 'decimal' => 'El campo :attribute debe tener :decimal decimales.', + 'declined' => 'El campo :attribute debe ser rechazado.', + 'declined_if' => 'El campo :attribute debe ser rechazado cuando :other es :value.', + 'different' => 'El campo :attribute y :other deben ser diferentes.', + 'digits' => 'El campo :attribute debe tener :digits dígitos.', + 'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.', + 'dimensions' => 'El campo :attribute tiene dimensiones de imagen inválidas.', + 'distinct' => 'El campo :attribute tiene un valor duplicado.', + 'doesnt_contain' => 'El campo :attribute no debe contener ninguno de los siguientes valores: :values.', + 'doesnt_end_with' => 'El campo :attribute no debe terminar con uno de los siguientes valores: :values.', + 'doesnt_start_with' => 'El campo :attribute no debe comenzar con uno de los siguientes valores: :values.', + 'email' => 'El campo :attribute debe ser una dirección de correo válida.', + 'encoding' => 'El campo :attribute debe estar codificado en :encoding.', + 'ends_with' => 'El campo :attribute debe terminar con uno de los siguientes valores: :values.', + 'enum' => 'El :attribute seleccionado no es válido.', + 'exists' => 'El :attribute seleccionado no es válido.', + 'extensions' => 'El campo :attribute debe tener una de las siguientes extensiones: :values.', + 'file' => 'El campo :attribute debe ser un archivo.', + 'filled' => 'El campo :attribute debe tener un valor.', + 'gt' => [ + 'array' => 'El campo :attribute debe tener más de :value elementos.', + 'file' => 'El campo :attribute debe ser mayor que :value kilobytes.', 'numeric' => 'El campo :attribute debe ser mayor que :value.', - 'string' => 'El campo :attribute debe tener más de :value caracteres.', + 'string' => 'El campo :attribute debe tener más de :value caracteres.', ], - 'gte' => [ - 'array' => 'El campo :attribute debe tener :value elementos o más.', - 'file' => 'El campo :attribute debe ser mayor o igual a :value kilobytes.', + 'gte' => [ + 'array' => 'El campo :attribute debe tener :value elementos o más.', + 'file' => 'El campo :attribute debe ser mayor o igual a :value kilobytes.', 'numeric' => 'El campo :attribute debe ser mayor o igual a :value.', - 'string' => 'El campo :attribute debe tener :value caracteres o más.', + 'string' => 'El campo :attribute debe tener :value caracteres o más.', ], - 'hex_color' => 'El campo :attribute debe ser un color hexadecimal válido.', - 'image' => 'El campo :attribute debe ser una imagen.', - 'in' => 'El :attribute seleccionado no es válido.', - 'in_array' => 'El campo :attribute debe existir en :other.', - 'in_array_keys' => 'El campo :attribute debe contener al menos una de las siguientes claves: :values.', - 'integer' => 'El campo :attribute debe ser un número entero.', - 'ip' => 'El campo :attribute debe ser una dirección IP válida.', - 'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.', - 'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.', - 'json' => 'El campo :attribute debe ser una cadena JSON válida.', - 'list' => 'El campo :attribute debe ser una lista.', - 'lowercase' => 'El campo :attribute debe estar en minúsculas.', - 'lt' => [ - 'array' => 'El campo :attribute debe tener menos de :value elementos.', - 'file' => 'El campo :attribute debe ser menor que :value kilobytes.', + 'hex_color' => 'El campo :attribute debe ser un color hexadecimal válido.', + 'image' => 'El campo :attribute debe ser una imagen.', + 'in' => 'El :attribute seleccionado no es válido.', + 'in_array' => 'El campo :attribute debe existir en :other.', + 'in_array_keys' => 'El campo :attribute debe contener al menos una de las siguientes claves: :values.', + 'integer' => 'El campo :attribute debe ser un número entero.', + 'ip' => 'El campo :attribute debe ser una dirección IP válida.', + 'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.', + 'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.', + 'json' => 'El campo :attribute debe ser una cadena JSON válida.', + 'list' => 'El campo :attribute debe ser una lista.', + 'lowercase' => 'El campo :attribute debe estar en minúsculas.', + 'lt' => [ + 'array' => 'El campo :attribute debe tener menos de :value elementos.', + 'file' => 'El campo :attribute debe ser menor que :value kilobytes.', 'numeric' => 'El campo :attribute debe ser menor que :value.', - 'string' => 'El campo :attribute debe tener menos de :value caracteres.', + 'string' => 'El campo :attribute debe tener menos de :value caracteres.', ], - 'lte' => [ - 'array' => 'El campo :attribute no debe tener más de :value elementos.', - 'file' => 'El campo :attribute debe ser menor o igual a :value kilobytes.', + 'lte' => [ + 'array' => 'El campo :attribute no debe tener más de :value elementos.', + 'file' => 'El campo :attribute debe ser menor o igual a :value kilobytes.', 'numeric' => 'El campo :attribute debe ser menor o igual a :value.', - 'string' => 'El campo :attribute debe tener :value caracteres o menos.', + 'string' => 'El campo :attribute debe tener :value caracteres o menos.', ], - 'mac_address' => 'El campo :attribute debe ser una dirección MAC válida.', - 'max' => [ - 'array' => 'El campo :attribute no debe tener más de :max elementos.', - 'file' => 'El campo :attribute no debe ser mayor que :max kilobytes.', + 'mac_address' => 'El campo :attribute debe ser una dirección MAC válida.', + 'max' => [ + 'array' => 'El campo :attribute no debe tener más de :max elementos.', + 'file' => 'El campo :attribute no debe ser mayor que :max kilobytes.', 'numeric' => 'El campo :attribute no debe ser mayor que :max.', - 'string' => 'El campo :attribute no debe tener más de :max caracteres.', + 'string' => 'El campo :attribute no debe tener más de :max caracteres.', ], - 'max_digits' => 'El campo :attribute no debe tener más de :max dígitos.', - 'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.', - 'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.', - 'min' => [ - 'array' => 'El campo :attribute debe tener al menos :min elementos.', - 'file' => 'El campo :attribute debe tener al menos :min kilobytes.', + 'max_digits' => 'El campo :attribute no debe tener más de :max dígitos.', + 'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.', + 'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.', + 'min' => [ + 'array' => 'El campo :attribute debe tener al menos :min elementos.', + 'file' => 'El campo :attribute debe tener al menos :min kilobytes.', 'numeric' => 'El campo :attribute debe ser al menos :min.', - 'string' => 'El campo :attribute debe tener al menos :min caracteres.', + 'string' => 'El campo :attribute debe tener al menos :min caracteres.', ], - 'min_digits' => 'El campo :attribute debe tener al menos :min dígitos.', - 'missing' => 'El campo :attribute debe estar ausente.', - 'missing_if' => 'El campo :attribute debe estar ausente cuando :other es :value.', - 'missing_unless' => 'El campo :attribute debe estar ausente a menos que :other sea :value.', - 'missing_with' => 'El campo :attribute debe estar ausente cuando :values está presente.', - 'missing_with_all' => 'El campo :attribute debe estar ausente cuando :values están presentes.', - 'multiple_of' => 'El campo :attribute debe ser un múltiplo de :value.', - 'not_in' => 'El :attribute seleccionado no es válido.', - 'not_regex' => 'El formato del campo :attribute no es válido.', - 'numeric' => 'El campo :attribute debe ser un número.', - 'password' => [ - 'letters' => 'El campo :attribute debe contener al menos una letra.', - 'mixed' => 'El campo :attribute debe contener al menos una letra mayúscula y una minúscula.', - 'numbers' => 'El campo :attribute debe contener al menos un número.', - 'symbols' => 'El campo :attribute debe contener al menos un símbolo.', + 'min_digits' => 'El campo :attribute debe tener al menos :min dígitos.', + 'missing' => 'El campo :attribute debe estar ausente.', + 'missing_if' => 'El campo :attribute debe estar ausente cuando :other es :value.', + 'missing_unless' => 'El campo :attribute debe estar ausente a menos que :other sea :value.', + 'missing_with' => 'El campo :attribute debe estar ausente cuando :values está presente.', + 'missing_with_all' => 'El campo :attribute debe estar ausente cuando :values están presentes.', + 'multiple_of' => 'El campo :attribute debe ser un múltiplo de :value.', + 'not_in' => 'El :attribute seleccionado no es válido.', + 'not_regex' => 'El formato del campo :attribute no es válido.', + 'numeric' => 'El campo :attribute debe ser un número.', + 'password' => [ + 'letters' => 'El campo :attribute debe contener al menos una letra.', + 'mixed' => 'El campo :attribute debe contener al menos una letra mayúscula y una minúscula.', + 'numbers' => 'El campo :attribute debe contener al menos un número.', + 'symbols' => 'El campo :attribute debe contener al menos un símbolo.', 'uncompromised' => 'El :attribute proporcionado ha aparecido en una filtración de datos. Elige un :attribute diferente.', ], - 'present' => 'El campo :attribute debe estar presente.', - 'present_if' => 'El campo :attribute debe estar presente cuando :other es :value.', - 'present_unless' => 'El campo :attribute debe estar presente a menos que :other sea :value.', - 'present_with' => 'El campo :attribute debe estar presente cuando :values está presente.', - 'present_with_all' => 'El campo :attribute debe estar presente cuando :values están presentes.', - 'prohibited' => 'El campo :attribute está prohibido.', - 'prohibited_if' => 'El campo :attribute está prohibido cuando :other es :value.', + 'present' => 'El campo :attribute debe estar presente.', + 'present_if' => 'El campo :attribute debe estar presente cuando :other es :value.', + 'present_unless' => 'El campo :attribute debe estar presente a menos que :other sea :value.', + 'present_with' => 'El campo :attribute debe estar presente cuando :values está presente.', + 'present_with_all' => 'El campo :attribute debe estar presente cuando :values están presentes.', + 'prohibited' => 'El campo :attribute está prohibido.', + 'prohibited_if' => 'El campo :attribute está prohibido cuando :other es :value.', 'prohibited_if_accepted' => 'El campo :attribute está prohibido cuando :other es aceptado.', 'prohibited_if_declined' => 'El campo :attribute está prohibido cuando :other es rechazado.', - 'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.', - 'prohibits' => 'El campo :attribute prohíbe que :other esté presente.', - 'regex' => 'El formato del campo :attribute no es válido.', - 'required' => 'El campo :attribute es obligatorio.', - 'required_array_keys' => 'El campo :attribute debe contener entradas para: :values.', - 'required_if' => 'El campo :attribute es obligatorio cuando :other es :value.', + 'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.', + 'prohibits' => 'El campo :attribute prohíbe que :other esté presente.', + 'regex' => 'El formato del campo :attribute no es válido.', + 'required' => 'El campo :attribute es obligatorio.', + 'required_array_keys' => 'El campo :attribute debe contener entradas para: :values.', + 'required_if' => 'El campo :attribute es obligatorio cuando :other es :value.', 'required_if_accepted' => 'El campo :attribute es obligatorio cuando :other es aceptado.', 'required_if_declined' => 'El campo :attribute es obligatorio cuando :other es rechazado.', - 'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.', - 'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.', - 'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.', - 'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.', + 'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.', + 'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.', + 'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.', + 'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.', 'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values está presente.', - 'same' => 'El campo :attribute debe coincidir con :other.', - 'size' => [ - 'array' => 'El campo :attribute debe contener :size elementos.', - 'file' => 'El campo :attribute debe pesar :size kilobytes.', + 'same' => 'El campo :attribute debe coincidir con :other.', + 'size' => [ + 'array' => 'El campo :attribute debe contener :size elementos.', + 'file' => 'El campo :attribute debe pesar :size kilobytes.', 'numeric' => 'El campo :attribute debe ser :size.', - 'string' => 'El campo :attribute debe tener :size caracteres.', + 'string' => 'El campo :attribute debe tener :size caracteres.', ], - 'starts_with' => 'El campo :attribute debe comenzar con uno de los siguientes valores: :values.', - 'string' => 'El campo :attribute debe ser una cadena de texto.', - 'timezone' => 'El campo :attribute debe ser una zona horaria válida.', - 'unique' => 'El :attribute ya está en uso.', - 'uploaded' => 'El campo :attribute no se pudo subir.', - 'uppercase' => 'El campo :attribute debe estar en mayúsculas.', - 'url' => 'El campo :attribute debe ser una URL válida.', - 'ulid' => 'El campo :attribute debe ser un ULID válido.', - 'uuid' => 'El campo :attribute debe ser un UUID válido.', + 'starts_with' => 'El campo :attribute debe comenzar con uno de los siguientes valores: :values.', + 'string' => 'El campo :attribute debe ser una cadena de texto.', + 'timezone' => 'El campo :attribute debe ser una zona horaria válida.', + 'unique' => 'El :attribute ya está en uso.', + 'uploaded' => 'El campo :attribute no se pudo subir.', + 'uppercase' => 'El campo :attribute debe estar en mayúsculas.', + 'url' => 'El campo :attribute debe ser una URL válida.', + 'ulid' => 'El campo :attribute debe ser un ULID válido.', + 'uuid' => 'El campo :attribute debe ser un UUID válido.', 'custom' => [ 'attribute-name' => [ @@ -163,31 +163,31 @@ return [ ], 'attributes' => [ - 'name' => 'nombre', - 'email' => 'correo electrónico', - 'password' => 'contraseña', - 'address' => 'dirección', - 'phone' => 'teléfono', - 'description' => 'descripción', - 'start_date' => 'fecha de inicio', - 'end_date' => 'fecha de fin', - 'end_date_estimated' => 'fecha estimada de fin', - 'reference' => 'referencia', - 'status' => 'estado', - 'type' => 'tipo', - 'color' => 'color', - 'progress_percent' => 'porcentaje de progreso', - 'tax_id' => 'NIF/CIF', - 'country' => 'país', - 'city' => 'ciudad', - 'latitude' => 'latitud', - 'longitude' => 'longitud', - 'logo' => 'logo', - 'avatar' => 'avatar', - 'role' => 'rol', - 'company_id' => 'empresa', - 'current_password' => 'contraseña actual', - 'new_password' => 'nueva contraseña', + 'name' => 'nombre', + 'email' => 'correo electrónico', + 'password' => 'contraseña', + 'address' => 'dirección', + 'phone' => 'teléfono', + 'description' => 'descripción', + 'start_date' => 'fecha de inicio', + 'end_date' => 'fecha de fin', + 'end_date_estimated' => 'fecha estimada de fin', + 'reference' => 'referencia', + 'status' => 'estado', + 'type' => 'tipo', + 'color' => 'color', + 'progress_percent' => 'porcentaje de progreso', + 'tax_id' => 'NIF/CIF', + 'country' => 'país', + 'city' => 'ciudad', + 'latitude' => 'latitud', + 'longitude' => 'longitud', + 'logo' => 'logo', + 'avatar' => 'avatar', + 'role' => 'rol', + 'company_id' => 'empresa', + 'current_password' => 'contraseña actual', + 'new_password' => 'nueva contraseña', 'new_password_confirmation' => 'confirmación de nueva contraseña', ], diff --git a/lang/fr/auth.php b/lang/fr/auth.php index 09d083b..c1a529c 100644 --- a/lang/fr/auth.php +++ b/lang/fr/auth.php @@ -17,4 +17,4 @@ return [ 'password' => 'Le mot de passe fourni est incorrect.', 'throttle' => 'Trop de tentatives de connexion. Veuillez réessayer dans :seconds secondes.', -]; \ No newline at end of file +]; diff --git a/lang/fr/pagination.php b/lang/fr/pagination.php index 1a0c4c2..8eff374 100644 --- a/lang/fr/pagination.php +++ b/lang/fr/pagination.php @@ -16,4 +16,4 @@ return [ 'previous' => '« Précédent', 'next' => 'Suivant »', -]; \ No newline at end of file +]; diff --git a/lang/fr/passwords.php b/lang/fr/passwords.php index 7575924..ab83efc 100644 --- a/lang/fr/passwords.php +++ b/lang/fr/passwords.php @@ -17,6 +17,6 @@ return [ 'sent' => 'Nous vous avons envoyé par e-mail votre lien de réinitialisation de mot de passe.', 'throttled' => 'Veuillez patienter avant de réessayer.', 'token' => 'Ce jeton de réinitialisation de mot de passe est invalide.', - 'user' => "Nous ne trouvons aucun utilisateur avec cette adresse e-mail.", + 'user' => 'Nous ne trouvons aucun utilisateur avec cette adresse e-mail.', -]; \ No newline at end of file +]; diff --git a/lang/fr/validation.php b/lang/fr/validation.php index fe64717..eca6437 100644 --- a/lang/fr/validation.php +++ b/lang/fr/validation.php @@ -191,4 +191,4 @@ return [ 'new_password_confirmation' => 'confirmation du nouveau mot de passe', ], -]; \ No newline at end of file +]; diff --git a/lang/ru/auth.php b/lang/ru/auth.php index 966a0a0..c9610c6 100644 --- a/lang/ru/auth.php +++ b/lang/ru/auth.php @@ -17,4 +17,4 @@ return [ 'password' => 'Предоставленный пароль неверен.', 'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте снова через :seconds секунд.', -]; \ No newline at end of file +]; diff --git a/lang/ru/pagination.php b/lang/ru/pagination.php index 2cd868d..4dd8db1 100644 --- a/lang/ru/pagination.php +++ b/lang/ru/pagination.php @@ -16,4 +16,4 @@ return [ 'previous' => '« Предыдущая', 'next' => 'Следующая »', -]; \ No newline at end of file +]; diff --git a/lang/ru/passwords.php b/lang/ru/passwords.php index 23edb6d..4537028 100644 --- a/lang/ru/passwords.php +++ b/lang/ru/passwords.php @@ -17,6 +17,6 @@ return [ 'sent' => 'Мы отправили ссылку для сброса пароля на вашу электронную почту.', 'throttled' => 'Пожалуйста, подождите перед повторной попыткой.', 'token' => 'Этот токен сброса пароля недействителен.', - 'user' => "Мы не можем найти пользователя с таким адресом электронной почты.", + 'user' => 'Мы не можем найти пользователя с таким адресом электронной почты.', -]; \ No newline at end of file +]; diff --git a/lang/ru/validation.php b/lang/ru/validation.php index f6049e9..3d914c3 100644 --- a/lang/ru/validation.php +++ b/lang/ru/validation.php @@ -191,4 +191,4 @@ return [ 'new_password_confirmation' => 'подтверждение нового пароля', ], -]; \ No newline at end of file +]; diff --git a/resources/utils/countries.php b/resources/utils/countries.php index d8ee4b7..233c08b 100644 --- a/resources/utils/countries.php +++ b/resources/utils/countries.php @@ -1,4 +1,5 @@ 'Afganistán', 'AX' => 'Islas Åland', @@ -249,6 +250,5 @@ $paises = [ 'BQ' => 'Bonaire, San Eustaquio y Saba', 'CW' => 'Curazao', 'MF' => 'San Martín (parte francesa)', - 'SX' => 'San Martín (parte neerlandesa)' + 'SX' => 'San Martín (parte neerlandesa)', ]; -?> \ No newline at end of file diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 1679f1f..0b1364e 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -44,7 +44,11 @@
- {{ $slot }} + @isset($slot) + {{ $slot }} + @else + @yield('content') + @endisset
diff --git a/resources/views/livewire/projects/project-map.blade.php b/resources/views/livewire/projects/project-map.blade.php index 90d8b9f..3575027 100644 --- a/resources/views/livewire/projects/project-map.blade.php +++ b/resources/views/livewire/projects/project-map.blade.php @@ -741,8 +741,13 @@ // Define selectFeature here so Livewire is guaranteed to be available window.selectFeature = function(featureId) { - console.log('selectFeature called with:', featureId); - Livewire.emit('map-select-feature', { featureId: featureId }); + console.log('[ProjectMap] selectFeature called with:', featureId, typeof featureId); + if (typeof Livewire !== 'undefined' && Livewire.emit) { + Livewire.emit('map-select-feature', { featureId: featureId }); + console.log('[ProjectMap] Livewire.emit executed'); + } else { + console.error('[ProjectMap] Livewire not available or emit not a function'); + } }; Livewire.on('layersUpdated', (activeIds) => { diff --git a/resources/views/livewire/reports/report-builder.blade.php b/resources/views/livewire/reports/report-builder.blade.php index 4c32395..d9cfe3e 100644 --- a/resources/views/livewire/reports/report-builder.blade.php +++ b/resources/views/livewire/reports/report-builder.blade.php @@ -1,6 +1,3 @@ -@extends('layouts.app') - -@section('content')
{{-- Header --}}
@@ -175,5 +172,4 @@
@endif
-
-@endsection \ No newline at end of file +
\ No newline at end of file diff --git a/tests/Feature/Api/MobileApiTest.php b/tests/Feature/Api/MobileApiTest.php index 399ed9e..9215dae 100644 --- a/tests/Feature/Api/MobileApiTest.php +++ b/tests/Feature/Api/MobileApiTest.php @@ -3,17 +3,19 @@ namespace Tests\Feature\Api; use App\Models\Feature; +use App\Models\FeatureType; use App\Models\Inspection; use App\Models\InspectionTemplate; use App\Models\Issue; use App\Models\IssueComment; use App\Models\IssueTask; use App\Models\Layer; +use App\Models\Media; use App\Models\Phase; -use App\Models\Project; use App\Models\ProgressUpdate; -use App\Models\User; +use App\Models\Project; use App\Models\SyncLog; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; use Illuminate\Support\Carbon; @@ -38,15 +40,15 @@ class MobileApiTest extends TestCase private function makeProject(?User $member = null): Project { $project = Project::create([ - 'reference' => 'TEST-1', - 'name' => 'Proyecto Test', - 'address' => 'Calle Falsa 123', - 'lat' => 40.0, - 'lng' => -3.0, - 'start_date' => now()->toDateString(), + 'reference' => 'TEST-1', + 'name' => 'Proyecto Test', + 'address' => 'Calle Falsa 123', + 'lat' => 40.0, + 'lng' => -3.0, + 'start_date' => now()->toDateString(), 'end_date_estimated' => now()->addMonths(6)->toDateString(), - 'status' => 'in_progress', - 'created_by' => $member?->id ?? User::factory()->create()->id, + 'status' => 'in_progress', + 'created_by' => $member?->id ?? User::factory()->create()->id, ]); if ($member) { @@ -59,10 +61,10 @@ class MobileApiTest extends TestCase private function makePhase(Project $project, string $name = 'Fase 1'): Phase { return Phase::create([ - 'project_id' => $project->id, - 'name' => $name, - 'order' => 1, - 'color' => '#3b82f6', + 'project_id' => $project->id, + 'name' => $name, + 'order' => 1, + 'color' => '#3b82f6', 'progress_percent' => 0, ]); } @@ -70,10 +72,10 @@ class MobileApiTest extends TestCase private function makeLayer(Phase $phase): Layer { return Layer::create([ - 'project_id' => $phase->project_id, - 'phase_id' => $phase->id, - 'name' => 'Capa 1', - 'color' => '#10b981', + 'project_id' => $phase->project_id, + 'phase_id' => $phase->id, + 'name' => 'Capa 1', + 'color' => '#10b981', 'uploaded_by' => $phase->project->created_by, ]); } @@ -82,10 +84,10 @@ class MobileApiTest extends TestCase { return Feature::create([ 'layer_id' => $layer->id, - 'name' => 'Elemento 1', + 'name' => 'Elemento 1', 'geometry' => ['type' => 'Point', 'coordinates' => [-3.0, 40.0]], 'progress' => 0, - 'status' => 'planned', + 'status' => 'planned', ]); } @@ -96,8 +98,8 @@ class MobileApiTest extends TestCase $user = User::factory()->create(); $res = $this->postJson('/api/v1/login', [ - 'email' => $user->email, - 'password' => 'password', + 'email' => $user->email, + 'password' => 'password', 'device_name' => 'Pixel-Test', ]); @@ -110,8 +112,8 @@ class MobileApiTest extends TestCase $user = User::factory()->create(); $this->postJson('/api/v1/login', [ - 'email' => $user->email, - 'password' => 'incorrecta', + 'email' => $user->email, + 'password' => 'incorrecta', 'device_name' => 'Pixel-Test', ])->assertStatus(422); } @@ -239,7 +241,7 @@ class MobileApiTest extends TestCase $new = $this->makePhase($project, 'Fase nueva'); Sanctum::actingAs($user, ['mobile-sync']); - $res = $this->getJson("/api/v1/projects/{$project->id}/bundle?since=" . urlencode($since->toIso8601String()))->assertOk(); + $res = $this->getJson("/api/v1/projects/{$project->id}/bundle?since=".urlencode($since->toIso8601String()))->assertOk(); $ids = collect($res->json('phases'))->pluck('id'); $this->assertTrue($ids->contains($new->id)); @@ -262,7 +264,7 @@ class MobileApiTest extends TestCase $feature->delete(); // soft delete Sanctum::actingAs($user, ['mobile-sync']); - $res = $this->getJson("/api/v1/projects/{$project->id}/bundle?since=" . urlencode($since->toIso8601String()))->assertOk(); + $res = $this->getJson("/api/v1/projects/{$project->id}/bundle?since=".urlencode($since->toIso8601String()))->assertOk(); $this->assertContains($feature->id, $res->json('deleted.features')); @@ -274,7 +276,7 @@ class MobileApiTest extends TestCase $user = User::factory()->create(); $project = $this->makeProject($user); $tpl = InspectionTemplate::create([ - 'name' => 'Plantilla A', + 'name' => 'Plantilla A', 'fields' => [['name' => 'ok', 'label' => 'OK', 'type' => 'boolean']], ]); $project->inspectionTemplates()->attach($tpl->id); @@ -362,7 +364,7 @@ class MobileApiTest extends TestCase $user->givePermissionTo('update progress'); $project = $this->makeProject($user); $feature = $this->makeFeature($this->makeLayer($this->makePhase($project))); - $type = \App\Models\FeatureType::create(['name' => 'Pilar', 'color' => '#ffffff']); + $type = FeatureType::create(['name' => 'Pilar', 'color' => '#ffffff']); Sanctum::actingAs($user, ['mobile-sync']); $this->postJson('/api/v1/sync', ['operations' => [[ @@ -431,7 +433,7 @@ class MobileApiTest extends TestCase 'file' => UploadedFile::fake()->image('foto.jpg'), ])->assertOk()->assertJsonPath('status', 'duplicate'); - $this->assertEquals(1, \App\Models\Media::where('uuid', $uuid)->count()); + $this->assertEquals(1, Media::where('uuid', $uuid)->count()); } // ── Issue tasks / comments (enriquecimiento incidencias) ───────────────────── @@ -439,10 +441,10 @@ class MobileApiTest extends TestCase private function makeIssue(Project $project, ?User $reporter = null): Issue { return Issue::create([ - 'project_id' => $project->id, - 'title' => 'Incidencia test', - 'status' => 'open', - 'priority' => 'medium', + 'project_id' => $project->id, + 'title' => 'Incidencia test', + 'status' => 'open', + 'priority' => 'medium', 'reported_by' => $reporter?->id ?? $project->created_by, ]); } diff --git a/tests/Feature/FeatureManagementTest.php b/tests/Feature/FeatureManagementTest.php index bc7e465..bf93208 100644 --- a/tests/Feature/FeatureManagementTest.php +++ b/tests/Feature/FeatureManagementTest.php @@ -21,7 +21,9 @@ class FeatureManagementTest extends TestCase use RefreshDatabase; private User $user; + private Project $project; + private Feature $feature; protected function setUp(): void diff --git a/tests/Feature/GlobalTemplatesTest.php b/tests/Feature/GlobalTemplatesTest.php index 280e8f7..c14d6f5 100644 --- a/tests/Feature/GlobalTemplatesTest.php +++ b/tests/Feature/GlobalTemplatesTest.php @@ -24,11 +24,12 @@ class GlobalTemplatesTest extends TestCase private function project(User $owner, string $ref = 'P'): Project { $p = Project::create([ - 'reference' => $ref, 'name' => 'P-' . $ref, 'address' => 'x', 'lat' => 40, 'lng' => -3, + 'reference' => $ref, 'name' => 'P-'.$ref, 'address' => 'x', 'lat' => 40, 'lng' => -3, 'start_date' => now()->toDateString(), 'end_date_estimated' => now()->addMonth()->toDateString(), 'status' => 'in_progress', 'created_by' => $owner->id, ]); $p->users()->attach($owner->id, ['role_in_project' => 'supervisor']); + return $p; } @@ -124,8 +125,8 @@ class GlobalTemplatesTest extends TestCase $admin->givePermissionTo('manage templates'); $csv = "group,name,label,question,type,required,options,min,max,step,help\n" - . "Dimensiones,altura,Altura (m),¿Cumple la cota?,decimal,1,,0,100,0.1,Medir con flexómetro\n" - . "Acabados,ok,¿Acabado?,,boolean,0,,,,,\n"; + ."Dimensiones,altura,Altura (m),¿Cumple la cota?,decimal,1,,0,100,0.1,Medir con flexómetro\n" + ."Acabados,ok,¿Acabado?,,boolean,0,,,,,\n"; $file = UploadedFile::fake()->createWithContent('plantilla.csv', $csv); $cmp = Livewire::actingAs($admin) @@ -159,7 +160,7 @@ class GlobalTemplatesTest extends TestCase $user = User::factory()->create(); $project = $this->project($user, 'M'); - $assigned = InspectionTemplate::create(['name' => 'Asignada', 'fields' => []]); + $assigned = InspectionTemplate::create(['name' => 'Asignada', 'fields' => []]); $unassigned = InspectionTemplate::create(['name' => 'Otra', 'fields' => []]); $project->inspectionTemplates()->attach($assigned->id); diff --git a/tests/Feature/InspectionFormTest.php b/tests/Feature/InspectionFormTest.php index c755d97..8e349b9 100644 --- a/tests/Feature/InspectionFormTest.php +++ b/tests/Feature/InspectionFormTest.php @@ -11,6 +11,8 @@ use App\Models\Media; use App\Models\Phase; use App\Models\Project; use App\Models\User; +use Database\Seeders\PermissionCatalogSeeder; +use Database\Seeders\RolesAndPermissionsSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; @@ -25,8 +27,8 @@ class InspectionFormTest extends TestCase { parent::setUp(); Storage::fake('public'); - $this->seed(\Database\Seeders\RolesAndPermissionsSeeder::class); - $this->seed(\Database\Seeders\PermissionCatalogSeeder::class); + $this->seed(RolesAndPermissionsSeeder::class); + $this->seed(PermissionCatalogSeeder::class); } private function createTestData(): array @@ -100,7 +102,7 @@ class InspectionFormTest extends TestCase 'mediable_type' => Inspection::class, 'mediable_id' => $inspection->id, 'name' => 'original.jpg', - 'file_path' => 'uploads/inspections/' . $inspection->id . '/original.jpg', + 'file_path' => 'uploads/inspections/'.$inspection->id.'/original.jpg', 'file_type' => 'image/jpeg', 'file_extension' => 'jpg', 'file_size' => 1000, @@ -150,7 +152,7 @@ class InspectionFormTest extends TestCase 'mediable_type' => Inspection::class, 'mediable_id' => $inspection->id, 'name' => 'to_delete.jpg', - 'file_path' => 'uploads/inspections/' . $inspection->id . '/to_delete.jpg', + 'file_path' => 'uploads/inspections/'.$inspection->id.'/to_delete.jpg', 'file_type' => 'image/jpeg', 'file_extension' => 'jpg', 'file_size' => 1000, @@ -161,7 +163,7 @@ class InspectionFormTest extends TestCase 'mediable_type' => Inspection::class, 'mediable_id' => $inspection->id, 'name' => 'keep.jpg', - 'file_path' => 'uploads/inspections/' . $inspection->id . '/keep.jpg', + 'file_path' => 'uploads/inspections/'.$inspection->id.'/keep.jpg', 'file_type' => 'image/jpeg', 'file_extension' => 'jpg', 'file_size' => 1000, @@ -233,86 +235,86 @@ class InspectionFormTest extends TestCase } public function test_delete_inspection_removes_inspection_and_media(): void - { - [$user, $project, $feature, $template] = $this->createTestData(); + { + [$user, $project, $feature, $template] = $this->createTestData(); - $inspection = Inspection::create([ - 'project_id' => $project->id, - 'layer_id' => $feature->layer_id, - 'feature_id' => $feature->id, - 'template_id' => $template->id, - 'user_id' => $user->id, - 'inspector_user_id' => $user->id, - 'status' => 'completed', - 'completed_at' => now(), - 'result' => 'pass', - 'notes' => 'Note', - 'data' => ['altura' => '5.0'], - ]); - $photo = Media::create([ - 'mediable_type' => Inspection::class, - 'mediable_id' => $inspection->id, - 'name' => 'test.jpg', - 'file_path' => 'uploads/inspections/' . $inspection->id . '/test.jpg', - 'file_type' => 'image/jpeg', - 'file_extension' => 'jpg', - 'file_size' => 1000, - 'category' => 'image', - 'uploaded_by' => $user->id, - ]); + $inspection = Inspection::create([ + 'project_id' => $project->id, + 'layer_id' => $feature->layer_id, + 'feature_id' => $feature->id, + 'template_id' => $template->id, + 'user_id' => $user->id, + 'inspector_user_id' => $user->id, + 'status' => 'completed', + 'completed_at' => now(), + 'result' => 'pass', + 'notes' => 'Note', + 'data' => ['altura' => '5.0'], + ]); + $photo = Media::create([ + 'mediable_type' => Inspection::class, + 'mediable_id' => $inspection->id, + 'name' => 'test.jpg', + 'file_path' => 'uploads/inspections/'.$inspection->id.'/test.jpg', + 'file_type' => 'image/jpeg', + 'file_extension' => 'jpg', + 'file_size' => 1000, + 'category' => 'image', + 'uploaded_by' => $user->id, + ]); - // Test direct model delete first - $inspection->delete(); - $this->assertSoftDeleted('inspections', ['id' => $inspection->id]); - $this->assertDatabaseMissing('media', ['id' => $photo->id]); - - // Restore for the actual test - $inspection->restore(); - $this->assertDatabaseHas('inspections', ['id' => $inspection->id, 'deleted_at' => null]); + // Test direct model delete first + $inspection->delete(); + $this->assertSoftDeleted('inspections', ['id' => $inspection->id]); + $this->assertDatabaseMissing('media', ['id' => $photo->id]); - // Now test through Livewire component - $inspection2 = Inspection::create([ - 'project_id' => $project->id, - 'layer_id' => $feature->layer_id, - 'feature_id' => $feature->id, - 'template_id' => $template->id, - 'user_id' => $user->id, - 'inspector_user_id' => $user->id, - 'status' => 'completed', - 'completed_at' => now(), - 'result' => 'pass', - 'notes' => 'Note', - 'data' => ['altura' => '5.0'], - ]); - $photo2 = Media::create([ - 'mediable_type' => Inspection::class, - 'mediable_id' => $inspection2->id, - 'name' => 'test2.jpg', - 'file_path' => 'uploads/inspections/' . $inspection2->id . '/test2.jpg', - 'file_type' => 'image/jpeg', - 'file_extension' => 'jpg', - 'file_size' => 1000, - 'category' => 'image', - 'uploaded_by' => $user->id, - ]); + // Restore for the actual test + $inspection->restore(); + $this->assertDatabaseHas('inspections', ['id' => $inspection->id, 'deleted_at' => null]); - $component = Livewire::actingAs($user) - ->test(ProjectMap::class, ['project' => $project]) - ->call('selectFeature', $feature->id); - - // Debug: check if component is instantiated - $component->assertSet('project.id', $project->id); - $component->assertSet('selectedFeature.id', $feature->id); - - // Call deleteInspection and capture events - $component->call('deleteInspection', $inspection2->id); - - // Check if notify was dispatched - $component->assertDispatched('notify', 'Inspección eliminada correctamente'); - - $this->assertSoftDeleted('inspections', ['id' => $inspection2->id]); - $this->assertDatabaseMissing('media', ['id' => $photo2->id]); - } + // Now test through Livewire component + $inspection2 = Inspection::create([ + 'project_id' => $project->id, + 'layer_id' => $feature->layer_id, + 'feature_id' => $feature->id, + 'template_id' => $template->id, + 'user_id' => $user->id, + 'inspector_user_id' => $user->id, + 'status' => 'completed', + 'completed_at' => now(), + 'result' => 'pass', + 'notes' => 'Note', + 'data' => ['altura' => '5.0'], + ]); + $photo2 = Media::create([ + 'mediable_type' => Inspection::class, + 'mediable_id' => $inspection2->id, + 'name' => 'test2.jpg', + 'file_path' => 'uploads/inspections/'.$inspection2->id.'/test2.jpg', + 'file_type' => 'image/jpeg', + 'file_extension' => 'jpg', + 'file_size' => 1000, + 'category' => 'image', + 'uploaded_by' => $user->id, + ]); + + $component = Livewire::actingAs($user) + ->test(ProjectMap::class, ['project' => $project]) + ->call('selectFeature', $feature->id); + + // Debug: check if component is instantiated + $component->assertSet('project.id', $project->id); + $component->assertSet('selectedFeature.id', $feature->id); + + // Call deleteInspection and capture events + $component->call('deleteInspection', $inspection2->id); + + // Check if notify was dispatched + $component->assertDispatched('notify', 'Inspección eliminada correctamente'); + + $this->assertSoftDeleted('inspections', ['id' => $inspection2->id]); + $this->assertDatabaseMissing('media', ['id' => $photo2->id]); + } public function test_delete_inspection_forbidden_without_permission(): void { @@ -342,4 +344,4 @@ class InspectionFormTest extends TestCase $this->assertDatabaseHas('inspections', ['id' => $inspection->id, 'deleted_at' => null]); } -} \ No newline at end of file +} diff --git a/tests/Feature/IssuesEnhancementsTest.php b/tests/Feature/IssuesEnhancementsTest.php index 869a0d6..d57d2e6 100644 --- a/tests/Feature/IssuesEnhancementsTest.php +++ b/tests/Feature/IssuesEnhancementsTest.php @@ -8,7 +8,6 @@ use App\Livewire\Issues\IssueForm; use App\Models\Feature; use App\Models\Issue; use App\Models\IssueChecklistTemplate; -use App\Models\IssueTask; use App\Models\Layer; use App\Models\Phase; use App\Models\Project; @@ -18,8 +17,8 @@ use App\Notifications\IssueStatusChangedNotification; use App\Notifications\IssueTaskAssignedNotification; use App\Notifications\IssueTaskOverdueNotification; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Str; use Livewire\Livewire; use Spatie\Permission\Models\Permission; use Tests\TestCase; @@ -29,7 +28,9 @@ class IssuesEnhancementsTest extends TestCase use RefreshDatabase; private User $user; + private User $assignee; + private Project $project; protected function setUp(): void @@ -44,15 +45,15 @@ class IssuesEnhancementsTest extends TestCase $this->assignee = User::factory()->create(); $this->project = Project::create([ - 'reference' => 'ENH-1', - 'name' => 'Proyecto Enh', - 'address' => 'Calle 1', - 'lat' => 40.0, - 'lng' => -3.0, - 'start_date' => now()->toDateString(), + 'reference' => 'ENH-1', + 'name' => 'Proyecto Enh', + 'address' => 'Calle 1', + 'lat' => 40.0, + 'lng' => -3.0, + 'start_date' => now()->toDateString(), 'end_date_estimated' => now()->addMonths(3)->toDateString(), - 'status' => 'in_progress', - 'created_by' => $this->user->id, + 'status' => 'in_progress', + 'created_by' => $this->user->id, ]); $this->project->users()->attach($this->user->id, ['role_in_project' => 'supervisor']); $this->project->users()->attach($this->assignee->id, ['role_in_project' => 'worker']); @@ -61,10 +62,10 @@ class IssuesEnhancementsTest extends TestCase private function makeIssue(array $attrs = []): Issue { return Issue::create(array_merge([ - 'project_id' => $this->project->id, - 'title' => 'Incidencia enh', - 'status' => 'open', - 'priority' => 'medium', + 'project_id' => $this->project->id, + 'title' => 'Incidencia enh', + 'status' => 'open', + 'priority' => 'medium', 'reported_by' => $this->user->id, ], $attrs)); } @@ -79,6 +80,7 @@ class IssuesEnhancementsTest extends TestCase 'project_id' => $this->project->id, 'phase_id' => $phase->id, 'name' => 'L', 'color' => '#111', 'uploaded_by' => $this->user->id, ]); + return Feature::create([ 'layer_id' => $layer->id, 'name' => 'Muro norte', 'geometry' => ['type' => 'Point', 'coordinates' => [-3.0, 40.0]], @@ -148,8 +150,8 @@ class IssuesEnhancementsTest extends TestCase $issue = $this->makeIssue(); $template = IssueChecklistTemplate::create([ 'project_id' => $this->project->id, - 'name' => 'Plantilla', - 'items' => ['Picar', 'Sellar', 'Pintar'], + 'name' => 'Plantilla', + 'items' => ['Picar', 'Sellar', 'Pintar'], ]); Livewire::actingAs($this->user) @@ -168,11 +170,11 @@ class IssuesEnhancementsTest extends TestCase Notification::fake(); $issue = $this->makeIssue(); $task = $issue->tasks()->create([ - 'title' => 'Tarea vencida', + 'title' => 'Tarea vencida', 'assigned_to' => $this->assignee->id, - 'due_date' => now()->subDays(2)->toDateString(), - 'is_done' => false, - 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'due_date' => now()->subDays(2)->toDateString(), + 'is_done' => false, + 'uuid' => (string) Str::uuid(), ]); $this->artisan('issues:notify-overdue')->assertSuccessful(); diff --git a/tests/Feature/IssuesTablePageTest.php b/tests/Feature/IssuesTablePageTest.php index 530374f..4f70285 100644 --- a/tests/Feature/IssuesTablePageTest.php +++ b/tests/Feature/IssuesTablePageTest.php @@ -26,23 +26,23 @@ class IssuesTablePageTest extends TestCase $user->givePermissionTo(['view issues', 'edit issues', 'delete issues']); $project = Project::create([ - 'reference' => 'TBL-1', - 'name' => 'Proyecto Tabla', - 'address' => 'Calle Falsa 123', - 'lat' => 40.0, - 'lng' => -3.0, - 'start_date' => now()->toDateString(), + 'reference' => 'TBL-1', + 'name' => 'Proyecto Tabla', + 'address' => 'Calle Falsa 123', + 'lat' => 40.0, + 'lng' => -3.0, + 'start_date' => now()->toDateString(), 'end_date_estimated' => now()->addMonths(6)->toDateString(), - 'status' => 'in_progress', - 'created_by' => $user->id, + 'status' => 'in_progress', + 'created_by' => $user->id, ]); $project->users()->attach($user->id, ['role_in_project' => 'supervisor']); $issue = Issue::create([ - 'project_id' => $project->id, - 'title' => 'Grieta en muro', - 'status' => 'open', - 'priority' => 'high', + 'project_id' => $project->id, + 'title' => 'Grieta en muro', + 'status' => 'open', + 'priority' => 'high', 'reported_by' => $user->id, ]); diff --git a/tests/Feature/MapTablesTest.php b/tests/Feature/MapTablesTest.php index e4793ee..f00150b 100644 --- a/tests/Feature/MapTablesTest.php +++ b/tests/Feature/MapTablesTest.php @@ -19,7 +19,9 @@ class MapTablesTest extends TestCase use RefreshDatabase; private User $user; + private Project $project; + private Feature $feature; protected function setUp(): void @@ -84,12 +86,12 @@ class MapTablesTest extends TestCase { Inspection::create([ 'project_id' => $this->project->id, - 'layer_id' => $this->feature->layer_id, + 'layer_id' => $this->feature->layer_id, 'feature_id' => $this->feature->id, - 'user_id' => $this->user->id, - 'data' => ['ok' => true], - 'status' => 'completed', - 'result' => 'pass', + 'user_id' => $this->user->id, + 'data' => ['ok' => true], + 'status' => 'completed', + 'result' => 'pass', ]); Livewire::actingAs($this->user) diff --git a/tests/Feature/PhaseManagementTest.php b/tests/Feature/PhaseManagementTest.php index 4cd66ec..a09fd28 100644 --- a/tests/Feature/PhaseManagementTest.php +++ b/tests/Feature/PhaseManagementTest.php @@ -4,7 +4,6 @@ namespace Tests\Feature; use App\Livewire\Phases\PhaseList; use App\Livewire\Phases\PhaseTable; -use App\Models\Phase; use App\Models\Project; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -17,6 +16,7 @@ class PhaseManagementTest extends TestCase use RefreshDatabase; private User $user; + private Project $project; protected function setUp(): void @@ -30,15 +30,15 @@ class PhaseManagementTest extends TestCase $this->user->givePermissionTo(['manage phases', 'edit projects']); $this->project = Project::create([ - 'reference' => 'PH-1', - 'name' => 'Proyecto Fases', - 'address' => 'Calle 1', - 'lat' => 40.0, - 'lng' => -3.0, - 'start_date' => now()->toDateString(), + 'reference' => 'PH-1', + 'name' => 'Proyecto Fases', + 'address' => 'Calle 1', + 'lat' => 40.0, + 'lng' => -3.0, + 'start_date' => now()->toDateString(), 'end_date_estimated' => now()->addMonths(3)->toDateString(), - 'status' => 'in_progress', - 'created_by' => $this->user->id, + 'status' => 'in_progress', + 'created_by' => $this->user->id, ]); $this->project->users()->attach($this->user->id, ['role_in_project' => 'supervisor']); } @@ -59,10 +59,10 @@ class PhaseManagementTest extends TestCase ->assertHasNoErrors(); $this->assertDatabaseHas('phases', [ - 'project_id' => $this->project->id, - 'name' => 'Cimentación', - 'color' => '#ff0000', - 'order' => 2, + 'project_id' => $this->project->id, + 'name' => 'Cimentación', + 'color' => '#ff0000', + 'order' => 2, 'progress_percent' => 30, ]); } diff --git a/tests/Feature/ProjectAssignmentsTest.php b/tests/Feature/ProjectAssignmentsTest.php index ccdc70a..670d486 100644 --- a/tests/Feature/ProjectAssignmentsTest.php +++ b/tests/Feature/ProjectAssignmentsTest.php @@ -19,6 +19,7 @@ class ProjectAssignmentsTest extends TestCase use RefreshDatabase; private User $admin; + private Project $project; protected function setUp(): void diff --git a/tests/Feature/SpatialFileConverterTest.php b/tests/Feature/SpatialFileConverterTest.php index a72ae7d..f07ea73 100644 --- a/tests/Feature/SpatialFileConverterTest.php +++ b/tests/Feature/SpatialFileConverterTest.php @@ -12,6 +12,7 @@ class SpatialFileConverterTest extends TestCase { $tmp = tempnam(sys_get_temp_dir(), 'kml_'); file_put_contents($tmp, $content); + return new UploadedFile($tmp, $name, 'application/vnd.google-earth.kml+xml', null, true); } diff --git a/tests/Feature/UserLocaleTest.php b/tests/Feature/UserLocaleTest.php index efda61f..6e527ff 100644 --- a/tests/Feature/UserLocaleTest.php +++ b/tests/Feature/UserLocaleTest.php @@ -24,6 +24,7 @@ class UserLocaleTest extends TestCase $admin = User::factory()->create(); $admin->givePermissionTo(['create users', 'edit users']); + return $admin; } @@ -56,10 +57,10 @@ class UserLocaleTest extends TestCase $admin = $this->admin(); $company = $this->company(); $target = User::factory()->create([ - 'locale' => 'es', + 'locale' => 'es', 'company_id' => $company->id, 'first_name' => 'Bob', - 'last_name' => 'Stone', + 'last_name' => 'Stone', ]); $target->assignRole('Tecnico');