2026-05-07 23:31:33 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Livewire;
|
|
|
|
|
|
|
|
|
|
use Livewire\Component;
|
2026-06-17 10:23:29 +02:00
|
|
|
use Livewire\WithFileUploads;
|
2026-05-07 23:31:33 +02:00
|
|
|
use App\Models\InspectionTemplate;
|
|
|
|
|
use App\Models\Project;
|
2026-05-11 15:28:16 +02:00
|
|
|
use App\Models\Phase;
|
2026-06-16 18:05:53 +02:00
|
|
|
use Illuminate\Support\Facades\Auth;
|
2026-06-17 10:23:29 +02:00
|
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
2026-05-07 23:31:33 +02:00
|
|
|
|
|
|
|
|
class TemplateManager extends Component
|
|
|
|
|
{
|
2026-06-17 10:23:29 +02:00
|
|
|
use WithFileUploads;
|
|
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
public $project;
|
|
|
|
|
public $templates;
|
2026-05-11 15:28:16 +02:00
|
|
|
public $phases;
|
2026-06-17 10:23:29 +02:00
|
|
|
|
|
|
|
|
// ── Formulario principal ───────────────────────────────────────────────
|
2026-05-07 23:31:33 +02:00
|
|
|
public $editingTemplate = null;
|
2026-06-17 10:23:29 +02:00
|
|
|
public $showForm = false;
|
2026-05-07 23:31:33 +02:00
|
|
|
public $form = [
|
2026-06-17 10:23:29 +02:00
|
|
|
'name' => '',
|
2026-05-07 23:31:33 +02:00
|
|
|
'description' => '',
|
2026-06-17 10:23:29 +02:00
|
|
|
'phase_id' => null,
|
|
|
|
|
'fields' => [],
|
2026-05-07 23:31:33 +02:00
|
|
|
];
|
2026-06-17 10:23:29 +02:00
|
|
|
|
|
|
|
|
// ── 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 = [];
|
|
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
public $fieldTypes = [
|
2026-06-17 10:23:29 +02:00
|
|
|
'text' => 'Texto corto',
|
|
|
|
|
'textarea' => 'Texto largo',
|
|
|
|
|
'integer' => 'Número entero',
|
|
|
|
|
'decimal' => 'Número decimal',
|
2026-05-07 23:31:33 +02:00
|
|
|
'percentage' => 'Porcentaje (0-100)',
|
2026-06-17 10:23:29 +02:00
|
|
|
'boolean' => 'Sí/No (checkbox)',
|
|
|
|
|
'date' => 'Fecha',
|
|
|
|
|
'select' => 'Lista desplegable',
|
2026-05-07 23:31:33 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public function mount(Project $project)
|
|
|
|
|
{
|
|
|
|
|
$this->project = $project;
|
2026-05-11 15:28:16 +02:00
|
|
|
$this->loadPhases();
|
2026-05-07 23:31:33 +02:00
|
|
|
$this->loadTemplates();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:28:16 +02:00
|
|
|
public function loadPhases()
|
|
|
|
|
{
|
|
|
|
|
$this->phases = $this->project->phases()->orderBy('name')->get();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
public function loadTemplates()
|
|
|
|
|
{
|
2026-06-17 10:23:29 +02:00
|
|
|
$this->templates = InspectionTemplate::where('project_id', $this->project->id)
|
|
|
|
|
->with('phase')
|
|
|
|
|
->get();
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-17 10:23:29 +02:00
|
|
|
// ── Formulario manual ─────────────────────────────────────────────────
|
|
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
public function newTemplate()
|
|
|
|
|
{
|
|
|
|
|
$this->resetForm();
|
|
|
|
|
$this->showForm = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function editTemplate($id)
|
|
|
|
|
{
|
2026-06-17 10:23:29 +02:00
|
|
|
$template = InspectionTemplate::findOrFail($id);
|
|
|
|
|
$this->form = [
|
|
|
|
|
'name' => $template->name,
|
|
|
|
|
'description' => $template->description ?? '',
|
|
|
|
|
'phase_id' => $template->phase_id,
|
|
|
|
|
'fields' => $template->fields ?? [],
|
|
|
|
|
];
|
2026-05-07 23:31:33 +02:00
|
|
|
$this->editingTemplate = $id;
|
|
|
|
|
$this->showForm = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function cancelForm()
|
|
|
|
|
{
|
|
|
|
|
$this->showForm = false;
|
|
|
|
|
$this->resetForm();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function resetForm()
|
|
|
|
|
{
|
|
|
|
|
$this->form = [
|
2026-06-17 10:23:29 +02:00
|
|
|
'name' => '',
|
2026-05-07 23:31:33 +02:00
|
|
|
'description' => '',
|
2026-06-17 10:23:29 +02:00
|
|
|
'phase_id' => null,
|
|
|
|
|
'fields' => [],
|
2026-05-07 23:31:33 +02:00
|
|
|
];
|
|
|
|
|
$this->editingTemplate = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function addField()
|
|
|
|
|
{
|
|
|
|
|
$this->form['fields'][] = [
|
2026-06-17 10:23:29 +02:00
|
|
|
'name' => '',
|
|
|
|
|
'label' => '',
|
|
|
|
|
'type' => 'text',
|
|
|
|
|
'options' => '',
|
2026-05-07 23:31:33 +02:00
|
|
|
'required' => false,
|
2026-06-17 10:23:29 +02:00
|
|
|
'min' => null,
|
|
|
|
|
'max' => null,
|
|
|
|
|
'step' => null,
|
2026-05-07 23:31:33 +02:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function removeField($index)
|
|
|
|
|
{
|
|
|
|
|
unset($this->form['fields'][$index]);
|
|
|
|
|
$this->form['fields'] = array_values($this->form['fields']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function saveTemplate()
|
|
|
|
|
{
|
|
|
|
|
$this->validate([
|
2026-06-17 10:23:29 +02:00
|
|
|
'form.name' => 'required|string|max:255',
|
2026-05-11 15:28:16 +02:00
|
|
|
'form.phase_id' => 'nullable|exists:phases,id',
|
2026-06-17 10:23:29 +02:00
|
|
|
'form.fields' => 'array',
|
2026-05-07 23:31:33 +02:00
|
|
|
]);
|
|
|
|
|
|
2026-06-17 10:23:29 +02:00
|
|
|
$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']),
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
if ($this->editingTemplate) {
|
2026-06-17 10:23:29 +02:00
|
|
|
InspectionTemplate::findOrFail($this->editingTemplate)->update($data);
|
|
|
|
|
$this->dispatch('notify', 'Template actualizado correctamente');
|
2026-05-07 23:31:33 +02:00
|
|
|
} else {
|
2026-06-17 10:23:29 +02:00
|
|
|
InspectionTemplate::create($data);
|
|
|
|
|
$this->dispatch('notify', 'Template creado correctamente');
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->cancelForm();
|
|
|
|
|
$this->loadTemplates();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function deleteTemplate($id)
|
|
|
|
|
{
|
2026-06-17 10:23:29 +02:00
|
|
|
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 = [];
|
2026-05-07 23:31:33 +02:00
|
|
|
$this->loadTemplates();
|
2026-06-17 10:23:29 +02:00
|
|
|
$this->dispatch('notify', "$imported template(s) importado(s) desde otro proyecto");
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function render()
|
|
|
|
|
{
|
|
|
|
|
return view('livewire.template-manager');
|
|
|
|
|
}
|
2026-05-11 16:36:16 +02:00
|
|
|
}
|