Browse Source

agrego vostas para Padre de familia, CONTROLLER, SERVICE Y NUEVO COMPONENTE

FREDY 3 months ago
parent
commit
d40d7d5b0f

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

@@ -188,5 +188,96 @@ public function update(Request $request, $id): JsonResponse
             ], 500);
         }
     }
+    public function publish($id)
+{
+    try {
+        $form = DB::table('forms')->where('id', $id)->first();
+
+        if (!$form) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Formulario no encontrado',
+            ], 404);
+        }
+
+        if ($form->is_published) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Formulario ya está publicado',
+            ], 400);
+        }
+
+        DB::table('forms')->where('id', $id)->update([
+            'is_published' => true,
+            'updated_at' => now()
+        ]);
+
+        return response()->json([
+            'success' => true,
+            'message' => 'Formulario publicado exitosamente',
+        ]);
+    } catch (\Exception $e) {
+        return response()->json([
+            'success' => false,
+            'message' => 'Error al publicar el formulario',
+            'error' => $e->getMessage()
+        ], 500);
+    }
+}
+
+public function unpublish($id)
+{
+    try {
+        $form = DB::table('forms')->where('id', $id)->first();
+
+        if (!$form) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Formulario no encontrado',
+            ], 404);
+        }
+
+        if (!$form->is_published) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Formulario ya está despublicado',
+            ], 400);
+        }
+
+        DB::table('forms')->where('id', $id)->update([
+            'is_published' => false,
+            'updated_at' => now()
+        ]);
+
+        return response()->json([
+            'success' => true,
+            'message' => 'Formulario despublicado exitosamente',
+        ]);
+    } catch (\Exception $e) {
+        return response()->json([
+            'success' => false,
+            'message' => 'Error al despublicar el formulario',
+            'error' => $e->getMessage()
+        ], 500);
+    }
+}
+
+public function getPublicado()
+{
+    $formulario = DB::table('forms')->where('is_published', true)->first();
+
+    if (!$formulario) {
+        return response()->json([
+            'success' => false,
+            'message' => 'No hay formulario publicado'
+        ]);
+    }
+
+    return response()->json([
+        'success' => true,
+        'form' => $formulario
+    ]);
+}
+
 }
 

+ 6 - 0
Back/backendP-Educativa/routes/api.php

@@ -251,5 +251,11 @@ Route::get('/alumnos/bitacora/{id}', [AlumnosBitacoraController::class, 'index']
         Route::get('/formulario/{id}', [FormController::class, 'getFormById']);
     Route::put('/updateForm/{id}', [FormController::class, 'update']);
 Route::delete('/forms/{id}', [FormController::class, 'destroy']);
+Route::put('formularios/{id}/publish', [FormController::class, 'publish']);
+Route::put('formularios/{id}/unpublish', [FormController::class, 'unpublish']);
+Route::get('/formularios/publicado', [FormController::class, 'getPublicado']);
+
+
+
 
 });

+ 7 - 1
Front/src/app/modules/Administrador/pages/admin-form/admin-form.component.ts

@@ -71,7 +71,13 @@ export class AdminFormComponent implements OnInit {
     { 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 }
+    { type: 'checkbox', label: 'Casilla', required: false },
+     {
+    type: 'select',
+    label: 'LADA Telefónica',
+    required: false,
+    options: ['+52 (MX)', '+1 (US)']
+  }
   ];
   formData: any;
   form: any;

+ 19 - 10
Front/src/app/modules/Administrador/pages/admin-form/form-selector/form-selector.component.html

@@ -48,17 +48,26 @@
   </td>
 </ng-container>
 
+//PUBLICAR
+ <ng-container matColumnDef="publicar">
+  <th mat-header-cell *matHeaderCellDef> Publicar </th>
+  <td mat-cell *matCellDef="let form">
+   <button *ngIf="!form.is_published" mat-button color="accent"
+        (click)="publicarFormulario(form.id)"
+        [disabled]="isLoading || hayFormularioPublicado">
+  <mat-icon>publish</mat-icon> Publicar
+</button>
+
+<button *ngIf="form.is_published" mat-button color="warn"
+        (click)="despublicarFormulario(form.id)"
+        [disabled]="isLoading">
+  <mat-icon>cancel</mat-icon> Despublicar
+</button>
+
+  </td>
+</ng-container>
+
 
-        <!-- Publicar Column -->
-        <ng-container matColumnDef="publicar">
-          <th mat-header-cell *matHeaderCellDef> Publicar </th>
-          <td mat-cell *matCellDef="let form">
-            <button mat-button color="accent">
-              <mat-icon>publish</mat-icon>
-              Publicar
-            </button>
-          </td>
-        </ng-container>
         <!-- Eliminar Column -->
 <ng-container matColumnDef="eliminar">
   <th mat-header-cell *matHeaderCellDef> Eliminar </th>

+ 70 - 0
Front/src/app/modules/Administrador/pages/admin-form/form-selector/form-selector.component.ts

@@ -39,6 +39,9 @@ displayedColumns: string[] = ['id', 'title', 'ver', 'editar', 'publicar', 'elimi
 
   constructor(private formService: FormService, private _enviarInfo: EnviarInfoService, private router: Router,  private snackBar: MatSnackBar
   ) {}
+    get hayFormularioPublicado(): boolean {
+    return this.dataSource.data.some(f => f.is_published);
+  }
 
   ngOnInit(): void {
       this._enviarInfo.currentTextColor.subscribe(color => this.textColor = color);
@@ -97,8 +100,75 @@ showMessage(message: string) {
     horizontalPosition: 'right',
     verticalPosition: 'top'
   });
+}publicarFormulario(formId: number): void {
+  this.isLoading = true;
+
+  // Primero despublicamos cualquier otro formulario publicado antes de publicar el nuevo
+  const formPublicado = this.dataSource.data.find(f => f.is_published);
+
+  const despublicar$ = formPublicado && formPublicado.id !== formId
+    ? this.formService.unpublishForm(formPublicado.id)
+    : null;
+
+  const publicar$ = () => this.formService.publishForm(formId);
+
+  if (despublicar$) {
+    despublicar$.subscribe({
+      next: () => {
+        publicar$().subscribe({
+          next: () => {
+            this.updateFormsAfterPublish(formId);
+          },
+          error: (err) => this.handleError(err, 'Error al publicar el formulario.')
+        });
+      },
+      error: (err) => this.handleError(err, 'Error al despublicar el formulario anterior.')
+    });
+  } else {
+    publicar$().subscribe({
+      next: () => {
+        this.updateFormsAfterPublish(formId);
+      },
+      error: (err) => this.handleError(err, 'Error al publicar el formulario.')
+    });
+  }
 }
 
+updateFormsAfterPublish(formId: number) {
+  // Actualiza todos los formularios: solo el publicado queda true, los demás false
+  this.dataSource.data = this.dataSource.data.map(f => ({
+    ...f,
+    is_published: f.id === formId
+  }));
+  this.isLoading = false;
+  this.showMessage('Formulario publicado correctamente. Solo uno puede estar publicado a la vez.');
+}
+
+despublicarFormulario(formId: number): void {
+  this.isLoading = true;
+  this.formService.unpublishForm(formId).subscribe({
+    next: () => {
+      const form = this.dataSource.data.find(f => f.id === formId);
+      if (form) {
+        form.is_published = false;
+      }
+      this.dataSource.data = [...this.dataSource.data];
+      this.isLoading = false;
+      this.showMessage('Formulario despublicado correctamente.');
+    },
+    error: (err) => {
+      this.isLoading = false;
+      console.error('Error al despublicar el formulario:', err);
+      this.showMessage('Error al despublicar el formulario.');
+    }
+  });
+}
+
+handleError(error: any, message: string) {
+  this.isLoading = false;
+  console.error(error);
+  this.showMessage(message);
+}
 
 
 

+ 19 - 5
Front/src/app/modules/Administrador/services/FormService.service.ts

@@ -52,11 +52,12 @@ updateForm(id: number, data: any): Observable<any> {
 
 
   // ✅ Publicar un formulario
-  publishForm(id: number): Observable<any> {
-    return this.http.put(`${this.URL}/${id}/publish`, {}, {
-      headers: this.getHeaders()
-    });
-  }
+publishForm(id: number): Observable<any> {
+  return this.http.put<any>(`${this.URL}/formularios/${id}/publish`, {}, {
+    headers: this.getHeaders()
+  });
+}
+
 
   // ✅ Enviar una respuesta de formulario
   sendFormResponse(formId: number, data: any): Observable<any> {
@@ -76,5 +77,18 @@ updateForm(id: number, data: any): Observable<any> {
     headers: this.getHeaders()
   });
 }
+unpublishForm(id: number): Observable<any> {
+  return this.http.put<any>(`${this.URL}/formularios/${id}/unpublish`, {}, {
+    headers: this.getHeaders()
+  });
+}
+getPublishedForm() {
+  return this.http.get<any>(`${this.URL}/formularios/publicado`,{
+      headers: this.getHeaders()
+    });
+  }
+
+
+
 
 }

+ 2 - 1
Front/src/app/modules/Padres/padres-routing.module.ts

@@ -8,7 +8,7 @@ import { registroAlumnoComponent } from './pages/registroAlumno/registroAlumno.c
 import { registroComponent, RegistroAdComponent } from './pages/Registro/registro.component';
 import { CircularHijosComponent } from './pages/circularHijos/circular-hijos.component';
 import { BitacoraHijosComponent } from './pages/bitacora-hijos/bitacora-hijos.component';
-
+import { FormulariosComponent } from './pages/formularios/formularios.component';
 
 
 
@@ -24,6 +24,7 @@ const routes: Routes = [
             { path: 'registroAd/:id', component: RegistroAdComponent },
             { path: 'circularHijo/:id', component: CircularHijosComponent },
             { path: 'bitacoraHijo/:id', component: BitacoraHijosComponent },
+            { path: 'formularios', component: FormulariosComponent },
 
 
             { path: '', redirectTo: 'registroAlumno', pathMatch: 'full' },

+ 2 - 0
Front/src/app/modules/Padres/padres.module.ts

@@ -14,6 +14,7 @@ import { registroAlumnoComponent } from './pages/registroAlumno/registroAlumno.c
 import { RegistroAdComponent, registroComponent } from './pages/Registro/registro.component';
 import { CircularHijosComponent } from './pages/circularHijos/circular-hijos.component';
 import { BitacoraHijosComponent } from './pages/bitacora-hijos/bitacora-hijos.component';
+import { FormulariosComponent } from './pages/formularios/formularios.component';
 
 
 
@@ -29,6 +30,7 @@ import { BitacoraHijosComponent } from './pages/bitacora-hijos/bitacora-hijos.co
     RegistroAdComponent,
     CircularHijosComponent,
     BitacoraHijosComponent,
+    FormulariosComponent,
   ],
   imports: [
     CommonModule,

+ 210 - 0
Front/src/app/modules/Padres/pages/formularios/formularios.component.css

@@ -0,0 +1,210 @@
+/* Contenedor principal para centrar y dar margen */
+form {
+  width: 80%;
+  margin: 50px auto 30px auto;
+  background-color: #fff;
+  padding: 24px 30px;
+  border-radius: 8px;
+  box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
+  font-family: 'Roboto', sans-serif;
+  color: #333;
+
+/* Sección de cada pestaña (tab) */
+.tab-section {
+  margin-bottom: 40px;
+}
+
+/* Título de la sección */
+.tab-section h3 {
+  font-weight: 600;
+  font-size: 1.3rem;
+  margin-bottom: 20px;
+  border-bottom: 2px solid #2196F3;
+  padding-bottom: 8px;
+  color: #0d47a1;
+}
+
+/* Grid para la disposición de los campos */
+.grid {
+  display: grid;
+  gap: 20px 15px;
+}
+
+/* Cada campo individual */
+.form-element {
+  display: flex;
+  flex-direction: column;
+}
+
+/* Etiqueta de campo */
+.form-element label {
+  font-weight: 500;
+  margin-bottom: 6px;
+  color: #444;
+}
+
+/* Inputs, textarea y select con estilo uniforme */
+input[type="text"],
+input[type="email"],
+input[type="date"],
+input[type="number"],
+textarea,
+select {
+  padding: 10px 14px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  font-size: 1rem;
+  font-family: inherit;
+  transition: border-color 0.3s ease;
+}
+
+input[type="text"]:focus,
+input[type="email"]:focus,
+input[type="date"]:focus,
+input[type="number"]:focus,
+textarea:focus,
+select:focus {
+  outline: none;
+  border-color: #2196F3;
+  box-shadow: 0 0 6px #bbdefb;
+}
+
+/* Radios y checkboxes alineados */
+.form-element div {
+  display: flex;
+  gap: 15px;
+  flex-wrap: wrap;
+  margin-top: 4px;
+}
+
+.form-element div label {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  cursor: pointer;
+  font-weight: 400;
+  color: #555;
+}
+
+/* Mensajes de error */
+.error small {
+  color: #e53935;
+  font-size: 0.875rem;
+  margin-top: 4px;
+}
+
+/* Botón submit */
+button[type="submit"] {
+  background-color: #1976d2;
+  color: white;
+  padding: 12px 28px;
+  font-size: 1.1rem;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: background-color 0.25s ease;
+  display: block;
+  margin: 0 auto;
+  margin-top: 15px;
+  box-shadow: 0 3px 8px rgb(25 118 210 / 0.4);
+}
+
+button[type="submit"]:hover:not(:disabled) {
+  background-color: #0d47a1;
+}
+
+button[type="submit"]:disabled {
+  background-color: #90caf9;
+  cursor: not-allowed;
+}
+.containerPagina {
+  margin: 0 auto;
+  margin-top: 50px;
+  padding: 16px;
+  background-color: #fff;
+  box-shadow: 0 2px 8px rgb(0 0 0 / 0.1);
+  border-radius: 8px;
+}
+
+.grid {
+  display: grid;
+  gap: 16px;
+}
+
+.form-element {
+  display: flex;
+  flex-direction: column;
+}
+
+label {
+  font-weight: 600;
+  margin-bottom: 6px;
+  font-size: 14px;
+}
+
+.input-field, .textarea-field, .select-field {
+  padding: 8px;
+  font-size: 14px;
+  border: 1.5px solid #ccc;
+  border-radius: 4px;
+  font-family: inherit;
+  resize: vertical;
+}
+
+.textarea-field {
+  min-height: 70px;
+}
+
+.radio-group, .checkbox-group {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.radio-label {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 14px;
+}
+
+.error {
+  color: #d32f2f;
+  margin-top: 4px;
+  font-size: 12px;
+}
+
+.button-container {
+  margin-top: 24px;
+  text-align: right;
+}
+
+.submit-button {
+  background-color: #1976d2;
+  color: white;
+  font-weight: 600;
+  padding: 10px 24px;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  font-size: 16px;
+  transition: background-color 0.3s ease;
+}
+
+.submit-button:disabled {
+  background-color: #90caf9;
+  cursor: not-allowed;
+}
+
+.submit-button:not(:disabled):hover {
+  background-color: #1565c0;
+}
+
+
+/* Responsive para pantallas pequeñas */
+@media (max-width: 600px) {
+  .grid {
+    grid-template-columns: 1fr !important; /* Forzar 1 columna en móvil */
+  }
+}
+}

+ 60 - 0
Front/src/app/modules/Padres/pages/formularios/formularios.component.html

@@ -0,0 +1,60 @@
+<form [formGroup]="form" (ngSubmit)="onSubmit()" *ngIf="form" class="containerPagina">
+
+  <mat-tab-group [(selectedIndex)]="selectedTabIndex">
+    <mat-tab *ngFor="let tab of configuration.tabs" [label]="tab.title">
+
+      <div class="grid" [style.gridTemplateColumns]="'repeat(' + tab.columns + ', 1fr)'" style="gap: 16px; padding: 16px;">
+        <div *ngFor="let el of tab.elements" class="form-element"
+             [style.gridColumnStart]="el.position.col + 1"
+             [style.gridRowStart]="el.position.row + 1">
+
+          <label [for]="el.element.name">{{ el.element.label }}</label>
+
+          <!-- Inputs tipo text, email, date, number -->
+          <input *ngIf="['text', 'email', 'date', 'number'].includes(el.element.type)"
+                 [attr.type]="el.element.type"
+                 [formControlName]="el.element.name"
+                 [attr.placeholder]="el.element.placeholder"
+                 class="input-field" />
+
+          <!-- Textarea -->
+          <textarea *ngIf="el.element.type === 'textarea'"
+                    [formControlName]="el.element.name"
+                    [attr.placeholder]="el.element.placeholder"
+                    class="textarea-field"></textarea>
+
+          <!-- Select -->
+          <select *ngIf="el.element.type === 'select'" [formControlName]="el.element.name" class="select-field">
+            <option *ngFor="let option of el.element.options" [value]="option">{{ option }}</option>
+          </select>
+
+          <!-- Radio buttons -->
+          <div *ngIf="el.element.type === 'radio'" class="radio-group">
+            <label *ngFor="let option of el.element.options" class="radio-label">
+              <input type="radio" [formControlName]="el.element.name" [value]="option" /> {{ option }}
+            </label>
+          </div>
+
+          <!-- Checkbox -->
+          <div *ngIf="el.element.type === 'checkbox'" class="checkbox-group">
+            <input type="checkbox" [formControlName]="el.element.name" /> {{ el.element.label }}
+          </div>
+
+          <!-- Validaciones simples -->
+          <div class="error" *ngIf="form.controls[el.element.name].invalid && form.controls[el.element.name].touched">
+            <small *ngIf="form.controls[el.element.name].errors?.['required']">{{ el.element.label }} es requerido.</small>
+          </div>
+
+        </div>
+      </div>
+
+    </mat-tab>
+  </mat-tab-group>
+
+  <div class="button-container" style="margin-top: 16px;">
+    <button type="submit" [disabled]="form.invalid" class="submit-button mat-raised-button mat-primary">
+      Enviar
+    </button>
+  </div>
+
+</form>

+ 23 - 0
Front/src/app/modules/Padres/pages/formularios/formularios.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FormulariosComponent } from './formularios.component';
+
+describe('FormulariosComponent', () => {
+  let component: FormulariosComponent;
+  let fixture: ComponentFixture<FormulariosComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [FormulariosComponent]
+    })
+    .compileComponents();
+    
+    fixture = TestBed.createComponent(FormulariosComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 84 - 0
Front/src/app/modules/Padres/pages/formularios/formularios.component.ts

@@ -0,0 +1,84 @@
+import { Component, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { FormService } from './../../../Administrador/services/FormService.service';
+
+@Component({
+  selector: 'app-formularios',
+  templateUrl: './formularios.component.html',
+  styleUrls: ['./formularios.component.css']
+})
+export class FormulariosComponent implements OnInit {
+  form!: FormGroup;
+  configuration: any;
+  selectedTabIndex = 0;
+
+  constructor(
+    private formService: FormService,
+    private fb: FormBuilder
+  ) {}
+
+  ngOnInit(): void {
+    this.obtenerFormularioPublicado();
+  }
+
+  obtenerFormularioPublicado(): void {
+    this.formService.getPublishedForm().subscribe({
+      next: (response: any) => {
+        if (response.success && response.form) {
+          console.log('Respuesta form:', response.form);
+          console.log('Configuration raw:', response.form.configuration);
+
+          this.configuration = response.form.configuration;
+
+          // Si configuration viene como string JSON, parsea a objeto
+          if (typeof this.configuration === 'string') {
+            try {
+              this.configuration = JSON.parse(this.configuration);
+              console.log('Configuration parseada:', this.configuration);
+            } catch (error) {
+              console.error('Error parseando configuración JSON:', error);
+              return;
+            }
+          }
+
+          this.reconstruirFormulario(this.configuration);
+        } else {
+          console.log('No se encontró formulario publicado');
+        }
+      },
+      error: (err) => {
+        console.error('Error al obtener el formulario:', err);
+      }
+    });
+  }
+
+  reconstruirFormulario(config: any): void {
+    if (!config || !config.tabs) {
+      console.error('Configuración inválida o vacía');
+      return;
+    }
+
+    const group: any = {};
+
+    config.tabs.forEach((tab: any) => {
+      tab.elements.forEach((el: any) => {
+        const controlName = el.element.name;
+        const validators = el.element.required ? [Validators.required] : [];
+        group[controlName] = this.fb.control('', validators);
+      });
+    });
+
+    this.form = this.fb.group(group);
+    console.log('Formulario reconstruido:', this.form);
+  }
+
+  onSubmit(): void {
+    if (this.form.valid) {
+      console.log('Formulario enviado con datos:', this.form.value);
+      // Aquí puedes enviar los datos al backend si lo necesitas
+    } else {
+      console.log('Formulario inválido');
+      this.form.markAllAsTouched();
+    }
+  }
+}

+ 4 - 0
Front/src/app/shared/sidebar/sidebar.component.ts

@@ -86,6 +86,9 @@ export class SidebarComponent {
     { icon: "account_circle", text: "Mi cuenta", url: "main" },
     { icon: "calendar_month", text: "Calendario", url: "main" },
     { icon: "tune", text: "Registro del Alumno", url: "registroAlumno" },
+            { icon: "tune", text: "formularios", url: "formularios" },
+
+
   ]
 
   public optionsAlumnos = [
@@ -101,6 +104,7 @@ export class SidebarComponent {
     { icon: "menu_book", text: "Asignar Tareas", url: "main" },
     { icon: "calendar_month", text: "Registrar en Bitácora", url: "registroBitacora" },
     { icon: "tune", text: "Registro de Calificaciones", url: "registroCalificaciones" },
+
   ]
 
   ngOnInit() {