Modelo Client - CRUD
Crearemos las peticiones desde el frontend para el modelo Client.
Para ello realizamos paso a paso para que relliquemos lo mismo a los demas modelos
1. Realizamos el Modelo Client
Creamos el modelo en src/app/models/client.ts
export interface ClientI {
id?: number;
name: string;
address: string;
phone: string;
email: string;
password: string;
status: "ACTIVE" | "INACTIVE";
}
export interface ClientResponseI {
id?: number;
name: string;
address: string;
phone: string;
email: string;
}
2. Realizamos el Servicio Client
Creamos el modelo en src/app/services/client.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, BehaviorSubject, tap } from 'rxjs';
import { ClientI} from '../models/client';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class ClientService {
private baseUrl = 'http://localhost:4000/api/clients';
private clientsSubject = new BehaviorSubject<ClientI[]>([]);
public clients$ = this.clientsSubject.asObservable();
constructor(
private http: HttpClient,
private authService: AuthService
) {}
private getHeaders(): HttpHeaders {
let headers = new HttpHeaders();
const token = this.authService.getToken();
if (token) {
headers = headers.set('Authorization', `Bearer ${token}`);
}
return headers;
}
getAllClients(): Observable<ClientI[]> {
return this.http.get<ClientI[]>(this.baseUrl, { headers: this.getHeaders() })
// .pipe(
// tap(response => {
// // console.log('Fetched clients:', response);
// })
// )
;
}
getClientById(id: number): Observable<ClientI> {
return this.http.get<ClientI>(`${this.baseUrl}/${id}`, { headers: this.getHeaders() });
}
createClient(client: ClientI): Observable<ClientI> {
return this.http.post<ClientI>(this.baseUrl, client, { headers: this.getHeaders() });
}
updateClient(id: number, client: ClientI): Observable<ClientI> {
return this.http.patch<ClientI>(`${this.baseUrl}/${id}`, client, { headers: this.getHeaders() });
}
deleteClient(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`, { headers: this.getHeaders() });
}
deleteClientLogic(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}/logic`, { headers: this.getHeaders() });
}
// Método para actualizar el estado local de clientes
updateLocalClients(clients: ClientI[]): void {
this.clientsSubject.next(clients);
}
refreshClients(): void {
this.getAllClients().subscribe(clients => {
this.clientsSubject.next(clients);
});
}
}
2. Enlazamos con el componente Client
a. Iniciamos con mostrar todo (getall.ts y getall.html)
Nota:
Como vamos a traer los datos que son enviados desde el controlador del backend. se requiere antes revisar el codigomde dicho controlador:
client.controller.ts
public async getAllClients(req: Request, res: Response) {
try {
const clients: ClientI[] = await Client.findAll({
where: { status: 'ACTIVE' },
});
res.status(200).json(clients );
} catch (error) {
res.status(500).json({ error: "Error fetching clients" });
}
}
src/app/components/client/getall.ts
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { TableModule } from 'primeng/table';
import { CommonModule } from '@angular/common';
import { ClientI } from '../../../models/client';
import { ButtonModule } from 'primeng/button';
import { RouterModule } from '@angular/router';
import { ClientService } from '../../../services/client.service';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ToastModule } from 'primeng/toast';
@Component({
selector: 'app-getall',
imports: [TableModule, CommonModule, ButtonModule, RouterModule, ConfirmDialogModule, ToastModule],
templateUrl: './getall.html',
styleUrl: './getall.css',
encapsulation: ViewEncapsulation.None,
providers: [ConfirmationService, MessageService]
})
export class Getall implements OnInit {
clients: ClientI[] = [];
loading: boolean = false;
constructor(
private clientService: ClientService,
private confirmationService: ConfirmationService,
private messageService: MessageService
) {}
ngOnInit(): void {
this.loadClients();
}
loadClients(): void {
this.loading = true;
this.clientService.getAllClients().subscribe({
next: (clients) => {
this.clients = clients;
this.clientService.updateLocalClients(clients);
this.loading = false;
},
error: (error) => {
console.error('Error loading clients:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Error al cargar los clientes'
});
this.loading = false;
}
});
}
deleteClient(client: ClientI): void {
this.confirmationService.confirm({
message: `¿Está seguro de eliminar el cliente ${client.name}?`,
header: 'Confirmar eliminación',
icon: 'pi pi-exclamation-triangle',
accept: () => {
if (client.id) {
this.clientService.deleteClient(client.id).subscribe({
next: () => {
this.messageService.add({
severity: 'success',
summary: 'Éxito',
detail: 'Cliente eliminado correctamente'
});
this.loadClients();
},
error: (error) => {
console.error('Error deleting client:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Error al eliminar el cliente'
});
}
});
}
}
});
}
}
src/app/components/client/getall.html
<div class="card p-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold">Gestión de Clientes</h2>
<button
pButton
type="button"
class="p-button p-component bg-blue-500 hover:bg-blue-600 text-white"
icon="pi pi-plus"
label="Nuevo Cliente"
[routerLink]="['/clients/new']"
>
</button>
</div>
<p-table
[value]="clients"
[loading]="loading"
stripedRows
[tableStyle]="{'min-width': '50rem'}"
[paginator]="true"
[rows]="10"
[showCurrentPageReport]="true"
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} registros"
>
<ng-template #header>
<tr>
<th class="text-left">ID</th>
<th class="text-left">Nombre</th>
<th class="text-left">Dirección</th>
<th class="text-left">Teléfono</th>
<th class="text-left">Email</th>
<th class="text-left">Estado</th>
<th class="text-center">Acciones</th>
</tr>
</ng-template>
<ng-template #body let-client>
<tr>
<td>{{client.id}}</td>
<td>{{client.name}}</td>
<td>{{client.address}}</td>
<td>{{client.phone}}</td>
<td>{{client.email}}</td>
<td>
<span
class="px-2 py-1 rounded text-xs font-semibold"
[class]="client.status === 'ACTIVE' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
>
{{client.status}}
</span>
</td>
<td class="text-center">
<div class="flex justify-center gap-2">
<button
pButton
type="button"
icon="pi pi-pencil"
class="p-button-rounded p-button-text p-button-warning"
[routerLink]="['/clients/edit', client.id]"
pTooltip="Editar"
></button>
<button
pButton
type="button"
icon="pi pi-trash"
class="p-button-rounded p-button-text p-button-danger"
(click)="deleteClient(client)"
pTooltip="Eliminar"
></button>
</div>
</td>
</tr>
</ng-template>
</p-table>
</div>
<p-confirmDialog></p-confirmDialog>
<p-toast></p-toast>
Corfirmamos las rutas en Confimamos en src/app.routes.ts
import { Routes } from '@angular/router';
import { Login } from './components/auth/login/login';
import { Register } from './components/auth/register/register';
import { AuthGuard } from './guards/authguard';
// Client components with aliases
import { Getall as ClientGetall } from './components/client/getall/getall';
import { Create as ClientCreate } from './components/client/create/create';
import { Update as ClientUpdate } from './components/client/update/update';
import { Delete as ClientDelete } from './components/client/delete/delete';
// Product components with aliases
import { Getall as ProductGetall } from './components/product/getall/getall';
import { Create as ProductCreate } from './components/product/create/create';
import { Update as ProductUpdate } from './components/product/update/update';
import { Delete as ProductDelete } from './components/product/delete/delete';
// Sale components with aliases
import { Getall as SaleGetall } from './components/sale/getall/getall';
import { Create as SaleCreate } from './components/sale/create/create';
import { Update as SaleSaleUpdate } from './components/sale/update/update';
import { Delete as SaleDelete } from './components/sale/delete/delete';
export const routes: Routes = [
{
path: '',
redirectTo: '/login',
pathMatch: 'full'
},
{
path: "login",
component: Login
},
{
path: "register",
component: Register
},
{
path: "clients",
component: ClientGetall,
canActivate: [AuthGuard]
},
{
path: "clients/new",
component: ClientCreate,
canActivate: [AuthGuard]
},
{
path: "clients/edit/:id",
component: ClientUpdate,
canActivate: [AuthGuard]
},
{
path: "clients/delete/:id",
component: ClientDelete,
canActivate: [AuthGuard]
},
{
path: "products",
component: ProductGetall,
canActivate: [AuthGuard]
},
{
path: "products/new",
component: ProductCreate,
canActivate: [AuthGuard]
},
{
path: "products/edit/:id",
component: ProductUpdate,
canActivate: [AuthGuard]
},
{
path: "products/delete/:id",
component: ProductDelete,
canActivate: [AuthGuard]
},
{
path: "sales",
component: SaleGetall,
canActivate: [AuthGuard]
},
{
path: "sales/new",
component: SaleCreate,
canActivate: [AuthGuard]
},
{
path: "sales/edit/:id",
component: SaleSaleUpdate,
canActivate: [AuthGuard]
},
{
path: "sales/delete/:id",
component: SaleDelete,
canActivate: [AuthGuard]
},
{
path: "**",
redirectTo: "/login",
pathMatch: "full"
}
];
Confimamos en src/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeuix/themes/aura';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimationsAsync(),
provideHttpClient(),
providePrimeNG({
theme: {
preset: Aura
}
})
]
};
Al ejecutar en el terminal ng serve

Si no te aparecen los iconos en las acciones, es necesario instlalar PrimeIcons
En el terminal
Y en el archivo src/styles.css adicional al final
b. Create (create.ts y create.html)
Nota:
Como vamos a traer los datos que son enviados desde el controlador del backend. se requiere antes revisar el codigomde dicho controlador:
client.controller.ts
public async createClient(req: Request, res: Response) {
const { id, name, address, phone, email, password, status } = req.body;
try {
let body: ClientI = {
name,
address,
phone,
email,
password,
status,
};
const newClient = await Client.create({ ...body });
res.status(201).json(newClient);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
src/app/components/client/create.ts
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { ButtonModule } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { Select } from 'primeng/select';
import { MessageService } from 'primeng/api';
import { ToastModule } from 'primeng/toast';
import { ClientService } from '../../../services/client.service';
@Component({
selector: 'app-create',
imports: [CommonModule, ReactiveFormsModule, ButtonModule, InputTextModule, Select, ToastModule],
templateUrl: './create.html',
styleUrl: './create.css',
providers: [MessageService]
})
export class Create {
form: FormGroup;
loading: boolean = false;
statusOptions = [
{ label: 'Activo', value: 'ACTIVE' },
{ label: 'Inactivo', value: 'INACTIVE' }
];
constructor(
private fb: FormBuilder,
private router: Router,
private clientService: ClientService,
private messageService: MessageService
) {
this.form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
address: ['', [Validators.required, Validators.minLength(5)]],
phone: ['', [Validators.required, Validators.pattern(/^\d{10,15}$/)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
status: ['ACTIVE', Validators.required]
});
}
submit(): void {
if (this.form.valid) {
this.loading = true;
const clientData = this.form.value;
this.clientService.createClient(clientData).subscribe({
next: (response) => {
this.messageService.add({
severity: 'success',
summary: 'Éxito',
detail: 'Cliente creado correctamente'
});
setTimeout(() => {
this.router.navigate(['/clients']);
}, 1000);
},
error: (error) => {
console.error('Error creating client:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Error al crear el cliente'
});
this.loading = false;
}
});
} else {
this.markFormGroupTouched();
this.messageService.add({
severity: 'warn',
summary: 'Advertencia',
detail: 'Por favor complete todos los campos requeridos'
});
}
}
cancelar(): void {
this.router.navigate(['/clients']);
}
private markFormGroupTouched(): void {
Object.keys(this.form.controls).forEach(key => {
this.form.get(key)?.markAsTouched();
});
}
getFieldError(fieldName: string): string {
const field = this.form.get(fieldName);
if (field?.errors && field?.touched) {
if (field.errors['required']) return `${fieldName} es requerido`;
if (field.errors['email']) return 'Email no válido';
if (field.errors['minlength']) return `${fieldName} debe tener al menos ${field.errors['minlength'].requiredLength} caracteres`;
if (field.errors['pattern']) return 'Formato no válido';
}
return '';
}
}
src/app/components/client/create.html
<div class="max-w-4xl mx-auto p-6">
<div class="bg-white rounded-lg shadow-lg p-8">
<h2 class="text-2xl font-bold mb-6 text-gray-800">Crear Nuevo Cliente</h2>
<form [formGroup]="form" (ngSubmit)="submit()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Nombre *</label>
<input
pInputText
formControlName="name"
class="w-full"
[class.ng-invalid]="form.get('name')?.invalid && form.get('name')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('name')">
{{ getFieldError('name') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Dirección *</label>
<input
pInputText
formControlName="address"
class="w-full"
[class.ng-invalid]="form.get('address')?.invalid && form.get('address')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('address')">
{{ getFieldError('address') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Teléfono *</label>
<input
pInputText
formControlName="phone"
class="w-full"
placeholder="1234567890"
[class.ng-invalid]="form.get('phone')?.invalid && form.get('phone')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('phone')">
{{ getFieldError('phone') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Email *</label>
<input
pInputText
type="email"
formControlName="email"
class="w-full"
[class.ng-invalid]="form.get('email')?.invalid && form.get('email')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('email')">
{{ getFieldError('email') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Contraseña *</label>
<input
pInputText
type="password"
formControlName="password"
class="w-full"
[class.ng-invalid]="form.get('password')?.invalid && form.get('password')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('password')">
{{ getFieldError('password') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Estado *</label>
<p-select
formControlName="status"
[options]="statusOptions"
placeholder="Seleccionar estado"
class="w-full"
></p-select>
</div>
</div>
<div class="flex justify-center mt-8">
<div class="bg-gray-50 rounded-lg p-4 flex gap-4 items-center">
<button
pButton
type="submit"
class="p-button bg-blue-500 hover:bg-blue-600 text-white"
label="Guardar"
icon="pi pi-save"
[loading]="loading"
[disabled]="loading"
></button>
<button
pButton
type="button"
class="p-button p-button-secondary"
label="Cancelar"
icon="pi pi-times"
(click)="cancelar()"
[disabled]="loading"
></button>
</div>
</div>
</form>
</div>
</div>
<p-toast></p-toast>
c. Create (update.ts y update.html)
Nota:
Como vamos a traer los datos que son enviados desde el controlador del backend. se requiere antes revisar el codigomde dicho controlador:
client.controller.ts
// Get a client by ID
public async getClientById(req: Request, res: Response) {
try {
const { id: pk } = req.params;
const client = await Client.findOne({
where: {
id: pk,
status: 'ACTIVE' },
});
if (client) {
res.status(200).json(client);
} else {
res.status(404).json({ error: "Client not found or inactive" });
}
} catch (error) {
res.status(500).json({ error: "Error fetching client" });
}
}
// Update a client
public async updateClient(req: Request, res: Response) {
const { id: pk } = req.params;
const { id, name, address, phone, email, password, status } = req.body;
try {
let body: ClientI = {
name,
address,
phone,
email,
password,
status,
};
const clientExist = await Client.findOne({
where: {
id: pk,
status: 'ACTIVE' },
});
if (clientExist) {
await clientExist.update(body, {
where: { id: pk },
});
res.status(200).json(clientExist);
} else {
res.status(404).json({ error: "Client not found or inactive" });
}
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
src/app/components/client/update.ts
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ButtonModule } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { Select } from 'primeng/select';
import { MessageService } from 'primeng/api';
import { ToastModule } from 'primeng/toast';
import { ClientService } from '../../../services/client.service';
import { ClientI } from '../../../models/client';
@Component({
selector: 'app-update',
imports: [CommonModule, ReactiveFormsModule, ButtonModule, InputTextModule, Select, ToastModule],
templateUrl: './update.html',
styleUrl: './update.css',
providers: [MessageService]
})
export class Update implements OnInit {
form: FormGroup;
loading: boolean = false;
clientId: number = 0;
statusOptions = [
{ label: 'Activo', value: 'ACTIVE' },
{ label: 'Inactivo', value: 'INACTIVE' }
];
constructor(
private fb: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private clientService: ClientService,
private messageService: MessageService
) {
this.form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
address: ['', [Validators.required, Validators.minLength(5)]],
phone: ['', [Validators.required, Validators.pattern(/^\d{10,15}$/)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
status: ['ACTIVE', Validators.required]
});
}
ngOnInit(): void {
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.clientId = parseInt(id);
this.loadClient();
}
}
loadClient(): void {
this.loading = true;
this.clientService.getClientById(this.clientId).subscribe({
next: (client) => {
this.form.patchValue(client);
this.loading = false;
},
error: (error) => {
console.error('Error loading client:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Error al cargar el cliente'
});
this.loading = false;
}
});
}
submit(): void {
if (this.form.valid) {
this.loading = true;
const clientData = this.form.value;
this.clientService.updateClient(this.clientId, clientData).subscribe({
next: (response) => {
this.messageService.add({
severity: 'success',
summary: 'Éxito',
detail: 'Cliente actualizado correctamente'
});
setTimeout(() => {
this.router.navigate(['/clients']);
}, 1000);
},
error: (error) => {
console.error('Error updating client:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Error al actualizar el cliente'
});
this.loading = false;
}
});
} else {
this.markFormGroupTouched();
this.messageService.add({
severity: 'warn',
summary: 'Advertencia',
detail: 'Por favor complete todos los campos requeridos'
});
}
}
cancelar(): void {
this.router.navigate(['/clients']);
}
private markFormGroupTouched(): void {
Object.keys(this.form.controls).forEach(key => {
this.form.get(key)?.markAsTouched();
});
}
getFieldError(fieldName: string): string {
const field = this.form.get(fieldName);
if (field?.errors && field?.touched) {
if (field.errors['required']) return `${fieldName} es requerido`;
if (field.errors['email']) return 'Email no válido';
if (field.errors['minlength']) return `${fieldName} debe tener al menos ${field.errors['minlength'].requiredLength} caracteres`;
if (field.errors['pattern']) return 'Formato no válido';
}
return '';
}
}
src/app/components/client/update.html
<div class="max-w-4xl mx-auto p-6">
<div class="bg-white rounded-lg shadow-lg p-8">
<h2 class="text-2xl font-bold mb-6 text-gray-800">Editar Cliente</h2>
<form [formGroup]="form" (ngSubmit)="submit()" *ngIf="!loading">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Nombre *</label>
<input
pInputText
formControlName="name"
class="w-full"
[class.ng-invalid]="form.get('name')?.invalid && form.get('name')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('name')">
{{ getFieldError('name') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Dirección *</label>
<input
pInputText
formControlName="address"
class="w-full"
[class.ng-invalid]="form.get('address')?.invalid && form.get('address')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('address')">
{{ getFieldError('address') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Teléfono *</label>
<input
pInputText
formControlName="phone"
class="w-full"
[class.ng-invalid]="form.get('phone')?.invalid && form.get('phone')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('phone')">
{{ getFieldError('phone') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Email *</label>
<input
pInputText
type="email"
formControlName="email"
class="w-full"
[class.ng-invalid]="form.get('email')?.invalid && form.get('email')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('email')">
{{ getFieldError('email') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Contraseña *</label>
<input
pInputText
type="password"
formControlName="password"
class="w-full"
[class.ng-invalid]="form.get('password')?.invalid && form.get('password')?.touched"
/>
<small class="text-red-500 mt-1" *ngIf="getFieldError('password')">
{{ getFieldError('password') }}
</small>
</div>
<div class="flex flex-col">
<label class="block mb-2 font-semibold text-gray-700">Estado *</label>
<p-select
formControlName="status"
[options]="statusOptions"
placeholder="Seleccionar estado"
class="w-full"
></p-select>
</div>
</div>
<div class="flex justify-center mt-8">
<div class="bg-gray-50 rounded-lg p-4 flex gap-4 items-center">
<button
pButton
type="submit"
class="p-button bg-blue-500 hover:bg-blue-600 text-white"
label="Actualizar"
icon="pi pi-save"
[loading]="loading"
[disabled]="loading"
></button>
<button
pButton
type="button"
class="p-button p-button-secondary"
label="Cancelar"
icon="pi pi-times"
(click)="cancelar()"
[disabled]="loading"
></button>
</div>
</div>
</form>
<div class="flex justify-center" *ngIf="loading">
<i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
</div>
</div>
</div>
<p-toast></p-toast>