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 } from '@angular/common/http';
import { Observable, BehaviorSubject, tap, catchError, throwError } from 'rxjs';
import { map } from 'rxjs/operators';
import { ClientI, ClientResponseI } from '../models/client';
// Interfaz para la respuesta paginada de Django
interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
}
@Injectable({
providedIn: 'root'
})
export class ClientService {
private baseUrl = 'http://localhost:8000/api/clients';
private clientsSubject = new BehaviorSubject<ClientResponseI[]>([]);
public clients$ = this.clientsSubject.asObservable();
constructor(private http: HttpClient) {}
getAllClients(): Observable<ClientResponseI[]> {
return this.http.get<PaginatedResponse<ClientResponseI>>(`${this.baseUrl}/`)
.pipe(
map(response => response.results), // Extraer solo el array de results
tap(clients => {
console.log('Fetched clients:', clients);
this.clientsSubject.next(clients);
}),
catchError(error => {
console.error('Error fetching clients:', error);
return throwError(() => error);
})
);
}
getClientById(id: number): Observable<ClientResponseI> {
return this.http.get<ClientResponseI>(`${this.baseUrl}/${id}/`)
.pipe(
catchError(error => {
console.error('Error fetching client:', error);
return throwError(() => error);
})
);
}
createClient(client: ClientI): Observable<ClientResponseI> {
return this.http.post<ClientResponseI>(`${this.baseUrl}/`, client)
.pipe(
tap(response => {
console.log('Client created:', response);
this.refreshClients();
}),
catchError(error => {
console.error('Error creating client:', error);
return throwError(() => error);
})
);
}
updateClient(id: number, client: Partial<ClientI>): Observable<ClientResponseI> {
return this.http.put<ClientResponseI>(`${this.baseUrl}/${id}/`, client)
.pipe(
tap(response => {
console.log('Client updated:', response);
this.refreshClients();
}),
catchError(error => {
console.error('Error updating client:', error);
return throwError(() => error);
})
);
}
partialUpdateClient(id: number, client: Partial<ClientI>): Observable<ClientResponseI> {
return this.http.patch<ClientResponseI>(`${this.baseUrl}/${id}/`, client)
.pipe(
tap(response => {
console.log('Client partially updated:', response);
this.refreshClients();
}),
catchError(error => {
console.error('Error partially updating client:', error);
return throwError(() => error);
})
);
}
deleteClient(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}/`)
.pipe(
tap(() => {
console.log('Client deleted:', id);
this.refreshClients();
}),
catchError(error => {
console.error('Error deleting client:', error);
return throwError(() => error);
})
);
}
// Método para actualizar el estado local de clientes
updateLocalClients(clients: ClientResponseI[]): void {
this.clientsSubject.next(clients);
}
refreshClients(): void {
this.getAllClients().subscribe({
next: (clients) => {
this.clientsSubject.next(clients);
},
error: (error) => {
console.error('Error refreshing clients:', error);
}
});
}
}
2. Enlazamos con el componente Client
a. Iniciamos con mostrar todo (getall.ts y getall.html)
src/app/components/client/getall.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { TableModule } from 'primeng/table';
import { Button } from 'primeng/button';
import { ConfirmDialog } from 'primeng/confirmdialog';
import { Toast } from 'primeng/toast';
import { Tooltip } from 'primeng/tooltip';
import { ConfirmationService, MessageService } from 'primeng/api';
import { Subscription } from 'rxjs';
import { ClientService } from '../../../services/client.service';
import { ClientResponseI } from '../../../models/client';
@Component({
selector: 'app-client-getall',
standalone: true,
imports: [
CommonModule,
RouterModule,
TableModule,
Button,
ConfirmDialog,
Toast,
Tooltip
],
providers: [ConfirmationService, MessageService],
templateUrl: './getall.html',
styleUrl: './getall.css'
})
export class Getall implements OnInit, OnDestroy {
clients: ClientResponseI[] = [];
loading = false;
private subscription = new Subscription();
constructor(
private clientService: ClientService,
private confirmationService: ConfirmationService,
private messageService: MessageService
) {}
ngOnInit(): void {
this.loadClients();
// Subscribe to client updates
this.subscription.add(
this.clientService.clients$.subscribe(clients => {
this.clients = clients;
})
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
loadClients(): void {
this.loading = true;
this.subscription.add(
this.clientService.getAllClients().subscribe({
next: (clients) => {
this.clients = clients;
this.loading = false;
},
error: (error) => {
console.error('Error loading clients:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar los clientes'
});
this.loading = false;
}
})
);
}
confirmDelete(client: ClientResponseI): void {
this.confirmationService.confirm({
message: `¿Está seguro de que desea eliminar a ${client.name}?`,
header: 'Confirmar Eliminación',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Sí, eliminar',
rejectLabel: 'Cancelar',
acceptButtonStyleClass: 'p-button-danger',
accept: () => {
this.deleteClient(client.id!);
}
});
}
deleteClient(id: number): void {
this.subscription.add(
this.clientService.deleteClient(id).subscribe({
next: () => {
this.messageService.add({
severity: 'success',
summary: 'Éxito',
detail: 'Cliente eliminado correctamente'
});
},
error: (error) => {
console.error('Error deleting client:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudo 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>
<p-button
label="Nuevo Cliente"
icon="pi pi-plus"
[routerLink]="['/clients/new']"
styleClass="p-button-success"
></p-button>
</div>
<p-table
[value]="clients"
[loading]="loading"
[paginator]="true"
[rows]="10"
[showCurrentPageReport]="true"
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} registros"
[rowsPerPageOptions]="[5, 10, 25, 50]"
dataKey="id"
styleClass="p-datatable-striped"
[tableStyle]="{'min-width': '50rem'}"
>
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="id">
ID <p-sortIcon field="id"></p-sortIcon>
</th>
<th pSortableColumn="name">
Nombre <p-sortIcon field="name"></p-sortIcon>
</th>
<th pSortableColumn="address">
Dirección <p-sortIcon field="address"></p-sortIcon>
</th>
<th pSortableColumn="phone">
Teléfono <p-sortIcon field="phone"></p-sortIcon>
</th>
<th pSortableColumn="email">
Email <p-sortIcon field="email"></p-sortIcon>
</th>
<th class="text-center">Acciones</th>
</tr>
</ng-template>
<ng-template pTemplate="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 class="text-center">
<div class="flex justify-center gap-2">
<p-button
icon="pi pi-pencil"
[routerLink]="['/clients/edit', client.id]"
styleClass="p-button-rounded p-button-text p-button-warning"
pTooltip="Editar"
tooltipPosition="top"
></p-button>
<p-button
icon="pi pi-trash"
(onClick)="confirmDelete(client)"
styleClass="p-button-rounded p-button-text p-button-danger"
pTooltip="Eliminar"
tooltipPosition="top"
></p-button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="6" class="text-center p-4">
<div class="flex flex-col items-center gap-3">
<i class="pi pi-info-circle text-4xl text-gray-400"></i>
<span class="text-gray-500">No se encontraron clientes</span>
<p-button
label="Crear Primer Cliente"
icon="pi pi-plus"
[routerLink]="['/clients/new']"
styleClass="p-button-sm p-button-outlined"
></p-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';
// 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 SaleUpdate } from './components/sale/update/update';
import { Delete as SaleDelete } from './components/sale/delete/delete';
export const routes: Routes = [
{
path: '',
redirectTo: '/clients',
pathMatch: 'full'
},
{
path: "clients",
component: ClientGetall
},
{
path: "clients/new",
component: ClientCreate
},
{
path: "clients/edit/:id",
component: ClientUpdate
},
{
path: "clients/delete/:id",
component: ClientDelete
},
{
path: "products",
component: ProductGetall
},
{
path: "products/new",
component: ProductCreate
},
{
path: "products/edit/:id",
component: ProductUpdate
},
{
path: "products/delete/:id",
component: ProductDelete
},
{
path: "sales",
component: SaleGetall
},
{
path: "sales/new",
component: SaleCreate
},
{
path: "sales/edit/:id",
component: SaleUpdate
},
{
path: "sales/delete/:id",
component: SaleDelete
},
];
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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeuix/themes/aura';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
provideAnimationsAsync(),
providePrimeNG({
theme: {
preset: Aura
}
})
]
};
Al ejecutar en el terminal ng serve

Nota: 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)
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)
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>