'', 'description' => '', 'phase_id' => null, 'fields' => [], ]; // ── Importar desde CSV/Excel ─────────────────────────────────────────── public $showImportFileModal = false; public $importFile = null; public $importPreviewFields = []; public $importTemplateName = ''; public $importError = ''; // ── Importar desde otro proyecto ────────────────────────────────────── public $showImportProjectModal = false; public $availableProjects = []; public $importProjectId = null; public $importableTemplates = []; public $selectedImportTemplateIds = []; public $fieldTypes = [ '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', ]; public function mount(Project $project) { $this->project = $project; $this->loadPhases(); $this->loadTemplates(); } public function loadPhases() { $this->phases = $this->project->phases()->orderBy('name')->get(); } public function loadTemplates() { $this->templates = InspectionTemplate::where('project_id', $this->project->id) ->with('phase') ->get(); } // ── Formulario manual ───────────────────────────────────────────────── public function newTemplate() { $this->resetForm(); $this->showForm = true; } public function editTemplate($id) { $template = InspectionTemplate::findOrFail($id); $this->form = [ 'name' => $template->name, 'description' => $template->description ?? '', 'phase_id' => $template->phase_id, 'fields' => $template->fields ?? [], ]; $this->editingTemplate = $id; $this->showForm = true; } public function cancelForm() { $this->showForm = false; $this->resetForm(); } public function resetForm() { $this->form = [ 'name' => '', 'description' => '', 'phase_id' => null, 'fields' => [], ]; $this->editingTemplate = null; } public function addField() { $this->form['fields'][] = [ 'name' => '', 'label' => '', 'type' => 'text', 'options' => '', 'required' => false, 'min' => null, 'max' => null, 'step' => null, ]; } public function removeField($index) { unset($this->form['fields'][$index]); $this->form['fields'] = array_values($this->form['fields']); } public function saveTemplate() { $this->validate([ 'form.name' => 'required|string|max:255', 'form.phase_id' => 'nullable|exists:phases,id', 'form.fields' => 'array', ]); $data = [ 'name' => $this->form['name'], 'description' => $this->form['description'], 'project_id' => $this->project->id, 'phase_id' => $this->form['phase_id'] ?: null, 'fields' => array_values($this->form['fields']), ]; if ($this->editingTemplate) { InspectionTemplate::findOrFail($this->editingTemplate)->update($data); $this->dispatch('notify', 'Template actualizado correctamente'); } else { InspectionTemplate::create($data); $this->dispatch('notify', 'Template creado correctamente'); } $this->cancelForm(); $this->loadTemplates(); } public function deleteTemplate($id) { InspectionTemplate::findOrFail($id)->delete(); $this->loadTemplates(); $this->dispatch('notify', 'Template eliminado'); } // ── Exportar template a CSV ──────────────────────────────────────────── public function exportTemplate($id) { $template = InspectionTemplate::findOrFail($id); $rows = []; $rows[] = ['name', 'label', 'type', 'required', 'options', 'min', 'max', 'step']; foreach ($template->fields as $field) { $rows[] = [ $field['name'] ?? '', $field['label'] ?? '', $field['type'] ?? 'text', ($field['required'] ?? false) ? '1' : '0', $field['options'] ?? '', $field['min'] ?? '', $field['max'] ?? '', $field['step'] ?? '', ]; } $filename = preg_replace('/[^a-z0-9_\-]/i', '_', $template->name) . '.csv'; return response()->streamDownload(function () use ($rows) { $out = fopen('php://output', 'w'); // BOM para Excel con UTF-8 fwrite($out, "\xEF\xBB\xBF"); foreach ($rows as $row) { fputcsv($out, $row); } fclose($out); }, $filename, ['Content-Type' => 'text/csv; charset=UTF-8']); } public function downloadExampleCsv() { $rows = [ ['name', 'label', 'type', 'required', 'options', 'min', 'max', 'step'], ['altura_viga', 'Altura de viga (mm)', 'decimal', '1', '', '0', '2000', '1'], ['estado_armado', 'Estado del armado', 'select', '1', 'Conforme,No conforme,Obs.', '', '', ''], ['fotos_tomadas', '¿Fotos tomadas?', 'boolean', '1', '', '', '', ''], ['observaciones', 'Observaciones generales','textarea', '0', '', '', '', ''], ['fecha_visita', 'Fecha de visita', 'date', '0', '', '', '', ''], ['avance_pct', 'Avance medido (%)', 'percentage', '0', '', '0', '100', '1'], ]; return response()->streamDownload(function () use ($rows) { $out = fopen('php://output', 'w'); fwrite($out, "\xEF\xBB\xBF"); foreach ($rows as $row) { fputcsv($out, $row); } fclose($out); }, 'ejemplo-template.csv', ['Content-Type' => 'text/csv; charset=UTF-8']); } // ── Importar desde CSV / Excel ───────────────────────────────────────── public function openImportFileModal() { $this->importFile = null; $this->importPreviewFields = []; $this->importTemplateName = ''; $this->importError = ''; $this->showImportFileModal = true; } public function parseImportFile() { $this->importError = ''; $this->validate([ 'importFile' => 'required|file|mimes:csv,txt,xlsx,xls|max:5120', 'importTemplateName' => 'required|string|max:255', ], [ 'importFile.required' => 'Selecciona un archivo.', 'importFile.mimes' => 'Solo se aceptan archivos CSV o Excel (xlsx/xls).', 'importTemplateName.required' => 'Escribe un nombre para el template.', ]); try { $rows = $this->readFileRows(); } catch (\Throwable $e) { $this->importError = 'No se pudo leer el archivo: ' . $e->getMessage(); return; } $fields = $this->parseRows($rows); if (empty($fields)) { $this->importError = 'No se encontraron filas de datos válidas. Revisa que el archivo tenga el formato correcto.'; return; } $this->importPreviewFields = $fields; $this->dispatch('notify', count($fields) . ' campos detectados. Revisa la vista previa.'); } public function confirmImportFile() { if (empty($this->importPreviewFields) || empty($this->importTemplateName)) return; InspectionTemplate::create([ 'name' => $this->importTemplateName, 'description' => 'Importado desde archivo', 'project_id' => $this->project->id, 'phase_id' => null, 'fields' => array_values($this->importPreviewFields), ]); $this->showImportFileModal = false; $this->importPreviewFields = []; $this->importTemplateName = ''; $this->importFile = null; $this->loadTemplates(); $this->dispatch('notify', 'Template importado correctamente desde archivo'); } private function readFileRows(): array { $ext = strtolower($this->importFile->getClientOriginalExtension()); $path = $this->importFile->getRealPath(); if ($ext === 'xlsx' || $ext === 'xls') { $spreadsheet = IOFactory::load($path); $sheet = $spreadsheet->getActiveSheet(); $rows = $sheet->toArray(null, true, true, false); array_shift($rows); // quitar cabecera return array_filter($rows, fn($r) => !empty($r[0])); } // CSV / TXT $rows = []; $handle = fopen($path, 'r'); // Detectar y descartar BOM UTF-8 $bom = fread($handle, 3); if ($bom !== "\xEF\xBB\xBF") rewind($handle); fgetcsv($handle); // cabecera while (($row = fgetcsv($handle)) !== false) { if (!empty($row[0])) $rows[] = $row; } fclose($handle); return $rows; } private function parseRows(array $rows): array { $fields = []; foreach ($rows as $row) { $row = array_values((array) $row); $rawName = trim($row[0] ?? ''); if ($rawName === '') continue; $fields[] = [ 'name' => $this->slugify($rawName), 'label' => trim($row[1] ?? $rawName), 'type' => $this->normalizeType($row[2] ?? 'text'), 'required' => in_array(strtolower(trim($row[3] ?? '0')), ['1', 'si', 'sí', 'yes', 'true']), 'options' => trim($row[4] ?? ''), 'min' => $row[5] !== '' && $row[5] !== null ? $row[5] : null, 'max' => $row[6] !== '' && $row[6] !== null ? $row[6] : null, 'step' => $row[7] !== '' && $row[7] !== null ? $row[7] : null, ]; } return $fields; } private function slugify(string $str): string { $str = mb_strtolower(trim($str)); $str = preg_replace('/\s+/', '_', $str); $str = preg_replace('/[^a-z0-9_]/i', '', $str); return trim($str, '_') ?: 'campo'; } private function normalizeType(string $type): string { $map = [ 'texto' => 'text', 'text' => 'text', 'string' => 'text', 'corto' => 'text', 'textarea' => 'textarea', 'largo' => 'textarea', 'parrafo' => 'textarea', 'integer' => 'integer', 'entero' => 'integer', 'int' => 'integer', 'numero' => 'integer', 'decimal' => 'decimal', 'float' => 'decimal', 'number' => 'decimal', 'numerico' => 'decimal', 'percentage' => 'percentage', 'porcentaje' => 'percentage', 'pct' => 'percentage', '%' => 'percentage', 'boolean' => 'boolean', 'bool' => 'boolean', 'checkbox' => 'boolean', 'sino' => 'boolean', 'si/no' => 'boolean', 'date' => 'date', 'fecha' => 'date', 'select' => 'select', 'lista' => 'select', 'dropdown' => 'select', 'opciones' => 'select', ]; return $map[strtolower(trim($type))] ?? 'text'; } // ── Importar desde otro proyecto ────────────────────────────────────── public function openImportProjectModal() { $user = Auth::user(); $this->availableProjects = Project::accessibleBy($user) ->where('id', '!=', $this->project->id) ->orderBy('name') ->get(); $this->importProjectId = null; $this->importableTemplates = []; $this->selectedImportTemplateIds = []; $this->showImportProjectModal = true; } public function updatedImportProjectId() { $this->selectedImportTemplateIds = []; if (!$this->importProjectId) { $this->importableTemplates = []; return; } // Solo mostrar templates de proyectos accesibles $user = Auth::user(); $allowed = Project::accessibleBy($user)->pluck('id'); if (!$allowed->contains($this->importProjectId)) { $this->importableTemplates = []; return; } $this->importableTemplates = InspectionTemplate::where('project_id', $this->importProjectId)->get(); } public function importFromProject() { if (empty($this->selectedImportTemplateIds)) { $this->dispatch('notify', 'Selecciona al menos un template.'); return; } // Verificar que los templates pertenecen a un proyecto accesible $user = Auth::user(); $allowed = Project::accessibleBy($user)->pluck('id'); $imported = 0; foreach ($this->selectedImportTemplateIds as $templateId) { $source = InspectionTemplate::find($templateId); if (!$source || !$allowed->contains($source->project_id)) continue; // Evitar duplicados por nombre $name = $source->name; if (InspectionTemplate::where('project_id', $this->project->id)->where('name', $name)->exists()) { $name .= ' (copia)'; } InspectionTemplate::create([ 'name' => $name, 'description' => $source->description, 'project_id' => $this->project->id, 'phase_id' => null, 'fields' => $source->fields, ]); $imported++; } $this->showImportProjectModal = false; $this->importProjectId = null; $this->importableTemplates = []; $this->selectedImportTemplateIds = []; $this->loadTemplates(); $this->dispatch('notify', "$imported template(s) importado(s) desde otro proyecto"); } public function render() { return view('livewire.template-manager'); } }