'', 'description' => '', 'fields' => [], ]; // ── Importar desde CSV/Excel ─────────────────────────────────────────── 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', 'percentage' => 'Porcentaje (0-100)', 'boolean' => 'Sí/No (checkbox)', 'date' => 'Fecha', 'select' => 'Lista desplegable', ]; public function mount() { abort_unless(Auth::user()->can('manage templates'), 403); $this->loadTemplates(); } public function loadTemplates() { // Catálogo global: todas las plantillas. $this->templates = InspectionTemplate::orderBy('name')->get(); } public function newTemplate() { $this->resetForm(); $this->showForm = true; } #[On('template-edit')] public function editTemplate($id) { $template = InspectionTemplate::findOrFail($id); $this->form = [ 'name' => $template->name, 'description' => $template->description ?? '', 'fields' => $template->fields ?? [], ]; $this->editingTemplate = $id; $this->showForm = true; } public function cancelForm() { $this->showForm = false; $this->resetForm(); } public function resetForm() { $this->form = [ 'name' => '', 'description' => '', 'fields' => [], ]; $this->editingTemplate = null; } public function addField() { $this->form['fields'][] = [ 'group' => '', 'name' => '', 'label' => '', 'question' => '', 'type' => 'text', 'options' => '', 'required' => false, 'min' => null, 'max' => null, 'step' => null, 'help' => '', ]; } 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.fields' => 'array', ]); $data = [ 'name' => $this->form['name'], 'description' => $this->form['description'], 'project_id' => null, 'fields' => array_values($this->form['fields']), ]; if ($this->editingTemplate) { InspectionTemplate::findOrFail($this->editingTemplate)->update($data); $this->dispatch('notify', 'Plantilla actualizada'); } else { InspectionTemplate::create($data); $this->dispatch('notify', 'Plantilla creada'); } $this->cancelForm(); $this->loadTemplates(); $this->dispatch('templates-changed'); } #[On('template-delete')] public function deleteTemplate($id) { InspectionTemplate::findOrFail($id)->delete(); $this->loadTemplates(); $this->dispatch('templates-changed'); $this->dispatch('notify', 'Plantilla eliminada'); } // ── Importar desde CSV/Excel ─────────────────────────────────────────── public function openImportFileModal() { $this->importFile = null; $this->importPreviewFields = []; $this->importTemplateName = ''; $this->importError = ''; $this->showImportFileModal = true; } public function downloadExampleCsv() { $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); } public function parseImportFile() { $this->importError = ''; $this->validate([ '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(); return; } $fields = $this->parseRows($rows); if (empty($fields)) { $this->importError = 'No se encontraron filas válidas.'; return; } $this->importPreviewFields = $fields; } public function confirmImportFile() { if (empty($this->importPreviewFields) || empty($this->importTemplateName)) { return; } InspectionTemplate::create([ 'name' => $this->importTemplateName, 'description' => 'Importado desde archivo', 'project_id' => null, 'fields' => array_values($this->importPreviewFields), ]); $this->showImportFileModal = false; $this->importPreviewFields = []; $this->importTemplateName = ''; $this->importFile = null; $this->loadTemplates(); $this->dispatch('templates-changed'); $this->dispatch('notify', 'Plantilla importada'); } private function readFileRows(): array { $ext = strtolower($this->importFile->getClientOriginalExtension()); $path = $this->importFile->getRealPath(); // Fila no vacía = tiene al menos una celda con contenido (no filtramos por // la primera columna, que ahora es `group` y puede ir vacía). parseRows() // ya descarta las filas sin `name`. $notEmpty = fn ($r) => count(array_filter((array) $r, fn ($c) => trim((string) $c) !== '')) > 0; if ($ext === 'xlsx' || $ext === 'xls') { $spreadsheet = IOFactory::load($path); $sheet = $spreadsheet->getActiveSheet(); $rows = $sheet->toArray(null, true, true, false); array_shift($rows); return array_values(array_filter($rows, $notEmpty)); } $rows = []; $handle = fopen($path, 'r'); $bom = fread($handle, 3); if ($bom !== "\xEF\xBB\xBF") { rewind($handle); } fgetcsv($handle); while (($row = fgetcsv($handle)) !== false) { if ($notEmpty($row)) { $rows[] = $row; } } fclose($handle); return $rows; } private function parseRows(array $rows): array { // Orden de columnas: // group, name, label, question, type, required, options, min, max, step, help $fields = []; foreach ($rows as $row) { $row = array_values((array) $row); $rawName = trim($row[1] ?? ''); if ($rawName === '') { continue; } $fields[] = [ 'group' => trim($row[0] ?? ''), 'name' => $this->slugify($rawName), 'label' => trim($row[2] ?? '') ?: $rawName, 'question' => trim($row[3] ?? ''), '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] ?? ''), ]; } 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'; } public function render() { return view('livewire.inspections.global-template-manager'); } }