Skip to content

Commit d969eb9

Browse files
committed
Add manufacturers CRUD, pagination, and debounce
Add manufacturers route and implement full CRUD + pagination/search features. Introduces ManufacturerRequest model and extends ManufacturerResponse. Refactors ManufacturerService (getAll accepts filters; create/update/delete methods added). Reworks manufacturers page and form: debounced search, pagination via server meta, action handlers, updated types, and UI/template improvements. Renames CategoryDefaultQuery.name to search and updates categories component to use debounced search and reset behavior. Extracts table columns/actions to utils/manufacturer-table-collums.ts. Various import and minor layout cleanups across templates and components to align with the new API and models.
1 parent 1669ec6 commit d969eb9

10 files changed

Lines changed: 353 additions & 215 deletions

File tree

src/app/app.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export const routes: Routes = [
4949
loadComponent: () =>
5050
import('./features/catalog/categories/categories').then((m) => m.CategoriesPageComponent),
5151
},
52+
{
53+
path: 'catalog/manufacturers',
54+
loadComponent: () =>
55+
import('./features/catalog/manufacturer/manufacturers').then(
56+
(m) => m.ManufacturersPageComponent,
57+
),
58+
},
5259
{
5360
path: 'compatibility/:sub',
5461
loadComponent: () => import('./features/placeholder').then((m) => m.PlaceholderPageComponent),

src/app/core/models/catetories/categories-default-query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export interface CategoryDefaultQuery {
22
page: number | null;
3-
name: string | null;
3+
search: string | null;
44
active: boolean | null;
55
per_page: number | null;
66
parent_id: string | null;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface ManufacturerRequest {
2+
name: string;
3+
slug: string;
4+
website: string;
5+
active: boolean;
6+
image: string | File | null;
7+
}

src/app/core/models/manufactureres/manufacturer-response.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface ManufacturerResponse {
1+
export interface ManufacturerResponse extends Record<string, unknown> {
22
id: string;
33
name: string;
44
slug: string;

src/app/core/services/manufacture-service.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { GeneralOptionQuery } from '../models/generals/general-option-query.mode
88
import { ManufacturerOption } from '../models/manufactureres/manufaturer-options.model';
99
import { getAllResponse } from '../models/generals/general-responses-list.model';
1010
import { GeneralOption } from '../models/generals/general-options-response.model';
11+
import { ManufacturerRequest } from '../models/manufactureres/manufacturer-request.model';
1112

1213
@Injectable({
1314
providedIn: 'root',
@@ -17,8 +18,11 @@ export class ManufacturerService {
1718
private api = environment.apiUrl;
1819
private flag = 'manufacturer';
1920

20-
getAll() {
21-
return this.http.get<PaginatedResponse<ManufacturerResponse>>(`${this.api}/${this.flag}`);
21+
getAll(filters: Partial<GeneralOptionQuery>) {
22+
const params = buildHttpParams(filters);
23+
return this.http.get<PaginatedResponse<ManufacturerResponse>>(`${this.api}/${this.flag}`, {
24+
params,
25+
});
2226
}
2327

2428
getOptions(filters: GeneralOptionQuery) {
@@ -37,4 +41,19 @@ export class ManufacturerService {
3741
`${this.api}/${this.flag}/product/${productId}`,
3842
);
3943
}
44+
45+
createManufacturer(data: ManufacturerRequest) {
46+
return this.http.post<getAllResponse<ManufacturerResponse>>(`${this.api}/${this.flag}`, data);
47+
}
48+
49+
updateManufacturer(id: string, data: ManufacturerRequest) {
50+
return this.http.put<getAllResponse<ManufacturerResponse>>(
51+
`${this.api}/${this.flag}/${id}`,
52+
data,
53+
);
54+
}
55+
56+
deleteManufacturer(id: string) {
57+
return this.http.delete<getAllResponse<ManufacturerResponse>>(`${this.api}/${this.flag}/${id}`);
58+
}
4059
}

src/app/features/catalog/categories/categories.ts

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import {
33
Component,
44
computed,
55
inject,
6+
OnDestroy,
67
OnInit,
78
signal,
89
} from '@angular/core';
910
import { Router } from '@angular/router';
11+
import { Subject, Subscription } from 'rxjs';
12+
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
13+
1014
import { PageHeaderComponent } from '../../../design-system/page-header/page-header';
1115
import { ToolbarComponent } from '../../../design-system/toolbar/toolbar';
1216
import { SearchInputComponent } from '../../../design-system/input/search-input';
@@ -43,18 +47,22 @@ import { initialValuesPagination } from '../../../design-system/pagination/utils
4347
templateUrl: './categories.html',
4448
styleUrl: './categories.css',
4549
})
46-
export class CategoriesPageComponent implements OnInit {
50+
export class CategoriesPageComponent implements OnInit, OnDestroy {
4751
private router = inject(Router);
4852
private toastService = inject(ToastService);
4953
private readonly categoryService = inject(CategoryService);
5054

51-
private readonly perPage = signal<number>(25);
55+
private readonly perPage = signal<number>(10);
5256
private readonly currentPage = signal<number>(1);
5357

5458
readonly loading = signal<boolean>(false);
5559
readonly searchQuery = signal<string>('');
5660
readonly selectedStatus = signal<string>('');
5761

62+
// Subject para gerenciar o debounce da digitação no campo de busca
63+
private searchSubject = new Subject<string>();
64+
private searchSubscription?: Subscription;
65+
5866
// Category Form Drawer State
5967
readonly isFormOpen = signal<boolean>(false);
6068
readonly formMode = signal<'create' | 'edit' | 'view'>('create');
@@ -82,15 +90,33 @@ export class CategoriesPageComponent implements OnInit {
8290

8391
readonly totalItens = signal<number>(0);
8492
query: CategoryDefaultQuery = {
85-
name: null,
93+
search: null,
8694
active: null,
8795
parent_id: null,
8896
page: this.currentPage(),
8997
per_page: this.perPage(),
9098
};
9199

92100
ngOnInit(): void {
101+
// 1. Busca inicial
93102
this.getPaginationAllCategories(this.query);
103+
104+
// 2. Inscrição para escutar a busca com debounce de 400ms
105+
this.searchSubscription = this.searchSubject
106+
.pipe(debounceTime(400), distinctUntilChanged())
107+
.subscribe((queryText) => {
108+
this.currentPage.set(1);
109+
this.query = {
110+
...this.query,
111+
search: queryText || null,
112+
page: 1,
113+
};
114+
this.getPaginationAllCategories(this.query);
115+
});
116+
}
117+
118+
ngOnDestroy(): void {
119+
this.searchSubscription?.unsubscribe();
94120
}
95121

96122
getPaginationAllCategories(filters: CategoryDefaultQuery): void {
@@ -111,30 +137,24 @@ export class CategoriesPageComponent implements OnInit {
111137
});
112138
}
113139

114-
readonly filteredCategories = computed(() => {
115-
const q = this.searchQuery().toLowerCase().trim();
116-
const st = this.selectedStatus();
117-
118-
return this.allCategories().filter((c) => {
119-
const matchesQ =
120-
!q ||
121-
c.name.toLowerCase().includes(q) ||
122-
c.slug.toLowerCase().includes(q) ||
123-
(c.description || '').toLowerCase().includes(q);
124-
const matchesSt = !st || (st === 'active' ? c.active : !c.active);
125-
return matchesQ && matchesSt;
126-
});
127-
});
128-
129140
onSearchChange(query: string): void {
130-
this.query = { ...this.query, name: query };
131141
this.searchQuery.set(query);
132-
this.getPaginationAllCategories(this.query);
142+
this.searchSubject.next(query);
133143
}
134144

135145
onStatusChange(status: string): void {
136-
this.query = { ...this.query, active: status === 'active' };
146+
let activeValue: boolean | null = null;
147+
if (status === 'active') activeValue = true;
148+
if (status === 'inactive') activeValue = false;
149+
137150
this.selectedStatus.set(status);
151+
this.currentPage.set(1);
152+
153+
this.query = {
154+
...this.query,
155+
active: activeValue,
156+
page: 1,
157+
};
138158
this.getPaginationAllCategories(this.query);
139159
}
140160

@@ -165,13 +185,16 @@ export class CategoriesPageComponent implements OnInit {
165185
resetFilters(): void {
166186
this.searchQuery.set('');
167187
this.selectedStatus.set('');
188+
this.currentPage.set(1);
189+
168190
this.query = {
169-
name: null,
191+
search: null,
170192
active: null,
171193
parent_id: null,
172-
page: this.currentPage(),
194+
page: 1,
173195
per_page: this.perPage(),
174196
};
197+
this.getPaginationAllCategories(this.query);
175198
this.toastService.info('Filtros limpos', 'Todos os parâmetros de busca foram resetados.');
176199
}
177200

src/app/features/catalog/manufacturer/manufacturer-form/manufacturer-form.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,35 @@ import {
66
signal,
77
computed,
88
effect,
9-
inject
9+
inject,
1010
} from '@angular/core';
1111
import { CommonModule } from '@angular/common';
1212
import { ButtonComponent } from '../../../../design-system/button/button';
1313
import { AppIconComponent } from '../../../../design-system/icon/app-icon';
1414
import { DrawerComponent } from '../../../../design-system/drawer/drawer';
15-
import { ManufacturerService } from '../../../../core/services/manufacturer';
1615
import { ToastService } from '../../../../core/services/toast';
17-
import {
18-
Manufacturer,
19-
CreateManufacturerPayload,
20-
UpdateManufacturerPayload
21-
} from '../../../../core/models/manufacturer';
16+
import { ManufacturerRequest } from '../../../../core/models/manufactureres/manufacturer-request.model';
17+
import { ManufacturerService } from '../../../../core/services/manufacture-service';
18+
import { ManufacturerResponse } from '../../../../core/models/manufactureres/manufacturer-response.model';
2219

2320
@Component({
2421
selector: 'app-manufacturer-form',
2522
standalone: true,
26-
imports: [
27-
CommonModule,
28-
ButtonComponent,
29-
AppIconComponent,
30-
DrawerComponent
31-
],
23+
imports: [CommonModule, ButtonComponent, AppIconComponent, DrawerComponent],
3224
changeDetection: ChangeDetectionStrategy.OnPush,
3325
templateUrl: './manufacturer-form.html',
34-
styleUrl: './manufacturer-form.css'
26+
styleUrl: './manufacturer-form.css',
3527
})
3628
export class ManufacturerFormComponent {
3729
private manufacturerService = inject(ManufacturerService);
3830
private toastService = inject(ToastService);
3931

4032
readonly isOpen = input<boolean>(false);
4133
readonly mode = input<'create' | 'edit' | 'view'>('create');
42-
readonly manufacturer = input<Manufacturer | null>(null);
34+
readonly manufacturer = input<ManufacturerResponse | null>(null);
4335

4436
readonly closeForm = output<void>();
45-
readonly manufacturerSaved = output<Manufacturer>();
37+
readonly manufacturerSaved = output<ManufacturerResponse>();
4638

4739
// Reactive State Signals
4840
readonly isSubmitting = signal<boolean>(false);
@@ -230,28 +222,31 @@ export class ManufacturerFormComponent {
230222
}
231223

232224
if (!this.validateForm()) {
233-
this.toastService.error('Formulário Inválido', 'Corrija os campos indicados antes de salvar.');
225+
this.toastService.error(
226+
'Formulário Inválido',
227+
'Corrija os campos indicados antes de salvar.',
228+
);
234229
return;
235230
}
236231

237232
this.isSubmitting.set(true);
238233

239-
const payload: CreateManufacturerPayload | UpdateManufacturerPayload = {
234+
const payload: ManufacturerRequest = {
240235
name: this.name().trim(),
241236
slug: this.slug().trim(),
242-
image: this.image().trim() || null,
243-
website: this.website().trim() || null,
244-
active: this.active()
237+
image: this.image().trim(),
238+
website: this.website().trim(),
239+
active: this.active(),
245240
};
246241

247242
if (this.mode() === 'edit' && this.manufacturer()?.id) {
248243
const id = this.manufacturer()!.id;
249-
this.manufacturerService.update(id, payload).subscribe({
244+
this.manufacturerService.updateManufacturer(id, payload).subscribe({
250245
next: (response) => {
251246
this.isSubmitting.set(false);
252247
this.toastService.success(
253248
'Fabricante Atualizado',
254-
`O fabricante "${response.data.name}" foi salvo com sucesso.`
249+
`O fabricante "${response.data.name}" foi salvo com sucesso.`,
255250
);
256251
this.manufacturerSaved.emit(response.data);
257252
this.close();
@@ -269,15 +264,15 @@ export class ManufacturerFormComponent {
269264
}
270265
this.formErrors.set(serverErrors);
271266
}
272-
}
267+
},
273268
});
274269
} else {
275-
this.manufacturerService.create(payload as CreateManufacturerPayload).subscribe({
270+
this.manufacturerService.createManufacturer(payload).subscribe({
276271
next: (response) => {
277272
this.isSubmitting.set(false);
278273
this.toastService.success(
279274
'Fabricante Criado',
280-
`O fabricante "${response.data.name}" foi cadastrado com sucesso.`
275+
`O fabricante "${response.data.name}" foi cadastrado com sucesso.`,
281276
);
282277
this.manufacturerSaved.emit(response.data);
283278
this.close();
@@ -295,7 +290,7 @@ export class ManufacturerFormComponent {
295290
}
296291
this.formErrors.set(serverErrors);
297292
}
298-
}
293+
},
299294
});
300295
}
301296
}

0 commit comments

Comments
 (0)