Skip to content

Commit 838c93b

Browse files
committed
Add product API and data-table action/badge support
Introduce product API calls and wire up data-table actions/badges. Key changes: ProductService: use new environments import, add flag, getAll and getById. ProductResponse now extends Record<string, unknown>. Data-table: render badges and icon cells, loop actions, centralize clicks with onActionClick and emit actionClick; import BadgeComponent. ProductForm: inject ProductService, add product signal and productById() call. Products page: handle loading/meta from API and added handleProductAction. Move environment files to src/environments and add environment.prod.ts.
1 parent d86078e commit 838c93b

11 files changed

Lines changed: 159 additions & 38 deletions

File tree

src/app/core/models/products/product-response.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface ProductResponse {
1+
export interface ProductResponse extends Record<string, unknown> {
22
id: string;
33
status: Status;
44
category: Category;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Injectable } from '@angular/core';
2-
import { environment } from '../../../environment/environment';
2+
import { environment } from '../../../environments/environment';
33
import { MenuItem } from '../models/menu-item';
44

55
export interface MenuGroup {

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ import { HttpClient } from '@angular/common/http';
22
import { inject, Injectable } from '@angular/core';
33
import { PaginatedResponse } from '../models/pagination/pagination.model';
44
import { ProductResponse } from '../models/products/product-response.model';
5-
import { environment } from '../../../environment/environment';
5+
import { environment } from '../../../environments/environment';
66

77
@Injectable({
88
providedIn: 'root',
99
})
1010
export class ProductService {
1111
private http = inject(HttpClient);
1212
private api = environment.apiUrl;
13+
private flag = 'product';
1314

1415
getAll() {
15-
return this.http.get<PaginatedResponse<ProductResponse>>(`${this.api}/product`);
16+
return this.http.get<PaginatedResponse<ProductResponse>>(`${this.api}/${this.flag}`);
17+
}
18+
19+
getById(id: string) {
20+
return this.http.get<ProductResponse>(`${this.api}/${this.flag}/${id}`);
1621
}
1722
}

src/app/design-system/data-table/data-table.html

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,34 @@
5858
[class.text-right]="col.align === 'right'"
5959
class="px-4 py-3.5 font-normal"
6060
>
61-
@if (col.cell) { {{ col.cell(row) }} } @else { {{ row[col.key] }} }
61+
@if (col.type === 'badge' || col.badgeConfig) { @let bg = getBadgeConfig(row, col);
62+
<app-badge [variant]="mapVariant(bg.variant)">
63+
@if (bg.icon) {
64+
<app-icon [name]="bg.icon" size="w-3 h-3" class="mr-1 inline" />
65+
} {{ bg.text }}
66+
</app-badge>
67+
} @else if (col.type === 'icon' || col.iconGetter) { @let iconName = col.iconGetter ?
68+
col.iconGetter(row) : 'box';
69+
<div
70+
class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-slate-300"
71+
>
72+
<app-icon [name]="iconName" size="w-4 h-4" />
73+
</div>
74+
} @else { {{ getFormattedValue(row, col) }} }
6275
</td>
6376
} @if (hasActions()) {
6477
<td class="px-4 py-3.5 text-right" (click)="$event.stopPropagation()">
6578
<div class="inline-flex items-center justify-end gap-1">
79+
@for (act of getVisibleActions(row); track act.id) {
6680
<button
6781
type="button"
68-
(click)="onEdit(row)"
69-
class="p-1 rounded text-gray-400 hover:text-[#4F8A6B] hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
70-
title="Editar"
71-
>
72-
<app-icon name="edit" size="w-4 h-4" />
73-
</button>
74-
<button
75-
type="button"
76-
(click)="onDelete(row)"
77-
class="p-1 rounded text-gray-400 hover:text-[#D66A6A] hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
78-
title="Excluir"
82+
(click)="onActionClick(act, row, $event)"
83+
[class]="'p-1.5 rounded transition-colors ' + (act.colorClass || 'text-gray-400 hover:text-[#4F8A6B] hover:bg-gray-100 dark:hover:bg-slate-700')"
84+
[title]="act.title || act.label"
7985
>
80-
<app-icon name="trash-2" size="w-4 h-4" />
86+
<app-icon [name]="act.icon" size="w-4 h-4" />
8187
</button>
88+
}
8289
</div>
8390
</td>
8491
}

src/app/design-system/data-table/data-table.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
TableBadgeConfig,
88
TableColumn,
99
} from '../../core/models/list-table/list-table.model';
10-
import { BadgeVariant } from '../badge/badge';
10+
import { BadgeVariant, BadgeComponent } from '../badge/badge';
1111

1212
export interface ColumnDef<T> {
1313
key: string;
@@ -21,7 +21,7 @@ export interface ColumnDef<T> {
2121
@Component({
2222
selector: 'app-data-table',
2323
standalone: true,
24-
imports: [AppIconComponent, SkeletonComponent, EmptyStateComponent],
24+
imports: [AppIconComponent, SkeletonComponent, EmptyStateComponent, BadgeComponent],
2525
changeDetection: ChangeDetectionStrategy.OnPush,
2626
templateUrl: './data-table.html',
2727
styleUrl: './data-table.css',
@@ -169,11 +169,12 @@ export class DataTableComponent<T extends Record<string, unknown>> {
169169
if (action.id) {
170170
action.handler(row);
171171
}
172+
console.log('[ON ACTION CLICK]', action, action.id);
172173
this.actionClick.emit({ actionId: action.id, row });
173-
if (action.id === 'edit') {
174-
this.editRow.emit(row);
175-
} else if (action.id === 'delete') {
176-
this.deleteRow.emit(row);
177-
}
174+
// if (action.id === 'edit') {
175+
// this.editRow.emit(row);
176+
// } else if (action.id === 'delete') {
177+
// this.deleteRow.emit(row);
178+
// }
178179
}
179180
}

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

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { ConfirmDialogComponent } from '../../../design-system/dialog/confirm-di
1515
import { AppIconComponent } from '../../../design-system/icon/app-icon';
1616
import { ToastService } from '../../../core/services/toast';
1717
import { ShellStateService } from '../../../core/services/shell-state';
18+
import { ProductService } from '../../../core/services/product-service';
19+
import { ProductResponse } from '../../../core/models/products/product-response.model';
1820

1921
export type ProductFormMode = 'create' | 'edit' | 'view' | 'duplicate';
2022
export type FormTab = 'geral' | 'comercial' | 'compatibilidade' | 'midia' | 'administracao';
@@ -95,6 +97,7 @@ export class ProductFormComponent implements OnInit {
9597
readonly shellState = inject(ShellStateService);
9698
private route = inject(ActivatedRoute);
9799
private router = inject(Router);
100+
private productService = inject(ProductService);
98101
private toastService = inject(ToastService);
99102

100103
readonly mode = signal<ProductFormMode>('create');
@@ -146,6 +149,66 @@ export class ProductFormComponent implements OnInit {
146149
readonly selectedTagIds = signal<string[]>([]);
147150
readonly productNotes = signal<ProductNoteItem[]>([]);
148151

152+
readonly product = signal<ProductResponse>({
153+
id: 'p0000000-0002-0000-0000-000000000002',
154+
status: {
155+
id: 's0000000-0001-0000-0000-000000000001',
156+
module: 'PRODUCT',
157+
code: 'DRAFT',
158+
name: 'Rascunho',
159+
color: '#64748B',
160+
icon: 'file-text',
161+
},
162+
category: {
163+
id: 'c0000000-0013-0000-0000-000000000013',
164+
name: 'Filtros de Óleo',
165+
slug: 'filtros-de-oleo',
166+
},
167+
manufacturer: {
168+
id: 'm0000000-0001-0000-0000-000000000001',
169+
name: 'Bosch',
170+
slug: 'bosch',
171+
},
172+
internal_code: 'OF-002',
173+
name: 'Filtro de Óleo Bosch OF-002',
174+
slug: 'filtro-de-oleo-bosch-of-002',
175+
icon: null,
176+
image: null,
177+
short_description: 'Filtro de óleo paralelo Bosch',
178+
description: 'Filtro de óleo paralelo de alta qualidade para linha VW/Fiat 1.0 e 1.6.',
179+
weight: '0.300',
180+
height: '10.00',
181+
width: '8.00',
182+
length: '8.00',
183+
featured: false,
184+
active: 0,
185+
is_sellable: false,
186+
commercial_status: 'ready',
187+
pricing: {
188+
regular_price: 35.9,
189+
promotional_price: 29.9,
190+
current_price: 29.9,
191+
is_on_promotion: true,
192+
promotion_start: '2026-07-01',
193+
promotion_end: '2026-12-31',
194+
active: true,
195+
display: {
196+
from: 35.9,
197+
to: 29.9,
198+
label: 'de R$ 35,90 por R$ 29,90',
199+
},
200+
},
201+
inventory: {
202+
quantity: 50,
203+
reserved_quantity: 0,
204+
available_quantity: 50,
205+
minimum_quantity: 5,
206+
maximum_quantity: 300,
207+
allow_backorder: true,
208+
active: true,
209+
},
210+
});
211+
149212
// Static Dropdown Options
150213
readonly categoryOptions: SelectOption[] = [
151214
{ label: 'Sistemas de Freios', value: 'cat-01' },
@@ -229,20 +292,39 @@ export class ProductFormComponent implements OnInit {
229292
const url = this.router.url;
230293
const id = this.route.snapshot.paramMap.get('id');
231294

295+
if (id) {
296+
this.productById(id);
297+
}
298+
299+
console.log('[URL CHEGANDO AQUI]', url);
300+
232301
if (url.includes('/new')) {
233302
this.mode.set('create');
234-
} else if (url.includes('/edit')) {
235-
this.mode.set('edit');
236-
this.loadMockProduct(id);
237-
} else if (url.includes('/view') || (id && !url.includes('/duplicate'))) {
238-
this.mode.set('view');
239-
this.loadMockProduct(id);
240-
} else if (url.includes('/duplicate')) {
241-
this.mode.set('duplicate');
242-
this.loadMockProduct(id);
243-
// Backend mandate rule for duplicate: clean internal_code automatically
244-
this.formInternalCode.set('');
245303
}
304+
// else if (url.includes('/edit')) {
305+
// this.mode.set('edit');
306+
// this.loadMockProduct(id);
307+
// } else if (url.includes('/view') || (id && !url.includes('/duplicate'))) {
308+
// this.mode.set('view');
309+
// this.loadMockProduct(id);
310+
// } else if (url.includes('/duplicate')) {
311+
// this.mode.set('duplicate');
312+
// this.loadMockProduct(id);
313+
// // Backend mandate rule for duplicate: clean internal_code automatically
314+
// this.formInternalCode.set('');
315+
// }
316+
}
317+
318+
productById(productId: string) {
319+
this.productService.getById(productId).subscribe({
320+
next: (response) => {
321+
console.log(response);
322+
// this.product = response;
323+
},
324+
error: (error) => {
325+
console.log(error);
326+
},
327+
});
246328
}
247329

248330
loadMockProduct(id: string | null): void {

src/app/features/catalog/products/products.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ <h3 class="text-xl font-bold text-gray-900 dark:text-slate-100 mt-1">184</h3>
126126
[actions]="actions"
127127
[loading]="loading()"
128128
[roundedBottom]="false"
129+
(actionClick)="handleProductAction($event)"
129130
/>
130131

131132
<app-pagination

src/app/features/catalog/products/products.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,23 @@ export class ProductsPageComponent implements OnInit {
9999
this.getPaginationAllProducts();
100100
}
101101

102-
getPaginationAllProducts() {
102+
getPaginationAllProducts(): void {
103+
this.loading.set(true);
103104
this.productService.getAll().subscribe({
104105
next: (response) => {
105106
this.allProducts.set(response.data);
106-
console.log('[PRODUCT ALL LIST]', this.allProducts);
107+
if (response.meta) {
108+
this.totalItems.set(response.meta.total);
109+
this.currentPage.set(response.meta.current_page);
110+
this.pageSize.set(response.meta.per_page);
111+
} else {
112+
this.totalItems.set(response.data.length);
113+
}
114+
this.loading.set(false);
107115
},
108116
error: (error) => {
109-
this.toastService.error('Erro ao buscar produtos', error.message);
117+
this.loading.set(false);
118+
this.toastService.error('Erro ao buscar produtos', error.message || 'Falha na requisição.');
110119
},
111120
});
112121
}
@@ -171,4 +180,16 @@ export class ProductsPageComponent implements OnInit {
171180
}
172181
this.deleteDialogOpen.set(false);
173182
}
183+
184+
handleProductAction(event: { actionId: string; row: ProductResponse }): void {
185+
console.log('[HANDLE PRODUCT ACTION]', event);
186+
switch (event.actionId) {
187+
case 'edit':
188+
this.editProduct(event.row);
189+
break;
190+
case 'delete':
191+
this.confirmDelete(event.row);
192+
break;
193+
}
194+
}
174195
}
File renamed without changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const environment = {
2+
production: false,
3+
apiUrl: 'http://localhost:8000/api',
4+
};

0 commit comments

Comments
 (0)