the first commit
This commit is contained in:
136
src/app/pages/items-list/items-list.component.html
Normal file
136
src/app/pages/items-list/items-list.component.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<div class="items-list-container">
|
||||
<mat-toolbar color="primary" class="list-toolbar">
|
||||
<button mat-icon-button (click)="goBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<span class="toolbar-title">Items List</span>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<button mat-mini-fab color="accent" (click)="addItem()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
|
||||
<div class="filters-bar">
|
||||
<mat-form-field appearance="outline" class="search-field">
|
||||
<mat-label>Search items</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="searchQuery"
|
||||
(keyup.enter)="onSearch()"
|
||||
placeholder="Search by name...">
|
||||
<button mat-icon-button matSuffix (click)="onSearch()">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="filter-field">
|
||||
<mat-label>Visibility</mat-label>
|
||||
<mat-select [(ngModel)]="visibilityFilter" (selectionChange)="onFilterChange()">
|
||||
<mat-option [value]="undefined">All</mat-option>
|
||||
<mat-option [value]="true">Visible</mat-option>
|
||||
<mat-option [value]="false">Hidden</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
@if (selectedItems().size > 0) {
|
||||
<div class="bulk-actions">
|
||||
<span class="selection-count">{{ selectedItems().size }} selected</span>
|
||||
<button mat-raised-button (click)="bulkToggleVisibility(true)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
Show
|
||||
</button>
|
||||
<button mat-raised-button (click)="bulkToggleVisibility(false)">
|
||||
<mat-icon>visibility_off</mat-icon>
|
||||
Hide
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="items-header">
|
||||
<mat-checkbox
|
||||
[checked]="selectedItems().size === items().length && items().length > 0"
|
||||
[indeterminate]="selectedItems().size > 0 && selectedItems().size < items().length"
|
||||
(change)="toggleSelectAll()">
|
||||
</mat-checkbox>
|
||||
<span class="items-count">{{ items().length }} items</span>
|
||||
</div>
|
||||
|
||||
<div class="items-grid">
|
||||
@for (item of items(); track item.id) {
|
||||
<div class="item-card" (click)="openItem(item.id)">
|
||||
<div class="item-card-actions">
|
||||
<mat-checkbox
|
||||
[checked]="selectedItems().has(item.id)"
|
||||
(change)="toggleSelection(item.id)"
|
||||
(click)="$event.stopPropagation()">
|
||||
</mat-checkbox>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
(click)="deleteItem(item, $event)"
|
||||
class="delete-btn">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="item-image">
|
||||
@if (item.imgs.length) {
|
||||
<img [src]="item.imgs[0]" [alt]="item.name">
|
||||
} @else {
|
||||
<div class="no-image">
|
||||
<mat-icon>image</mat-icon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="item-info">
|
||||
<h3>{{ item.name }}</h3>
|
||||
|
||||
<div class="item-details">
|
||||
<span class="price">{{ item.price }} {{ item.currency }}</span>
|
||||
<span class="quantity">Qty: {{ item.quantity }}</span>
|
||||
</div>
|
||||
|
||||
<div class="item-meta">
|
||||
<span class="priority">Priority: {{ item.priority }}</span>
|
||||
<mat-icon [class.visible]="item.visible" [class.hidden]="!item.visible">
|
||||
{{ item.visible ? 'visibility' : 'visibility_off' }}
|
||||
</mat-icon>
|
||||
</div>
|
||||
|
||||
@if (item.tags.length) {
|
||||
<div class="item-tags">
|
||||
@for (tag of item.tags.slice(0, 3); track tag) {
|
||||
<mat-chip>{{ tag }}</mat-chip>
|
||||
}
|
||||
@if (item.tags.length > 3) {
|
||||
<span class="more-tags">+{{ item.tags.length - 3 }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (loading()) {
|
||||
<div class="loading-more">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<span>Loading more items...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!hasMore() && items().length > 0) {
|
||||
<div class="end-message">
|
||||
No more items to load
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading() && items().length === 0) {
|
||||
<div class="empty-state">
|
||||
<mat-icon>inventory_2</mat-icon>
|
||||
<p>No items found</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
269
src/app/pages/items-list/items-list.component.scss
Normal file
269
src/app/pages/items-list/items-list.component.scss
Normal file
@@ -0,0 +1,269 @@
|
||||
.items-list-container {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.list-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.toolbar-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.filters-bar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 2rem;
|
||||
background: #fafafa;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.search-field {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
|
||||
.selection-count {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.items-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 2rem;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
.items-count {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
position: relative;
|
||||
background: white;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-4px);
|
||||
border-color: #1976d2;
|
||||
|
||||
.delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item-card-actions {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
mat-checkbox {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.item-checkbox {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #ccc;
|
||||
|
||||
mat-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
.price {
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #999;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
&.visible {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
color: #f44336;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
|
||||
mat-chip {
|
||||
font-size: 0.75rem;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.more-tags {
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
|
||||
span {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.end-message {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #999;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #999;
|
||||
|
||||
mat-icon {
|
||||
font-size: 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
252
src/app/pages/items-list/items-list.component.ts
Normal file
252
src/app/pages/items-list/items-list.component.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { Component, OnInit, signal, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { ApiService } from '../../services';
|
||||
import { Item } from '../../models';
|
||||
import { CreateDialogComponent } from '../../components/create-dialog/create-dialog.component';
|
||||
import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-items-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatChipsModule,
|
||||
MatCheckboxModule,
|
||||
MatSelectModule,
|
||||
MatToolbarModule,
|
||||
MatSnackBarModule,
|
||||
MatDialogModule
|
||||
],
|
||||
templateUrl: './items-list.component.html',
|
||||
styleUrls: ['./items-list.component.scss']
|
||||
})
|
||||
export class ItemsListComponent implements OnInit {
|
||||
items = signal<Item[]>([]);
|
||||
loading = signal(false);
|
||||
hasMore = signal(true);
|
||||
page = signal(1);
|
||||
searchQuery = signal('');
|
||||
visibilityFilter = signal<boolean | undefined>(undefined);
|
||||
selectedItems = signal<Set<string>>(new Set());
|
||||
|
||||
subcategoryId = signal<string>('');
|
||||
projectId = signal<string>('');
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
private snackBar: MatSnackBar,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// Get projectId from parent route immediately
|
||||
const parentParams = this.route.parent?.snapshot.params;
|
||||
if (parentParams) {
|
||||
this.projectId.set(parentParams['projectId']);
|
||||
}
|
||||
|
||||
this.route.params.subscribe(params => {
|
||||
this.subcategoryId.set(params['subcategoryId']);
|
||||
this.loadItems();
|
||||
});
|
||||
}
|
||||
|
||||
loadItems(append = false) {
|
||||
if (this.loading() || (!append && this.items().length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading.set(true);
|
||||
const currentPage = append ? this.page() + 1 : 1;
|
||||
|
||||
this.apiService.getItems(
|
||||
this.subcategoryId(),
|
||||
currentPage,
|
||||
20,
|
||||
this.searchQuery() || undefined,
|
||||
{
|
||||
visible: this.visibilityFilter(),
|
||||
tags: []
|
||||
}
|
||||
).subscribe({
|
||||
next: (response) => {
|
||||
if (append) {
|
||||
this.items.set([...this.items(), ...response.items]);
|
||||
} else {
|
||||
this.items.set(response.items);
|
||||
}
|
||||
this.page.set(currentPage);
|
||||
this.hasMore.set(response.hasMore);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to load items', err);
|
||||
this.snackBar.open('Failed to load items', 'Close', { duration: 3000 });
|
||||
this.loading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:scroll', [])
|
||||
onScroll() {
|
||||
const scrollPosition = window.pageYOffset + window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
if (scrollPosition >= documentHeight - 200 && this.hasMore() && !this.loading()) {
|
||||
this.loadItems(true);
|
||||
}
|
||||
}
|
||||
|
||||
onSearch() {
|
||||
this.page.set(1);
|
||||
this.items.set([]);
|
||||
this.loadItems();
|
||||
}
|
||||
|
||||
onFilterChange() {
|
||||
this.page.set(1);
|
||||
this.items.set([]);
|
||||
this.loadItems();
|
||||
}
|
||||
|
||||
toggleSelection(itemId: string) {
|
||||
const selected = new Set(this.selectedItems());
|
||||
if (selected.has(itemId)) {
|
||||
selected.delete(itemId);
|
||||
} else {
|
||||
selected.add(itemId);
|
||||
}
|
||||
this.selectedItems.set(selected);
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
if (this.selectedItems().size === this.items().length) {
|
||||
this.selectedItems.set(new Set());
|
||||
} else {
|
||||
this.selectedItems.set(new Set(this.items().map(item => item.id)));
|
||||
}
|
||||
}
|
||||
|
||||
bulkToggleVisibility(visible: boolean) {
|
||||
const itemIds = Array.from(this.selectedItems());
|
||||
if (!itemIds.length) {
|
||||
this.snackBar.open('No items selected', 'Close', { duration: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.apiService.bulkUpdateItems(itemIds, { visible }).subscribe({
|
||||
next: () => {
|
||||
this.items.update(items =>
|
||||
items.map(item =>
|
||||
itemIds.includes(item.id) ? { ...item, visible } : item
|
||||
)
|
||||
);
|
||||
this.snackBar.open(`Updated ${itemIds.length} items`, 'Close', { duration: 2000 });
|
||||
this.selectedItems.set(new Set());
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Failed to update items', 'Close', { duration: 3000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openItem(itemId: string) {
|
||||
console.log('Opening item:', itemId, 'projectId:', this.projectId());
|
||||
this.router.navigate(['/project', this.projectId(), 'item', itemId]);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
const subcategoryId = this.subcategoryId();
|
||||
if (subcategoryId) {
|
||||
// Navigate back to subcategory editor
|
||||
this.router.navigate(['/subcategory', subcategoryId]);
|
||||
} else {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
|
||||
addItem() {
|
||||
const dialogRef = this.dialog.open(CreateDialogComponent, {
|
||||
width: '500px',
|
||||
data: {
|
||||
title: 'Create New Item',
|
||||
fields: [
|
||||
{ name: 'name', label: 'Item Name', type: 'text', required: true },
|
||||
{ name: 'simpleDescription', label: 'Simple Description', type: 'text', required: false },
|
||||
{ name: 'price', label: 'Price', type: 'number', required: true },
|
||||
{ name: 'currency', label: 'Currency', type: 'text', required: true, value: 'USD' },
|
||||
{ name: 'quantity', label: 'Quantity', type: 'number', required: true, value: 0 },
|
||||
{ name: 'visible', label: 'Visible', type: 'toggle', required: false, value: true }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
const subcategoryId = this.subcategoryId();
|
||||
if (!subcategoryId) return;
|
||||
|
||||
this.apiService.createItem(subcategoryId, result).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Item created successfully', 'Close', { duration: 3000 });
|
||||
this.loadItems();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error creating item:', err);
|
||||
this.snackBar.open('Failed to create item', 'Close', { duration: 3000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteItem(item: Item, event: Event) {
|
||||
event.stopPropagation();
|
||||
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title: 'Delete Item',
|
||||
message: `Are you sure you want to delete "${item.name}"? This action cannot be undone.`,
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
dangerous: true
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.apiService.deleteItem(item.id).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Item deleted successfully', 'Close', { duration: 3000 });
|
||||
this.loadItems();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error deleting item:', err);
|
||||
this.snackBar.open('Failed to delete item', 'Close', { duration: 3000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user