Skip to main content

Overview

The Admin Panel provides authorized administrators with complete CRUD (Create, Read, Update, Delete) functionality for managing the library’s core entities: books, authors, and genres. The panel features separate management interfaces for each entity with form-based editing and list views.
Admin features are protected by role-based access control. Only users with ADMIN role can access these management interfaces.

Management Modules

The admin panel consists of three primary modules:

Book Management

Create, edit, and delete books with author and genre associations

Author Management

Manage author profiles with names and photo URLs

Genre Management

Organize books by creating and managing genre categories

Book Management

Components

  • List View: src/app/features/libros/libro-list/libro-list.ts:14
  • Form View: src/app/features/libros/libro-form/libro-form.ts:23
  • Service: src/app/core/services/libro.ts:10

Book List Features

The LibroListComponent displays all books with management actions:
export class LibroListComponent implements OnInit {
  private libroService = inject(LibroService);
  libros: Libro[] = [];

  ngOnInit(): void {
    this.cargarLibros();
  }

  cargarLibros(): void {
    this.libroService.getAll().subscribe({
      next: (data) => {
        this.libros = data;
        console.log('Libros cargados:', data);
      },
      error: (err) => {
        console.error('Error al cargar libros:', err);
      },
    });
  }
}
Source: libro-list.ts:14-33

Deleting Books

Admins can remove books with confirmation:
eliminarLibro(id: number): void {
  if (confirm('¿Estás seguro de eliminar este libro?')) {
    this.libroService.delete(id).subscribe({
      next: () => {
        this.cargarLibros();
        alert('Libro eliminado correctamente');
      },
      error: (err) => {
        console.error('Error al eliminar:', err);
        alert('No se pudo eliminar el libro.');
      },
    });
  }
}
Source: libro-list.ts:35-50

Book Form

The LibroFormComponent handles both creation and editing with complex relationships:
1

Load Dependencies

Fetch available authors and genres for dropdown selection
2

Populate Form

If editing, load existing book data including associated authors and genre
3

Validate Input

Ensure title, genre, and at least one author are selected
4

Submit

Create new book or update existing one with proper entity references

Form Initialization

export class LibroFormComponent implements OnInit {
  private libroService = inject(LibroService);
  private autorService = inject(AutorService);
  private generoService = inject(GeneroService);

  listaAutores: Autor[] = [];
  listaGeneros: Genero[] = [];
  
  selectedGeneroId: number | null = null;
  selectedAutoresIds: number[] = [];

  libro: Libro = {
    id: 0,
    titulo: '',
    portada: '',
    anioPublicacion: new Date().getFullYear(),
    disponible: true,
    genero: { id: 0, nombre: '' },
    autores: [],
  };

  isEditing: boolean = false;
}
Source: libro-form.ts:23-50

Loading Dropdown Data

cargarListas(): void {
  // Cargar Generos
  this.generoService.getAll().subscribe({
    next: (data) => (this.listaGeneros = data),
    error: (err) => console.error('Error cargando géneros', err),
  });

  // Cargar Autores
  this.autorService.getAll().subscribe({
    next: (data) => (this.listaAutores = data),
    error: (err) => console.error('Error cargando autores', err),
  });
}
Source: libro-form.ts:64-76

Form Submission with Validation

onSubmit(): void {
  // Validaciones
  if (!this.libro.titulo.trim()) {
    alert('El título es obligatorio');
    return;
  }
  if (!this.selectedGeneroId) {
    alert('Debes seleccionar un género');
    return;
  }
  if (this.selectedAutoresIds.length === 0) {
    alert('Debes seleccionar al menos un autor');
    return;
  }

  const libroParaEnviar = {
    ...this.libro,
    genero: { id: this.selectedGeneroId },
    autores: this.selectedAutoresIds.map((id) => ({ id: Number(id) })),
  };

  if (!this.isEditing) {
    delete (libroParaEnviar as any).id;
  }

  // Enviar
  if (this.isEditing) {
    this.libroService.update(this.libro.id, libroParaEnviar).subscribe({
      next: () => {
        alert('Libro actualizado correctamente');
        this.router.navigate(['/libros']);
      },
      error: (err) => alert('Error al actualizar'),
    });
  } else {
    this.libroService.create(libroParaEnviar).subscribe({
      next: () => {
        alert('Libro creado correctamente');
        this.router.navigate(['/libros']);
      },
      error: (err) => alert('Error al crear'),
    });
  }
}
Source: libro-form.ts:97-149

Book Service API

export class LibroService {
  private apiUrl = `${environment.apiUrl}/libros`;

  getAll(): Observable<Libro[]>
  getById(id: number): Observable<Libro>
  create(libro: any): Observable<Libro>
  update(id: number, libro: any): Observable<Libro>
  delete(id: number): Observable<any>
}
Source: libro.ts:10-40

Author Management

Components

  • List View: src/app/features/autores/autor-list/autor-list.ts:14
  • Form View: src/app/features/autores/autor-form/autor-form.ts:14
  • Service: src/app/core/services/autor.ts:10

Author Form

The author form is simpler, requiring only name and optional photo URL:
export class AutorFormComponent implements OnInit {
  private autorService = inject(AutorService);
  private router = inject(Router);

  autor: Autor = {
    id: 0,
    nombre: '',
    urlFoto: '',
  };

  isEditing: boolean = false;

  onSubmit(): void {
    if (!this.autor.nombre.trim()) {
      alert('El nombre es obligatorio');
      return;
    }

    if (this.isEditing) {
      this.autorService.update(this.autor.id, this.autor).subscribe({
        next: () => {
          alert('Autor actualizado correctamente');
          this.router.navigate(['/autores']);
        },
        error: (err) => alert('Error al actualizar el autor.'),
      });
    } else {
      const autorParaGuardar = {
        nombre: this.autor.nombre,
        urlFoto: this.autor.urlFoto,
      };

      this.autorService.create(autorParaGuardar as any).subscribe({
        next: () => {
          alert('Autor creado correctamente');
          this.router.navigate(['/autores']);
        },
        error: (err) => alert('Error al crear el autor.'),
      });
    }
  }
}
Source: autor-form.ts:14-91

Deleting Authors

eliminarAutor(id: number): void {
  if (confirm('¿Estás seguro de eliminar este autor?')) {
    this.autorService.delete(id).subscribe({
      next: () => {
        this.cargarAutores();
      },
      error: (err) => {
        console.error('Error al eliminar:', err);
        alert(
          'No se pudo eliminar el autor (quizás tiene libros asociados).'
        );
      },
    });
  }
}
Source: autor-list.ts:35-49
Deleting authors with associated books will fail due to database constraints. The system alerts users when deletion is blocked by existing relationships.

Author Service API

export class AutorService {
  private apiUrl = `${environment.apiUrl}/autores`;

  getAll(): Observable<Autor[]>
  getById(id: number): Observable<Autor>
  create(autor: Autor): Observable<Autor>
  update(id: number, autor: Autor): Observable<Autor>
  delete(id: number): Observable<any>
}
Source: autor.ts:10-39

Genre Management

Components

  • List View: src/app/features/generos/genero-list/genero-list.ts:14
  • Form View: src/app/features/generos/genero-form/genero-form.ts:15
  • Service: src/app/core/services/genero.ts:10

Genre Form

The genre form is the simplest, requiring only a name:
export class GeneroFormComponent implements OnInit {
  private generoService = inject(GeneroService);
  
  genero: Genero = {
    id: 0,
    nombre: '',
  };

  isEditing: boolean = false;

  onSubmit(): void {
    if (!this.genero.nombre.trim()) {
      alert('El nombre del género es obligatorio');
      return;
    }

    if (this.isEditing) {
      this.generoService.update(this.genero.id, this.genero).subscribe({
        next: () => {
          alert('Género actualizado correctamente');
          this.router.navigate(['/generos']);
        },
        error: (err) => alert('Error al actualizar el género.'),
      });
    } else {
      const generoParaGuardar = {
        nombre: this.genero.nombre,
      };

      this.generoService.create(generoParaGuardar as any).subscribe({
        next: () => {
          alert('Género creado correctamente');
          this.router.navigate(['/generos']);
        },
        error: (err) => alert('Error al crear el género.'),
      });
    }
  }
}
Source: genero-form.ts:15-87

Deleting Genres

eliminarGenero(id: number): void {
  if (confirm('¿Estás seguro de eliminar este género?')) {
    this.generoService.delete(id).subscribe({
      next: () => {
        this.cargarGeneros();
        alert('Género eliminado correctamente');
      },
      error: (err) => {
        console.error('Error al eliminar:', err);
        alert(
          'No se pudo eliminar el género. Es probable que existan libros asociados a él.'
        );
      },
    });
  }
}
Source: genero-list.ts:36-53

Genre Service API

export class GeneroService {
  private apiUrl = `${environment.apiUrl}/generos`;

  getAll(): Observable<Genero[]>
  getById(id: number): Observable<Genero>
  create(genero: Genero): Observable<Genero>
  update(id: number, genero: Genero): Observable<Genero>
  delete(id: number): Observable<any>
}
Source: genero.ts:10-38

Common Patterns

All three management modules share consistent patterns:

CRUD Operations

Create

Navigate to form, fill required fields, submit to create new entity

Read

List view displays all entities with key information

Update

Edit button loads entity data into form for modification

Delete

Delete button with confirmation dialog removes entity

Form Modes

All forms support dual modes determined by route parameters:
ngOnInit(): void {
  const id = this.route.snapshot.paramMap.get('id');
  
  if (id) {
    this.isEditing = true;
    this.cargarEntidad(Number(id));
  } else {
    this.isEditing = false;
  }
}
  • Create Mode: No ID parameter, form displays empty
  • Edit Mode: ID parameter present, form loads existing data
Common routing pattern for each module:
  • /libros - Book list
  • /libros/new - Create book
  • /libros/edit/:id - Edit book
  • /autores - Author list
  • /autores/new - Create author
  • /autores/edit/:id - Edit author
  • /generos - Genre list
  • /generos/new - Create genre
  • /generos/edit/:id - Edit genre

Data Relationships

Entity Relationships

Libro (Book)
  ├─ genero: Genero (Many-to-One)
  └─ autores: Autor[] (Many-to-Many)

Autor (Author)
  └─ libros: Libro[] (Many-to-Many)

Genero (Genre)
  └─ libros: Libro[] (One-to-Many)

Referential Integrity

The system enforces referential integrity:
  • Deleting an author with associated books fails
  • Deleting a genre with associated books fails
  • Books can only reference existing authors and genres

API Endpoints Summary

Books

  • GET /libros - List all books
  • GET /libros/{id} - Get book by ID
  • POST /libros - Create book
  • PUT /libros/{id} - Update book
  • DELETE /libros/{id} - Delete book

Authors

  • GET /autores - List all authors
  • GET /autores/{id} - Get author by ID
  • POST /autores - Create author
  • PUT /autores/{id} - Update author
  • DELETE /autores/{id} - Delete author

Genres

  • GET /generos - List all genres
  • GET /generos/{id} - Get genre by ID
  • POST /generos - Create genre
  • PUT /generos/{id} - Update genre
  • DELETE /generos/{id} - Delete genre

Features Summary

Complete CRUD

Full create, read, update, delete operations for all entities

Relationship Management

Handle complex many-to-many and one-to-many relationships

Form Validation

Client-side validation ensures data integrity before submission

Error Handling

User-friendly error messages for constraint violations

Build docs developers (and LLMs) love