瀏覽代碼

subo actualizaciones para back y front modulo Adminstrador de formularios

FREDY 4 月之前
父節點
當前提交
0006ed9b2b

+ 246 - 0
Back/backendP-Educativa/app/Http/Controllers/FormController.php

@@ -0,0 +1,246 @@
+<?php
+
+// app/Http/Controllers/FormController.php
+
+namespace App\Http\Controllers;
+
+use App\Models\Form;
+use App\Models\FormResponse;
+use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Validation\ValidationException;
+
+class FormController extends Controller
+{
+    /**
+     * Guardar un formulario nuevo
+     */
+    public function store(Request $request): JsonResponse
+    {
+        try {
+            $validated = $request->validate([
+                'title' => 'required|string|max:255',
+                'tabs' => 'required|array',
+                'tabs.*.id' => 'required|string',
+                'tabs.*.title' => 'required|string',
+                'tabs.*.rows' => 'required|integer|min:1',
+                'tabs.*.columns' => 'required|integer|min:1',
+                'tabs.*.elements' => 'required|array',
+                'tabs.*.elements.*.position' => 'required|array',
+                'tabs.*.elements.*.element' => 'required|array',
+            ]);
+
+            $form = Form::create([
+                'title' => $validated['title'],
+                'configuration' => [
+                    'tabs' => $validated['tabs']
+                ],
+                'is_published' => false
+            ]);
+
+            return response()->json([
+                'success' => true,
+                'message' => 'Formulario guardado exitosamente',
+                'form' => $form
+            ], 201);
+
+        } catch (ValidationException $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Error de validación',
+                'errors' => $e->errors()
+            ], 422);
+        } catch (\Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Error interno del servidor',
+                'error' => $e->getMessage()
+            ], 500);
+        }
+    }
+
+    /**
+     * Obtener un formulario para renderizar
+    //  */
+    // public function show($id): JsonResponse
+    // {
+    //     try {
+    //         $form = Form::findOrFail($id);
+
+    //         return response()->json([
+    //             'success' => true,
+    //             'form' => $form
+    //         ]);
+
+    //     } catch (\Exception $e) {
+    //         return response()->json([
+    //             'success' => false,
+    //             'message' => 'Formulario no encontrado'
+    //         ], 404);
+    //     }
+    // }
+
+    /**
+     * Publicar un formulario
+    //  */
+    // public function publish($id): JsonResponse
+    // {
+    //     try {
+    //         $form = Form::findOrFail($id);
+    //         $form->update(['is_published' => true]);
+
+    //         return response()->json([
+    //             'success' => true,
+    //             'message' => 'Formulario publicado exitosamente',
+    //             'form' => $form
+    //         ]);
+
+    //     } catch (\Exception $e) {
+    //         return response()->json([
+    //             'success' => false,
+    //             'message' => 'Error al publicar el formulario'
+    //         ], 500);
+    //     }
+    // }
+
+    /**
+     * Obtener todos los formularios
+     */
+    // public function index(): JsonResponse
+    // {
+    //     try {
+    //         $forms = Form::with('responses')->get();
+
+    //         return response()->json([
+    //             'success' => true,
+    //             'forms' => $forms
+    //         ]);
+
+    //     } catch (\Exception $e) {
+    //         return response()->json([
+    //             'success' => false,
+    //             'message' => 'Error al obtener formularios'
+    //         ], 500);
+    //     }
+    // }
+
+    /**
+     * Guardar respuesta de un formulario
+     */
+    public function storeResponse(Request $request, $formId): JsonResponse
+    {
+        try {
+            $form = Form::findOrFail($formId);
+
+            if (!$form->is_published) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Este formulario no está publicado'
+                ], 403);
+            }
+
+            // Validar que las respuestas coincidan con la configuración del formulario
+            $validated = $this->validateFormResponse($request, $form);
+
+            $response = FormResponse::create([
+                'form_id' => $formId,
+                'responses' => $validated,
+                'user_identifier' => $request->ip() // O puedes usar session()->getId()
+            ]);
+
+            return response()->json([
+                'success' => true,
+                'message' => 'Respuesta guardada exitosamente',
+                'response' => $response
+            ], 201);
+
+        } catch (ValidationException $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Error de validación',
+                'errors' => $e->errors()
+            ], 422);
+        } catch (\Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Error al guardar la respuesta',
+                'error' => $e->getMessage()
+            ], 500);
+        }
+    }
+
+    /**
+     * Validar respuesta del formulario dinámicamente
+     */
+    private function validateFormResponse(Request $request, Form $form): array
+    {
+        $rules = [];
+        $validatedData = [];
+
+        foreach ($form->configuration['tabs'] as $tab) {
+            foreach ($tab['elements'] as $element) {
+                $fieldName = $element['element']['name'];
+                $fieldRules = [];
+
+                // Aplicar reglas según el tipo de campo
+                if ($element['element']['required']) {
+                    $fieldRules[] = 'required';
+                }
+
+                switch ($element['element']['type']) {
+                    case 'text':
+                        $fieldRules[] = 'string';
+                        if (isset($element['element']['max'])) {
+                            $fieldRules[] = 'max:' . $element['element']['max'];
+                        }
+                        if (isset($element['element']['min'])) {
+                            $fieldRules[] = 'min:' . $element['element']['min'];
+                        }
+                        break;
+                    case 'email':
+                        $fieldRules[] = 'email';
+                        break;
+                    case 'date':
+                        $fieldRules[] = 'date';
+                        break;
+                    case 'select':
+                        if (isset($element['element']['options'])) {
+                            $fieldRules[] = 'in:' . implode(',', $element['element']['options']);
+                        }
+                        break;
+                    case 'checkbox':
+                        $fieldRules[] = 'boolean';
+                        break;
+                }
+
+                $rules[$fieldName] = $fieldRules;
+            }
+        }
+
+        $validated = $request->validate($rules);
+
+        return $validated;
+    }
+
+    /**
+     * Obtener respuestas de un formulario
+     */
+    public function getResponses($formId): JsonResponse
+    {
+        try {
+            $form = Form::with('responses')->findOrFail($formId);
+
+            return response()->json([
+                'success' => true,
+                'form' => $form->title,
+                'responses' => $form->responses
+            ]);
+
+        } catch (\Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Error al obtener respuestas'
+            ], 500);
+        }
+    }
+}

+ 57 - 0
Back/backendP-Educativa/app/Models/Form.php

@@ -0,0 +1,57 @@
+<?php
+
+// app/Models/Form.php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Form extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'title',
+        'configuration',
+        'is_published'
+    ];
+
+    protected $casts = [
+        'configuration' => 'array',
+        'is_published' => 'boolean'
+    ];
+
+    public function responses()
+    {
+        return $this->hasMany(FormResponse::class);
+    }
+}
+
+// app/Models/FormResponse.php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class FormResponse extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'form_id',
+        'responses',
+        'user_identifier'
+    ];
+
+    protected $casts = [
+        'responses' => 'array'
+    ];
+
+    public function form()
+    {
+        return $this->belongsTo(Form::class);
+    }
+}
+// quiero que cuando se pulse el btn de generar configuracion del fomrulario se mande al end de guarar folruario y se inserte en mi base de datos

+ 49 - 0
Back/backendP-Educativa/database/migrations/2025_07_09_150400_create_forms_table.php

@@ -0,0 +1,49 @@
+<?php
+
+// Migración para la tabla de formularios
+// database/migrations/2024_01_01_000001_create_forms_table.php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateFormsTable extends Migration
+{
+    public function up()
+    {
+        Schema::create('forms', function (Blueprint $table) {
+            $table->id();
+            $table->string('title');
+            $table->json('configuration'); // Guarda toda la estructura del formulario
+            $table->boolean('is_published')->default(false);
+            $table->timestamps();
+        });
+    }
+
+    public function down()
+    {
+        Schema::dropIfExists('forms');
+    }
+}
+
+// Migración para la tabla de respuestas
+// database/migrations/2024_01_01_000002_create_form_responses_table.php
+
+class CreateFormResponsesTable extends Migration
+{
+    public function up()
+    {
+        Schema::create('form_responses', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('form_id')->constrained()->onDelete('cascade');
+            $table->json('responses'); // Guarda las respuestas del usuario
+            $table->string('user_identifier')->nullable(); // IP, session, etc.
+            $table->timestamps();
+        });
+    }
+
+    public function down()
+    {
+        Schema::dropIfExists('form_responses');
+    }
+}

+ 12 - 1
Back/backendP-Educativa/routes/api.php

@@ -26,7 +26,7 @@ use App\Http\Controllers\Api\RegistroAcademico;
 use App\Http\Controllers\Api\profesorRegistroBitacora;
 use App\Http\Controllers\Api\RegistroCalicaciones;
 use App\Http\Controllers\NivelExportController;
-
+use App\Http\Controllers\FormController;
 /*
 |--------------------------------------------------------------------------
 | API Routes
@@ -241,4 +241,15 @@ Route::get('/alumnos/bitacora/{id}', [AlumnosBitacoraController::class, 'index']
     Route::put('habilitarEstadoCalificacion',[RegistroCalicaciones::class,'habilitarEstado']);
 
     Route::get('getAlumnosCalificacion',[RegistroCalicaciones::class,'getCalificacionesMateria']);
+
+
+    //ADMINISTRADOR DE FORMULARIOS RUTAS
+     Route::get('/formularios', [FormController::class, 'index']); // Obtener todos los formularios
+    Route::post('/createForm', [FormController::class, 'store']); // Crear un nuevo formulario
+    Route::get('/obtener/{id}', [FormController::class, 'show']); // Obtener un formulario específico
+    Route::put('/{id}/publish', [FormController::class, 'publish']); // Publicar un formulario
+
+    // Rutas para manejar respuestas
+    Route::post('/{formId}/responses', [FormController::class, 'storeResponse']); // Guardar respuesta
+    Route::get('/{formId}/responses', [FormController::class, 'getResponses']); // Obtener respuestas
 });

+ 38 - 52
Front/src/app/modules/Administrador/pages/admin-form/admin-form.component.html

@@ -4,9 +4,8 @@
   </div>
 </div>
 
-
 <div class="container-main">
-  <!-- Panel configuración general -->
+  <!-- Panel de configuración general -->
   <div class="container-formConfig">
     <app-configuration-panel
       [formTitle]="formTitle"
@@ -14,31 +13,35 @@
       [columns]="columns"
       [showTooltip]="showTooltip"
       [tabs]="tabs"
+      [currentTab]="currentTab"
       (formTitleChange)="onFormTitleChange($event)"
       (rowsChange)="onRowsChange($event)"
       (columnsChange)="onColumnsChange($event)"
       (showTooltipChange)="onShowTooltipChange($event)"
-      (generateClick)="logConfiguration()"
-      (addTabClick)="addTab()"
-      (removeTabClick)="removeTab($event)"
-      (gridConfigChange)="buildGrid()">
+ (generateClick)="saveForm()"
+
+       (addTabClick)="addTab()"
+      (removeTabClick)="removeTab()"
+      (gridConfigChange)="buildGrid()"
+      (tabDimensionsChange)="onTabDimensionsChange($event)">
     </app-configuration-panel>
   </div>
 
-  <!-- Vista previa del grid con tabs -->
+
+
+  <!-- Vista previa del formulario -->
   <div class="container-form">
     <div class="title-form">Vista Previa del Formulario</div>
 
     <!-- Navegación de tabs -->
-    <div class="tabs-navigation" *ngIf="tabs.length > 0">
+    <div class="tabs-navigation" *ngIf="tabs.length">
       <div class="tab-header">
-        <div
-          *ngFor="let tab of tabs; let i = index"
-          class="tab-item"
-          [class.active]="tab.active"
-          (click)="switchToTab(tab)">
+        <div *ngFor="let tab of tabs; let i = index"
+             class="tab-item"
+             [class.active]="tab.active"
+             (click)="switchToTab(tab)">
 
-      <input
+        <input
   #tabInput
   type="text"
   class="tab-name-input"
@@ -46,70 +49,53 @@
   (blur)="updateTabName(tab, tabInput.value)"
   (keyup.enter)="updateTabName(tab, tabInput.value)">
 
-          <button
-            *ngIf="tabs.length > 1"
-            class="tab-close-btn"
-            (click)="removeTab(i); $event.stopPropagation()">
-            ×
+
+          <button *ngIf="tabs.length > 1"
+                  class="tab-close-btn"
+                  (click)="removeTab(); $event.stopPropagation()">
+            &times;
           </button>
         </div>
       </div>
     </div>
 
-    <!-- Grid del tab activo -->
+    <!-- Cuadrícula de campos -->
     <div class="form-grid" [style.grid-template-columns]="'repeat(' + columns + ', 1fr)'">
       <div *ngFor="let row of grid; let i = index" class="grid-row">
-        <div
-          *ngFor="let cell of row; let j = index"
-          class="grid-cell"
-          [class.selected]="cell.selected"
-          [class.has-element]="cell.element"
-          (click)="selectCell(cell)"
-          (dblclick)="selectElementForEditing(cell)">
+        <div *ngFor="let cell of row; let j = index"
+             class="grid-cell"
+             [class.selected]="cell.selected"
+             [class.has-element]="cell.element"
+             (click)="selectCell(cell)"
+             (dblclick)="selectElementForEditing(cell)">
 
           <div *ngIf="cell.element" class="cell-element">
-            <label>{{cell.element.label}}</label>
+            <label>{{ cell.element.label }}</label>
 
             <ng-container [ngSwitch]="cell.element.type">
-              <!-- Texto Corto -->
-              <input *ngSwitchCase="'text'" type="text" [placeholder]="cell.element.placeholder || ''" class="form__input">
-
-              <!-- Número -->
-              <input *ngSwitchCase="'number'" type="number" [placeholder]="cell.element.placeholder || ''" class="form__input">
-
-              <!-- Correo -->
-              <input *ngSwitchCase="'email'" type="email" [placeholder]="cell.element.placeholder || ''" class="form__input">
-
-              <!-- Fecha -->
+              <input *ngSwitchCase="'text'" type="text" class="form__input" [placeholder]="cell.element.placeholder">
+              <input *ngSwitchCase="'number'" type="number" class="form__input" [placeholder]="cell.element.placeholder">
+              <input *ngSwitchCase="'email'" type="email" class="form__input" [placeholder]="cell.element.placeholder">
               <input *ngSwitchCase="'date'" type="date" class="form__input">
-
-              <!-- Área de texto -->
-              <textarea *ngSwitchCase="'textarea'" [placeholder]="cell.element.placeholder || ''" class="form__textarea"></textarea>
-
-              <!-- Select -->
+              <textarea *ngSwitchCase="'textarea'" class="form__textarea" [placeholder]="cell.element.placeholder"></textarea>
               <select *ngSwitchCase="'select'" class="form__select">
                 <option *ngFor="let option of cell.element.options">{{ option }}</option>
               </select>
-
-              <!-- Radio -->
               <div *ngSwitchCase="'radio'" class="form__radio-group">
                 <label *ngFor="let option of cell.element.options">
                   <input type="radio" [name]="cell.element.name" [value]="option"> {{ option }}
                 </label>
               </div>
-
-              <!-- Checkbox -->
               <label *ngSwitchCase="'checkbox'" class="form__checkbox">
                 <input type="checkbox"> {{ cell.element.label }}
               </label>
             </ng-container>
 
-            <!-- Botón para eliminar el elemento -->
-            <button (click)="removeElementFromCell(cell); $event.stopPropagation()" class="remove-btn">×</button>
+            <button class="remove-btn" (click)="removeElementFromCell(cell); $event.stopPropagation()">&times;</button>
           </div>
 
           <div *ngIf="!cell.element" class="cell-placeholder">
-            <span>Posición {{i + 1}},{{j + 1}}</span>
+            <span>Posición {{ i + 1 }},{{ j + 1 }}</span>
             <small>Haz clic para agregar un campo</small>
           </div>
         </div>
@@ -118,7 +104,7 @@
 
     <!-- Selector de tipos de campo -->
     <div *ngIf="showElementSelector && selectedCell" class="selector">
-      <p>Selecciona un tipo de campo para agregar en posición {{selectedCell.row + 1}}, {{selectedCell.col + 1}} - {{currentTab?.name}}</p>
+      <p>Selecciona un tipo de campo para agregar en posición {{ selectedCell.row + 1 }}, {{ selectedCell.col + 1 }} - {{ currentTab?.name }}</p>
       <div class="element-buttons">
         <button *ngFor="let el of availableElements" (click)="addElementToCell(el)">
           {{ el.label }}
@@ -127,7 +113,7 @@
     </div>
   </div>
 
-  <!-- Propiedades del campo seleccionado -->
+  <!-- Panel de propiedades del campo seleccionado -->
   <div class="container-formPropiedades">
     <app-properties-panel
       [element]="editingElement"

+ 330 - 200
Front/src/app/modules/Administrador/pages/admin-form/admin-form.component.ts

@@ -1,3 +1,4 @@
+import { FormApiService, FormConfiguration, FormListItem } from './../../services/FormApiService.service';
 import { Component, OnInit } from '@angular/core';
 import { EnviarInfoService } from '../../services/enviar-info.service';
 
@@ -23,257 +24,428 @@ interface FormElement {
   description?: string;
 }
 
-
 interface Tab {
   id: string;
   name: string;
   active: boolean;
-  rows: number;      // Filas específicas de este tab
-  columns: number;   // Columnas específicas de este tab
+  rows: number;
+  columns: number;
   grid: GridCell[][];
 }
 
 @Component({
   selector: 'app-admin-form',
-  templateUrl:'./admin-form.component.html',
+  templateUrl: './admin-form.component.html',
   styleUrls: ['./admin-form.component.css']
 })
 export class AdminFormComponent implements OnInit {
-  public color: string = '';
-  public textColor: string = '';
+  color: string = '';
+  textColor: string = '';
   formTitle: string = '';
 
-  // Dimensiones por defecto para nuevos tabs
-  defaultRows: number = 3;
-  defaultColumns: number = 3;
+  // Nuevas propiedades para manejar formularios
+  currentFormId: number | null = null;
+  savedForms: FormListItem[] = [];
+  isLoading = false;
+  saveStatus = '';
 
-  showTooltip: string = '';
+  defaultRows = 3;
+  defaultColumns = 3;
+  showTooltip = '';
 
-  // Sistema de tabs
   tabs: Tab[] = [];
   currentTab: Tab | null = null;
-
-  // Grid actual (del tab activo)
   grid: GridCell[][] = [];
   editingElement: FormElement | null = null;
 
-  constructor(private _enviarInfo: EnviarInfoService) {}
+  showElementSelector = false;
+  selectedCell: GridCell | null = null;
+
+  availableElements: FormElement[] = [
+    { type: 'text', label: 'Texto Corto', required: false },
+    { type: 'number', label: 'Número', required: false },
+    { type: 'email', label: 'Correo Electrónico', required: false },
+    { type: 'date', label: 'Fecha', required: false },
+    { type: 'textarea', label: 'Texto Largo', required: false },
+    { type: 'select', label: 'Lista de Opciones', required: false, options: ['Opción 1'] },
+    { type: 'radio', label: 'Selección Única', required: false, options: ['A', 'B'] },
+    { type: 'checkbox', label: 'Casilla', required: false }
+  ];
+
+  constructor(
+    private _enviarInfo: EnviarInfoService,
+    private formApiService: FormApiService
+  ) {}
 
   ngOnInit() {
-    this._enviarInfo.currentTextColor.subscribe(textColor => {
-      this.textColor = textColor;
+    this._enviarInfo.currentTextColor.subscribe(color => this.textColor = color);
+    this._enviarInfo.currentColor.subscribe(color => this.color = color);
+    this.addTab();
+    // this.loadSavedForms();
+  }
+
+  // MÉTODOS PARA MANEJAR LA API
+
+  loadSavedForms() {
+    this.isLoading = true;
+    this.formApiService.getForms().subscribe({
+      next: (forms) => {
+        this.savedForms = forms;
+        this.isLoading = false;
+      },
+      error: (error) => {
+        console.error('Error al cargar formularios:', error);
+        this.saveStatus = 'Error al cargar formularios';
+        this.isLoading = false;
+      }
+    });
+  }
+
+  saveForm() {
+  if (!this.formTitle.trim()) {
+    alert('Por favor, ingresa un título para el formulario');
+    return;
+  }
+
+  if (this.currentTab) {
+    this.currentTab.grid = [...this.grid];
+  }
+
+  const config = this.buildFormConfiguration();
+  this.isLoading = true;
+  this.saveStatus = 'Guardando...';
+
+  this.formApiService.createForm(config).subscribe({
+    next: (response) => {
+      console.log('Formulario guardado:', response);
+      this.currentFormId = response.id;
+      this.saveStatus = 'Formulario guardado exitosamente';
+      this.isLoading = false;
+
+    },
+    error: (error) => {
+      console.error('Error al guardar formulario:', error);
+      this.saveStatus = 'Error al guardar el formulario';
+      this.isLoading = false;
+
+    }
+  });
+}
+
+
+  loadForm(formId: number) {
+    this.isLoading = true;
+    this.formApiService.getForm(formId).subscribe({
+      next: (formData) => {
+        this.loadFormConfiguration(formData);
+        this.currentFormId = formId;
+        this.isLoading = false;
+      },
+      error: (error) => {
+        console.error('Error al cargar formulario:', error);
+        this.saveStatus = 'Error al cargar el formulario';
+        this.isLoading = false;
+      }
     });
+  }
 
-    this._enviarInfo.currentColor.subscribe(color => {
-      this.color = color;
+  publishForm(formId: number) {
+    if (!confirm('¿Estás seguro de que quieres publicar este formulario?')) {
+      return;
+    }
+
+    this.isLoading = true;
+    this.formApiService.publishForm(formId).subscribe({
+      next: (response) => {
+        this.saveStatus = 'Formulario publicado exitosamente';
+        this.loadSavedForms();
+        this.isLoading = false;
+
+        setTimeout(() => {
+          this.saveStatus = '';
+        }, 3000);
+      },
+      error: (error) => {
+        console.error('Error al publicar formulario:', error);
+        this.saveStatus = 'Error al publicar el formulario';
+        this.isLoading = false;
+      }
     });
+  }
 
-    // Crear el primer tab por defecto
-    this.addTab();
+  buildFormConfiguration(): FormConfiguration {
+    return {
+      title: this.formTitle,
+      tabs: this.tabs.map(tab => ({
+        id: tab.id,
+        title: tab.name,
+        rows: tab.rows,
+        columns: tab.columns,
+        elements: this.extractElementsFromGrid(tab.grid)
+      }))
+    };
+  }
+
+  loadFormConfiguration(formData: any) {
+    this.formTitle = formData.title || '';
+    this.tabs = [];
+
+    if (formData.tabs && formData.tabs.length > 0) {
+      formData.tabs.forEach((tabData: any) => {
+        const newTab: Tab = {
+          id: tabData.id,
+          name: tabData.title,
+          active: false,
+          rows: tabData.rows,
+          columns: tabData.columns,
+          grid: this.buildNewGrid(tabData.rows, tabData.columns)
+        };
+
+        // Cargar elementos en el grid
+        if (tabData.elements && tabData.elements.length > 0) {
+          tabData.elements.forEach((elementData: any) => {
+            const { row, col } = elementData.position;
+            if (row < newTab.rows && col < newTab.columns) {
+              newTab.grid[row][col].element = { ...elementData.element };
+            }
+          });
+        }
+
+        this.tabs.push(newTab);
+      });
+
+      this.switchToTab(this.tabs[0]);
+    } else {
+      this.addTab();
+    }
+  }
+
+  newForm() {
+    if (confirm('¿Estás seguro de que quieres crear un nuevo formulario? Se perderán los cambios no guardados.')) {
+      this.formTitle = '';
+      this.currentFormId = null;
+      this.tabs = [];
+      this.addTab();
+      this.saveStatus = '';
+    }
   }
 
+  deleteForm(formId: number) {
+    if (!confirm('¿Estás seguro de que quieres eliminar este formulario? Esta acción no se puede deshacer.')) {
+      return;
+    }
+
+    // Nota: Necesitarás agregar la ruta DELETE en tu API Laravel
+    // Por ahora, solo actualizar la lista
+    this.loadSavedForms();
+  }
+
+  // MÉTODOS EXISTENTES (sin cambios)
 
   get rows(): number {
-    return this.currentTab?.rows || this.defaultRows;
+    return this.currentTab?.rows ?? this.defaultRows;
   }
 
   get columns(): number {
-    return this.currentTab?.columns || this.defaultColumns;
+    return this.currentTab?.columns ?? this.defaultColumns;
   }
 
-  // Métodos para manejo de tabs
-  addTab(customRows?: number, customColumns?: number) {
-    const newTabId = `tab_${this.tabs.length + 1}`;
-    const tabRows = customRows || this.defaultRows;
-    const tabColumns = customColumns || this.defaultColumns;
-
+  // TABS
+  addTab(rows = this.defaultRows, columns = this.defaultColumns) {
     const newTab: Tab = {
-      id: newTabId,
+      id: `tab_${this.tabs.length + 1}`,
       name: `Sección ${this.tabs.length + 1}`,
       active: false,
-      rows: tabRows,
-      columns: tabColumns,
-      grid: this.buildNewGrid(tabRows, tabColumns)
+      rows,
+      columns,
+      grid: this.buildNewGrid(rows, columns)
     };
-
     this.tabs.push(newTab);
     this.switchToTab(newTab);
   }
 
-  removeTab(index: number) {
-    if (this.tabs.length === 1) return;
+  removeTab() {
+    if (this.tabs.length <= 1) return;
 
-    const tabToRemove = this.tabs[index];
-    this.tabs.splice(index, 1);
+    const activeIndex = this.tabs.findIndex(tab => tab.active);
+    if (activeIndex === -1) return;
 
-    if (this.currentTab === tabToRemove) {
-      this.switchToTab(this.tabs[0]);
-    }
+    this.tabs.splice(activeIndex, 1);
+    const newActiveIndex = activeIndex === 0 ? 0 : activeIndex - 1;
+    this.tabs.forEach((tab, index) => {
+      tab.active = index === newActiveIndex;
+    });
   }
 
   switchToTab(tab: Tab) {
-    // Guardar el grid actual en el tab anterior
-    if (this.currentTab) {
-      this.currentTab.grid = [...this.grid];
-    }
-
-    // Desactivar todos los tabs
+    if (this.currentTab) this.currentTab.grid = [...this.grid];
     this.tabs.forEach(t => t.active = false);
-
-    // Activar el tab seleccionado
     tab.active = true;
     this.currentTab = tab;
-
-    // Cargar el grid del tab seleccionado
     this.grid = [...tab.grid];
-
-    // Limpiar selecciones
     this.clearSelection();
   }
 
-  updateTabName(tab: Tab, newName: string) {
-    tab.name = newName;
+  updateTabName(tab: Tab, name: string) {
+    tab.name = name;
+  }
+
+  // GRID
+  buildNewGrid(rows: number, columns: number): GridCell[][] {
+    return Array.from({ length: rows }, (_, r) =>
+      Array.from({ length: columns }, (_, c) => ({
+        row: r, col: c, selected: false
+      }))
+    );
   }
 
-  // Actualizar dimensiones de un tab específico
   updateTabDimensions(tab: Tab, newRows: number, newColumns: number) {
-    const oldGrid = [...tab.grid];
+    const oldGrid = tab.grid.map(row => row.map(cell => ({ ...cell, element: cell.element ? { ...cell.element } : undefined })));
+
     tab.rows = newRows;
     tab.columns = newColumns;
     tab.grid = this.buildNewGrid(newRows, newColumns);
 
-    // Preservar elementos existentes si caben en las nuevas dimensiones
     this.preserveElementsInNewGrid(oldGrid, tab.grid, newRows, newColumns);
 
-    // Si es el tab activo, actualizar el grid actual
-    if (this.currentTab === tab) {
-      this.grid = [...tab.grid];
+    if (this.currentTab?.id === tab.id) {
+      this.grid = tab.grid.map(row => row.map(cell => ({ ...cell, element: cell.element ? { ...cell.element } : undefined })));
     }
   }
 
-  // Preservar elementos al cambiar dimensiones
-  private preserveElementsInNewGrid(oldGrid: GridCell[][], newGrid: GridCell[][], newRows: number, newColumns: number) {
-    oldGrid.forEach(row => {
-      row.forEach(cell => {
-        if (cell.element && cell.row < newRows && cell.col < newColumns) {
-          newGrid[cell.row][cell.col].element = cell.element;
+  preserveElementsInNewGrid(oldGrid: GridCell[][], newGrid: GridCell[][], maxR: number, maxC: number) {
+    for (let r = 0; r < oldGrid.length; r++) {
+      for (let c = 0; c < oldGrid[r]?.length; c++) {
+        const cell = oldGrid[r][c];
+        if (cell?.element && r < maxR && c < maxC) {
+          newGrid[r][c].element = { ...cell.element };
         }
-      });
-    });
-  }
-
-  buildNewGrid(rows: number, columns: number): GridCell[][] {
-    const newGrid: GridCell[][] = [];
-    for(let r = 0; r < rows; r++) {
-      const row: GridCell[] = [];
-      for(let c = 0; c < columns; c++) {
-        row.push({row: r, col: c, selected: false});
       }
-      newGrid.push(row);
     }
-    return newGrid;
+  }
+
+  onTabDimensionsChange(event: { tab: Tab, rows: number, columns: number }) {
+    this.updateTabDimensions(event.tab, event.rows, event.columns);
   }
 
   buildGrid() {
     if (this.currentTab) {
-      this.grid = this.buildNewGrid(this.currentTab.rows, this.currentTab.columns);
-      this.currentTab.grid = [...this.grid];
+      this.currentTab.grid = this.grid = this.buildNewGrid(this.currentTab.rows, this.currentTab.columns);
     }
   }
 
-  // Métodos de configuración JSON optimizados
-  logConfiguration() {
-    if (this.currentTab) {
-      this.currentTab.grid = [...this.grid];
-    }
+  verifyGridIntegrity(grid: GridCell[][]): boolean {
+    return grid.every((row, r) => row.every((cell, c) => {
+      cell.row = r;
+      cell.col = c;
+      return true;
+    }));
+  }
 
-    const configuration = {
-      title: this.formTitle,
-      tabs: this.tabs.map(tab => this.serializeTab(tab))
-    };
+  // FORM CONFIG
+  onFormTitleChange(title: string) {
+    this.formTitle = title;
+  }
 
-    console.log('Configuración completa:', configuration);
-    return configuration;
+  onRowsChange(rows: number) {
+    if (this.currentTab) this.updateTabDimensions(this.currentTab, rows, this.currentTab.columns);
   }
 
-  private serializeTab(tab: Tab) {
-    return {
-      id: tab.id,
-      title: tab.name,
-      rows: tab.rows,
-      columns: tab.columns,
-      elements: this.extractElementsFromGrid(tab.grid)
-    };
+  onColumnsChange(cols: number) {
+    if (this.currentTab) this.updateTabDimensions(this.currentTab, this.currentTab.rows, cols);
   }
 
-  private extractElementsFromGrid(grid: GridCell[][]): any[] {
-    const elements: any[] = [];
+  onShowTooltipChange(field: string) {
+    this.showTooltip = field;
+  }
 
-    grid.forEach(row => {
-      row.forEach(cell => {
-        if (cell.element) {
-          elements.push({
-            position: {
-              row: cell.row,
-              col: cell.col
-            },
-            element: this.serializeElement(cell.element)
-          });
-        }
-      });
-    });
+  // SERIALIZACIÓN
+  logConfiguration() {
+    const config = this.buildFormConfiguration();
+    console.log('Configuración completa:', config);
+    return config;
+  }
 
-    return elements;
+  extractElementsFromGrid(grid: GridCell[][]) {
+    return grid.flatMap(row => row
+      .filter(cell => cell.element)
+      .map(cell => ({
+        position: { row: cell.row, col: cell.col },
+        element: this.serializeElement(cell.element!)
+      }))
+    );
   }
 
-  private serializeElement(element: FormElement): any {
-    const serialized: any = {
-      type: element.type,
-      label: element.label,
-      required: element.required
+  serializeElement(el: FormElement) {
+    const base: any = {
+      type: el.type, label: el.label, required: el.required
     };
+    if (el.placeholder) base.placeholder = el.placeholder;
+    if (el.id) base.id = el.id;
+    if (el.name) base.name = el.name;
+    if (el.value) base.value = el.value;
+    if (el.description) base.description = el.description;
+    if (el.options?.length) base.options = el.options;
+    if (el.min !== undefined) base.min = el.min;
+    if (el.max !== undefined) base.max = el.max;
+    if (el.pattern) base.pattern = el.pattern;
+    return base;
+  }
 
-    if (element.placeholder) serialized.placeholder = element.placeholder;
-    if (element.id) serialized.id = element.id;
-    if (element.name) serialized.name = element.name;
-    if (element.value) serialized.value = element.value;
-    if (element.description) serialized.description = element.description;
-
-    if (element.options && element.options.length > 0) {
-      serialized.options = element.options;
-    }
-
-    if (element.min !== undefined) serialized.min = element.min;
-    if (element.max !== undefined) serialized.max = element.max;
-    if (element.pattern) serialized.pattern = element.pattern;
-
-    return serialized;
+  // INTERACCIÓN CON CELDAS Y CAMPOS
+  selectCell(cell: GridCell) {
+    this.clearSelection();
+    cell.selected = true;
+    this.selectedCell = cell;
+    this.showElementSelector = true;
   }
 
-  // Eventos del panel de configuración
-  onFormTitleChange(newTitle: string) {
-    this.formTitle = newTitle;
+  clearSelection() {
+    this.grid.forEach(row => row.forEach(c => c.selected = false));
+    this.selectedCell = null;
+    this.showElementSelector = false;
+    this.editingElement = null;
   }
 
-  onRowsChange(newRows: number) {
-    if (this.currentTab) {
-      this.updateTabDimensions(this.currentTab, newRows, this.currentTab.columns);
+  addElementToCell(template: FormElement) {
+    if (!this.selectedCell || this.selectedCell.element) {
+      alert('Esta celda ya tiene un campo. Elimínalo primero.');
+      return;
     }
-  }
 
-  onColumnsChange(newCols: number) {
-    if (this.currentTab) {
-      this.updateTabDimensions(this.currentTab, this.currentTab.rows, newCols);
+    const element = { ...template };
+    element.id = `${this.currentTab?.id}_campo_${this.selectedCell.row}_${this.selectedCell.col}`;
+    element.name = element.id;
+
+    switch (element.type) {
+      case 'email':
+        Object.assign(element, {
+          required: true,
+          pattern: '^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$',
+          placeholder: 'ejemplo@correo.com'
+        });
+        break;
+      case 'number':
+        Object.assign(element, { required: true, min: 1, max: 100 });
+        break;
+      case 'text':
+        Object.assign(element, {
+          required: true,
+          min: 1,
+          max: 20,
+          pattern: '^\\b(\\w+\\b\\s*){1,20}$'
+        });
+        break;
     }
-  }
 
-  onShowTooltipChange(field: string) {
-    this.showTooltip = field;
+    this.selectedCell.element = element;
+    this.editingElement = element;
+    this.showElementSelector = false;
+    this.updateCurrentTabGrid();
   }
 
-  // Resto de métodos existentes...
-  selectElementForEditing(cell: GridCell) {
-    this.editingElement = cell.element || null;
+  updateCurrentTabGrid() {
+    if (this.currentTab) this.currentTab.grid = [...this.grid];
   }
 
   removeElementFromCell(cell: GridCell) {
@@ -281,62 +453,20 @@ export class AdminFormComponent implements OnInit {
     this.updateCurrentTabGrid();
   }
 
-  updateCurrentTabGrid() {
-    if (this.currentTab) {
-      this.currentTab.grid = [...this.grid];
-    }
-  }
-
-  clearSelection() {
-    this.grid.forEach(row => row.forEach(c => c.selected = false));
-    this.selectedCell = null;
-    this.showElementSelector = false;
-    this.editingElement = null;
-  }
-
-  // Propiedades existentes para el selector de elementos
-  showElementSelector: boolean = false;
-  selectedCell: GridCell | null = null;
-
-  availableElements: FormElement[] = [
-    { type: 'text', label: 'Texto Corto', required: false },
-    { type: 'number', label: 'Número', required: false },
-    { type: 'email', label: 'Correo Electrónico', required: false },
-    { type: 'date', label: 'Fecha', required: false },
-    { type: 'textarea', label: 'Texto Largo', required: false },
-    { type: 'select', label: 'Lista de Opciones', required: false, options: ['Opción 1'] },
-    { type: 'radio', label: 'Selección Única', required: false, options: ['A', 'B'] },
-    { type: 'checkbox', label: 'Casilla', required: false }
-  ];
-
-  selectCell(cell: GridCell) {
-    this.grid.forEach(row => row.forEach(c => c.selected = false));
-    cell.selected = true;
-    this.selectedCell = cell;
-    this.showElementSelector = true;
+  selectElementForEditing(cell: GridCell) {
+    this.editingElement = cell.element || null;
   }
 
-  addElementToCell(element: FormElement) {
+  onEditingElementChange(updated: FormElement) {
     if (this.selectedCell) {
-      const cloned = { ...element };
-      cloned.id = `${this.currentTab?.id}_campo_${this.selectedCell.row}_${this.selectedCell.col}`;
-      cloned.name = cloned.id;
-      this.selectedCell.element = cloned;
-      this.showElementSelector = false;
-      this.editingElement = cloned;
+      this.selectedCell.element = updated;
       this.updateCurrentTabGrid();
     }
   }
 
-  onEditingElementChange(updatedElement: FormElement) {
-    if (!this.selectedCell) return;
-    this.selectedCell.element = updatedElement;
-    this.updateCurrentTabGrid();
-  }
-
-  onElementChange(updatedElement: FormElement) {
-    if(this.editingElement) {
-      Object.assign(this.editingElement, updatedElement);
+  onElementChange(updated: FormElement) {
+    if (this.editingElement) {
+      Object.assign(this.editingElement, updated);
       this.updateCurrentTabGrid();
     }
   }

+ 108 - 1
Front/src/app/modules/Administrador/pages/admin-form/configuration-panel/configuration-panel.component.css

@@ -143,9 +143,9 @@
 }
 .example-tab-button{
  width: 40%;
-  color: white;
   background: #007bff;
   border: none;
+  color: #dee2e6 !important;
   border-radius: 6px;
   font-weight: 500;
   cursor: pointer;
@@ -170,4 +170,111 @@
   .help-icon {
     font-size: 11px;
   }
+  /* Media query para pantallas medianas (tablets y móviles grandes) */
+@media (max-width: 768px) {
+  .grid-config {
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .help-btn {
+    width: 20px;
+    height: 20px;
+  }
+
+  .help-icon {
+    font-size: 11px;
+  }
+
+  .form__input,
+  .form__input-small {
+    width: 100%;
+  }
+
+  .btn-generate {
+    width: 100%;
+    font-size: 14px;
+    padding: 10px;
+  }
+
+  .example-tab-button {
+    width: 100%;
+    margin: 8px 0;
+    font-size: 14px;
+  }
+
+  .title-form {
+    font-size: 16px;
+    padding-bottom: 8px;
+  }
+
+  .text {
+    font-size: 14px;
+  }
+
+  .help-tooltip {
+    font-size: 12px;
+    padding: 10px;
+  }
+
+  .input-group label {
+    font-size: 11px;
+  }
+}
+
+/* Media query para teléfonos pequeños */
+@media (max-width: 480px) {
+  .grid-config {
+    flex-direction: column;
+    gap: 10px;
+  }
+
+  .form__input,
+  .form__input-small {
+    font-size: 13px;
+    padding: 10px;
+  }
+
+  .btn-generate {
+    font-size: 13px;
+    padding: 8px;
+  }
+
+  .help-btn {
+    width: 18px;
+    height: 18px;
+  }
+
+  .help-icon {
+    font-size: 10px;
+  }
+
+  .example-tab-button {
+    font-size: 13px;
+    padding: 8px;
+  }
+
+  .title-form {
+    font-size: 15px;
+  }
+
+  .help-tooltip {
+    font-size: 11px;
+    padding: 8px;
+  }
+
+  .input-with-help {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  .input-group {
+    width: 100%;
+  }
+
+  .form__input {
+    width: 100%;
+  }
+}
+
 }

+ 25 - 38
Front/src/app/modules/Administrador/pages/admin-form/configuration-panel/configuration-panel.component.html

@@ -1,10 +1,10 @@
 <div class="title-form">Configuración General</div>
 
-<!-- Form Title -->
+<!-- Título del Formulario -->
 <div class="config-section">
   <div class="input-with-help">
     <p class="text">Título del formulario</p>
-    <button class="help-btn" (click)="showHelp('formTitle')" type="button">
+    <button class="help-btn" (click)="showHelp('formTitle')">
       <i class="help-icon">?</i>
     </button>
   </div>
@@ -16,19 +16,19 @@
     class="form__input"
     placeholder="Ej: Formulario de Contacto"
     [ngModel]="formTitle"
-    (ngModelChange)="formTitleChange.emit($event)">
+    (ngModelChange)="onFormTitleChange($event)">
 </div>
 
-<!-- Grid Configuration -->
-<div class="config-section">
+<!-- Configuración de cuadrícula del tab actual -->
+<div class="config-section" *ngIf="currentTab">
   <div class="input-with-help">
-    <p class="text">Diseño de la Cuadrícula</p>
-    <button class="help-btn" (click)="showHelp('gridConfig')" type="button">
+    <p class="text">Diseño de la cuadrícula del tab activo</p>
+    <button class="help-btn" (click)="showHelp('gridConfig')">
       <i class="help-icon">?</i>
     </button>
   </div>
   <div class="help-tooltip" *ngIf="showTooltip === 'gridConfig'">
-    Define cómo se organizarán los campos en tu formulario.
+    Ajusta el número de filas y columnas del tab activo sin perder los campos existentes.
   </div>
 
   <div class="grid-config">
@@ -37,62 +37,49 @@
       <input
         type="number"
         class="form__input-small"
-        [ngModel]="rows"
-        (ngModelChange)="rowsChange.emit($event); gridConfigChange.emit()"
-        min="1"
-        max="10">
+        [ngModel]="currentTab.rows"
+        (ngModelChange)="onTabRowsChange(currentTab, $event)"
+        min="1" max="20">
     </div>
     <div class="input-group">
       <label>Columnas:</label>
       <input
         type="number"
         class="form__input-small"
-        [ngModel]="columns"
-        (ngModelChange)="columnsChange.emit($event); gridConfigChange.emit()"
-        min="1"
-        max="10">
+        [ngModel]="currentTab.columns"
+        (ngModelChange)="onTabColumnsChange(currentTab, $event)"
+        min="1" max="20">
     </div>
   </div>
-  <!-- Sección de diseño de secciones del formulario -->
+</div>
+
+<!-- Secciones del Formulario -->
 <div class="config-section">
   <div class="input-with-help">
     <p class="text">Diseño de las secciones del formulario</p>
-    <button class="help-btn" (click)="showHelp('tabConfig')" type="button">
+    <button class="help-btn" (click)="showHelp('tabConfig')">
       <i class="help-icon">?</i>
     </button>
   </div>
   <div class="help-tooltip" *ngIf="showTooltip === 'tabConfig'">
-    Define cómo se organizarán las secciones de tu formulario.
+    Agrega o elimina secciones de tu formulario. Cada sección tiene su propio diseño.
   </div>
 
   <div class="tab-actions">
-    <button mat-button
-            class="example-tab-button"
-            (click)="addTab()">
-      Agregar nueva sección
-    </button>
-
-    <button mat-button
-            class="example-tab-button"
-            [disabled]="tabs.length === 1"
-            (click)="removeTab(tabs.length - 1)">
-      Eliminar última sección
-    </button>
+    <button mat-button class="example-tab-button" (click)="addTab()">Agregar nueva sección</button>
+    <button mat-button class="example-tab-button" [disabled]="tabs.length === 1" (click)="removeTab(tabs.length - 1)">Eliminar última sección</button>
   </div>
 </div>
-<!-- Generar JSON -->
+
+<!-- Generar Configuración del Formulario -->
 <div class="config-section">
   <div class="input-with-help">
-    <button class="btn-generate" (click)="generateClick.emit()">
-      Generar Configuración del Formulario
-    </button>
-    <button class="help-btn" (click)="showHelp('generateJson')" type="button">
+    <button class="btn-generate" (click)="generateConfiguration()">Generar Configuración del Formulario</button>
+    <button class="help-btn" (click)="showHelp('generateJson')">
       <i class="help-icon">?</i>
     </button>
   </div>
   <div class="help-tooltip" *ngIf="showTooltip === 'generateJson'">
     Genera el código de configuración que podrás usar para implementarlo.
   </div>
-
 </div>
-

+ 42 - 21
Front/src/app/modules/Administrador/pages/admin-form/configuration-panel/configuration-panel.component.ts

@@ -15,10 +15,10 @@ interface Tab {
   styleUrls: ['./configuration-panel.component.css']
 })
 export class ConfigurationPanelComponent {
-  @Input() formTitle: string = '';
-  @Input() rows: number = 3;
-  @Input() columns: number = 3;
-  @Input() showTooltip: string = '';
+  @Input() formTitle = '';
+  @Input() rows = 3;
+  @Input() columns = 3;
+  @Input() showTooltip = '';
   @Input() tabs: Tab[] = [];
   @Input() currentTab: Tab | null = null;
 
@@ -30,35 +30,54 @@ export class ConfigurationPanelComponent {
   @Output() addTabClick = new EventEmitter<void>();
   @Output() removeTabClick = new EventEmitter<number>();
   @Output() gridConfigChange = new EventEmitter<void>();
-  @Output() tabDimensionsChange = new EventEmitter<{tab: Tab, rows: number, columns: number}>();
-
-  constructor() { }
+  @Output() tabDimensionsChange = new EventEmitter<{ tab: Tab, rows: number, columns: number }>();
 
   showHelp(field: string) {
-    if (this.showTooltip === field) {
-      this.showTooltipChange.emit('');
-    } else {
-      this.showTooltipChange.emit(field);
-    }
+    this.showTooltipChange.emit(this.showTooltip === field ? '' : field);
   }
 
   onRowsChange(value: number) {
-    this.rowsChange.emit(value);
-    this.gridConfigChange.emit();
+    if (this.isValidDimension(value)) {
+      this.rowsChange.emit(value);
+      this.gridConfigChange.emit();
+    }
   }
 
   onColumnsChange(value: number) {
-    this.columnsChange.emit(value);
-    this.gridConfigChange.emit();
+    if (this.isValidDimension(value)) {
+      this.columnsChange.emit(value);
+      this.gridConfigChange.emit();
+    }
   }
 
-  // Cambiar dimensiones de un tab específico
   onTabRowsChange(tab: Tab, value: number) {
-    this.tabDimensionsChange.emit({tab: tab, rows: value, columns: tab.columns});
+    if (!this.isValidDimension(value)) return;
+
+    if (value < tab.rows && this.checkIfElementsWillBeLost(tab, value, tab.columns)) {
+      if (!confirm('Al reducir las filas, se perderán algunos elementos. ¿Deseas continuar?')) return;
+    }
+
+    this.tabDimensionsChange.emit({ tab, rows: value, columns: tab.columns });
   }
 
   onTabColumnsChange(tab: Tab, value: number) {
-    this.tabDimensionsChange.emit({tab: tab, rows: tab.rows, columns: value});
+    if (!this.isValidDimension(value)) return;
+
+    if (value < tab.columns && this.checkIfElementsWillBeLost(tab, tab.rows, value)) {
+      if (!confirm('Al reducir las columnas, se perderán algunos elementos. ¿Deseas continuar?')) return;
+    }
+
+    this.tabDimensionsChange.emit({ tab, rows: tab.rows, columns: value });
+  }
+
+  private isValidDimension(value: number): boolean {
+    return value >= 1 && value <= 20;
+  }
+
+  private checkIfElementsWillBeLost(tab: Tab, newRows: number, newColumns: number): boolean {
+    return tab.grid?.some((row, r) =>
+      row?.some((cell, c) => cell?.element && (r >= newRows || c >= newColumns))
+    ) ?? false;
   }
 
   addTab() {
@@ -77,13 +96,15 @@ export class ConfigurationPanelComponent {
     this.generateClick.emit();
   }
 
-  // Método para obtener el tab activo
   getActiveTab(): Tab | null {
     return this.currentTab;
   }
 
-  // Método para verificar si un tab está activo
   isTabActive(tab: Tab): boolean {
     return this.currentTab?.id === tab.id;
   }
+
+  getElementsCount(tab: Tab): number {
+    return tab.grid?.flat().filter(cell => cell?.element).length ?? 0;
+  }
 }

+ 2 - 2
Front/src/app/modules/Administrador/pages/admin-form/properties-panel/properties-panel.component.html

@@ -64,7 +64,7 @@
     </div>
 
     <!-- Validación para patrón (regex) -->
-    <div *ngIf="['text', 'email', 'textarea'].includes(element.type)">
+    <!-- <div *ngIf="['text', 'email', 'textarea'].includes(element.type)">
       <label>Patrón de validación (regex):</label>
       <input
         type="text"
@@ -73,7 +73,7 @@
         (ngModelChange)="onValueChange()"
         placeholder="Ej: ^[A-Za-z]+$"
       />
-    </div>
+    </div> -->
 
   </div>
 </div>

+ 1 - 1
Front/src/app/modules/Administrador/pages/admin-form/properties-panel/properties-panel.component.spec.ts

@@ -11,7 +11,7 @@ describe('PropertiesPanelComponent', () => {
       declarations: [PropertiesPanelComponent]
     })
     .compileComponents();
-    
+
     fixture = TestBed.createComponent(PropertiesPanelComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();

+ 111 - 0
Front/src/app/modules/Administrador/services/FormApiService.service.ts

@@ -0,0 +1,111 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { environments } from '../../../../environments/environments';
+
+export interface FormConfiguration {
+  title: string;
+  tabs: {
+    id: string;
+    title: string;
+    rows: number;
+    columns: number;
+    elements: {
+      position: { row: number; col: number };
+      element: {
+        type: string;
+        label: string;
+        required: boolean;
+        placeholder?: string;
+        id?: string;
+        name?: string;
+        value?: string;
+        description?: string;
+        options?: string[];
+        min?: number;
+        max?: number;
+        pattern?: string;
+      };
+    }[];
+  }[];
+}
+
+export interface FormListItem {
+  id: number;
+  title: string;
+  is_published: boolean;
+  created_at: string;
+  updated_at: string;
+}
+
+export interface FormResponse {
+  id?: number;
+  form_id: number;
+  responses: { [key: string]: any };
+  created_at?: string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class FormApiService {
+  private apiUrl = environments.baseUrl;
+
+  constructor(private http: HttpClient) {}
+
+  // Encabezados reutilizables con token
+  private getHeaders(): HttpHeaders {
+    const token = localStorage.getItem('token') || '';
+    return new HttpHeaders({
+      'Content-Type': 'application/json',
+      'Accept': 'application/json',
+      'Authorization': `Bearer ${token}`
+    });
+  }
+
+  // Obtener todos los formularios
+  getForms(): Observable<FormListItem[]> {
+    return this.http.get<FormListItem[]>(`${this.apiUrl}/formularios`, {
+      headers: this.getHeaders()
+    });
+  }
+
+  // Crear un nuevo formulario
+  createForm(formData: FormConfiguration): Observable<any> {
+    return this.http.post<any>(`${this.apiUrl}/createForm`, formData, {
+      headers: this.getHeaders()
+    });
+  }
+
+  // Obtener un formulario específico
+  getForm(id: number): Observable<any> {
+    return this.http.get<any>(`${this.apiUrl}/obtener/${id}`, {
+      headers: this.getHeaders()
+    });
+  }
+
+  // Publicar un formulario
+  publishForm(id: number): Observable<any> {
+    return this.http.put<any>(`${this.apiUrl}/${id}/publish`, {}, {
+      headers: this.getHeaders()
+    });
+  }
+
+  // Guardar respuesta de un formulario
+  saveResponse(formId: number, responses: { [key: string]: any }): Observable<any> {
+    const responseData = {
+      form_id: formId,
+      responses: responses
+    };
+    return this.http.post<any>(`${this.apiUrl}/${formId}/responses`, responseData, {
+      headers: this.getHeaders()
+    });
+  }
+
+  // Obtener respuestas de un formulario
+  getResponses(formId: number): Observable<FormResponse[]> {
+    return this.http.get<FormResponse[]>(`${this.apiUrl}/${formId}/responses`, {
+      headers: this.getHeaders()
+    });
+  }
+}