feat: enhance product and purchase components

- Added new components for handling purchase receipts and payment wrappers.
- Updated product details view to include sales count and stock alerts.
- Refactored invoice payment form to use a template for better structure.
- Introduced confirmation dialog service for payment confirmations.
- Improved state card component for better visual representation of orders.
- Added loading indicators and error handling in various components.
- Updated routes to include new purchase functionality for suppliers.
- Enhanced stock alert component to visually indicate low stock levels.
This commit is contained in:
2025-12-30 21:03:39 +03:30
parent 85a9c8714d
commit 83c3d57866
48 changed files with 797 additions and 259 deletions
@@ -8,6 +8,7 @@ export const INVENTORIES_API_ROUTES = {
inventoryMovements: (inventoryId: string) => `${baseUrl}/${inventoryId}/movements`,
transfer: () => `${baseUrl}/transfers`,
stock: (inventoryId: string) => `${baseUrl}/${inventoryId}/stock`,
cardex: (inventoryId: string) => `${baseUrl}/${inventoryId}/cardex`,
productCardex: (inventoryId: string, productId: string) =>
`${baseUrl}/${inventoryId}/products/${productId}/cardex`,
bankAccounts: (inventoryId: number) => `${baseUrl}/${inventoryId}/bank-accounts`,
@@ -37,12 +37,21 @@ export const inventoriesNamedRoutes: NamedRoutes<any> = {
pagePath: () => '/inventories/:inventoryId/products',
},
},
cardex: {
path: 'inventories/:inventoryId/cardex',
loadComponent: () =>
import('../../views/cardex.component').then((m) => m.InventoryCardexComponent),
meta: {
title: 'کاردکس انبار',
pagePath: () => '/inventories/:inventoryId/cardex',
},
},
productCardex: {
path: 'inventories/:inventoryId/products/:productId/cardex',
loadComponent: () =>
import('../../views/product-cardex.component').then((m) => m.InventoryProductCardexComponent),
meta: {
title: 'کارتکس محصول',
title: 'کاردکس انبار محصول',
pagePath: () => '/inventories/:inventoryId/products/:productId/cardex',
},
},
+2 -1
View File
@@ -1,3 +1,4 @@
import { ICardexResponse } from '@/modules/cardex/models';
import {
IInventoryInfo,
IInventoryMovement,
@@ -61,6 +62,6 @@ export interface IInventoryStockQuery {
isAvailable?: boolean;
}
export interface IInventoryProductCardexRawResponse extends IInventoryStockMovementRawResponse {}
export interface IInventoryProductCardexRawResponse extends ICardexResponse {}
export interface IInventoryProductCardexResponse extends IInventoryProductCardexRawResponse {}
@@ -72,4 +72,9 @@ export class InventoriesService {
`${this.apiRoutes.productCardex(inventoryId, productId)}`,
);
}
getCardex(inventoryId: string): Observable<IPaginatedResponse<IInventoryProductCardexResponse>> {
return this.http.get<IPaginatedResponse<IInventoryProductCardexRawResponse>>(
`${this.apiRoutes.cardex(inventoryId)}`,
);
}
}
@@ -0,0 +1,15 @@
@if (pageLoading) {
<div class="h-svh w-svw flex items-center justify-center">
<p-progress-spinner />
</div>
} @else {
<p-card>
<ng-template #header>
<div class="p-4 flex items-center justify-between">
<span class="text-xl font-bold">کاردکس انبار {{ inventory()?.name }}</span>
</div>
</ng-template>
<cardex-templates-inventory [loading]="getCardexLoading()" [items]="cardex() || []" variant="inventory" />
</p-card>
}
@@ -0,0 +1,53 @@
import { Maybe } from '@/core';
import { CardexComponent } from '@/modules/cardex/components/templates/inventory.component';
import { IProductResponse } from '@/modules/products/models';
import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Card } from 'primeng/card';
import { ProgressSpinner } from 'primeng/progressspinner';
import { IInventoryProductCardexResponse } from '../models';
import { InventoriesService } from '../services/main.service';
import { InventoryStore } from '../store/inventory.store';
@Component({
selector: 'inventory-cardex',
templateUrl: './cardex.component.html',
imports: [ProgressSpinner, Card, CardexComponent],
})
export class InventoryCardexComponent {
private route = inject(ActivatedRoute);
private readonly store = inject(InventoryStore);
inventoryId = this.route.snapshot.paramMap.get('inventoryId')!;
inventory = computed(() => this.store.entities()[this.inventoryId]);
loading = this.store.loading;
getInventoryLoading = signal(false);
getProductLoading = signal(false);
product = signal<Maybe<IProductResponse>>(null);
getCardexLoading = signal(false);
cardex = signal<Maybe<IInventoryProductCardexResponse[]>>(null);
get pageLoading() {
return this.getInventoryLoading() || this.getProductLoading();
}
constructor(private service: InventoriesService) {
this.store.getSingle(this.inventoryId);
this.getCardex();
}
getCardex() {
this.service.getCardex(this.inventoryId).subscribe({
next: (res) => {
this.getCardexLoading.set(false);
this.cardex.set(res.data);
},
error: () => {
this.getCardexLoading.set(false);
},
});
}
}
@@ -12,12 +12,29 @@
[columns]="columns"
emptyPlaceholderTitle="نقطه فروش برای این انبار تعریف نشده است"
emptyPlaceholderDescription="برای تعریف نقطه‌ی فروش، روی دکمهٔ بالا کلیک کنید."
[showDetails]="true"
[items]="items()"
[loading]="loading()"
(onAdd)="openAddForm()"
(onDetails)="toPos($event)"
/>
>
<ng-template #salesInfoTpl let-item>
<div class="flex flex-col gap-1">
@if (item.todaySalesInfo.salesCount) {
<span [appPriceMask]="item.todaySalesInfo.salesTotal"></span>
<span class="block text-muted-color text-sm">{{ item.todaySalesInfo.salesCount }} فاکتور</span>
} @else {
<span class="text-muted-color">فروشی ثبت نشده است</span>
}
</div>
</ng-template>
<ng-template #actionsTpl let-item>
<div class="flex gap-2">
<p-button size="small" icon="pi pi-info" (click)="toPosInfo(item)" title="جزئیات" />
<p-button size="small" icon="pi pi-chevron-left" (click)="toPos(item)" title="ورود به صفحه‌ی فروش" />
</div>
</ng-template>
</app-page-data-list>
@if (inventoryId) {
<app-inventory-pos-account-form [(visible)]="visibleForm" [inventoryId]="inventoryId" />
}
@@ -3,9 +3,10 @@ import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, computed, inject, signal } from '@angular/core';
import { PriceMaskDirective } from '@/shared/directives';
import { Component, computed, inject, signal, TemplateRef, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
import { Button, ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { InventoryPosAccountFormComponent } from '../../components/posAccounts/form.component';
import { IInventoryPosAccountResponse } from '../../models';
@@ -21,6 +22,8 @@ import { InventoryStore } from '../../store/inventory.store';
ButtonDirective,
InventoryPosAccountFormComponent,
InnerPagesHeaderComponent,
PriceMaskDirective,
Button,
],
})
export class InventoryPosAccountListComponent {
@@ -34,9 +37,13 @@ export class InventoryPosAccountListComponent {
visibleForm = signal<boolean>(false);
inventory = computed(() => this.inventoryStore.entities()[this.inventoryId]);
@ViewChild('salesInfoTpl', { static: true }) salesInfoTpl!: TemplateRef<any>;
@ViewChild('actionsTpl', { static: true }) actionsTpl!: TemplateRef<any>;
columns = [] as IColumn<IInventoryPosAccountResponse>[];
pageTitle = computed(() => `نقاط فروش متصل به انبار ${this.inventory()?.name || ''}`);
ngOnInit() {
this.inventoryStore.getSingle(this.inventoryId);
this.getItems();
@@ -55,13 +62,18 @@ export class InventoryPosAccountListComponent {
return `${item.bankAccount.name} - بانک ${item.bankAccount.branch.bank.name} - شعبه‌ی ${item.bankAccount.branch.name}`;
},
},
{
field: 'salesInfo',
header: 'جزییات فروش امروز',
customDataModel: this.salesInfoTpl,
},
// {
// field: 'actions',
// header: '',
// customDataModel: this.actionsTpl,
// width: '100px',
// },
{
field: 'actions',
header: '',
customDataModel: this.actionsTpl,
width: '100px',
},
];
}
@@ -89,4 +101,6 @@ export class InventoryPosAccountListComponent {
toPos(item: IInventoryPosAccountResponse) {
window.open(`/pos/${item.id}`, '_blank');
}
toPosInfo(item: IInventoryPosAccountResponse) {}
}
@@ -11,6 +11,6 @@
</div>
</ng-template>
<shared-cardex [loading]="getCardexLoading()" [items]="cardex() || []" />
<cardex-templates-inventory [loading]="getCardexLoading()" [items]="cardex() || []" variant="general" />
</p-card>
}
@@ -1,13 +1,14 @@
import { Maybe } from '@/core';
import { CardexComponent } from '@/modules/cardex/components/templates/inventory.component';
import { IProductResponse } from '@/modules/products/models';
import { ProductsService } from '@/modules/products/services/main.service';
import { CardexComponent } from '@/shared/components/cardex/cardex.component';
import { Component, inject, signal } from '@angular/core';
import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Card } from 'primeng/card';
import { ProgressSpinner } from 'primeng/progressspinner';
import { IInventoryDetailResponse, IInventoryProductCardexResponse } from '../models';
import { IInventoryProductCardexResponse } from '../models';
import { InventoriesService } from '../services/main.service';
import { InventoryStore } from '../store/inventory.store';
@Component({
selector: 'inventory-product-cardex',
@@ -16,20 +17,15 @@ import { InventoriesService } from '../services/main.service';
})
export class InventoryProductCardexComponent {
private route = inject(ActivatedRoute);
constructor(
private service: InventoriesService,
private productService: ProductsService,
) {
this.getInventory();
this.getProduct();
this.getCardex();
}
private readonly store = inject(InventoryStore);
inventoryId = this.route.snapshot.params['inventoryId'];
inventoryId = this.route.snapshot.paramMap.get('inventoryId')!;
productId = this.route.snapshot.params['productId'];
inventory = computed(() => this.store.entities()[this.inventoryId]);
loading = this.store.loading;
getInventoryLoading = signal(false);
inventory = signal<Maybe<IInventoryDetailResponse>>(null);
getProductLoading = signal(false);
product = signal<Maybe<IProductResponse>>(null);
@@ -40,17 +36,13 @@ export class InventoryProductCardexComponent {
return this.getInventoryLoading() || this.getProductLoading();
}
getInventory() {
this.getInventoryLoading.set(true);
this.service.getSingle(this.inventoryId).subscribe({
next: (res) => {
this.inventory.set(res);
this.getInventoryLoading.set(false);
},
error: () => {
this.getInventoryLoading.set(false);
},
});
constructor(
private service: InventoriesService,
private productService: ProductsService,
) {
this.store.getSingle(this.inventoryId);
this.getProduct();
this.getCardex();
}
getProduct() {
@@ -46,6 +46,11 @@
</p-card>
<p-card header="اطلاعات تکمیلی">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input
label="حداقل سطح هشدار موجودی"
[control]="form.controls.minimumStockAlertLevel"
name="minimumStockAlertLevel"
/>
<product-categories-select-field [control]="form.controls.categoryId" [canInsert]="true" />
</form>
</p-card>
@@ -55,6 +55,10 @@ export class ProductFullFormComponent {
barcode: [this.initialData?.barcode || null],
description: [this.initialData?.description || null],
categoryId: [this.initialData?.category?.id || null],
minimumStockAlertLevel: [
this.initialData?.minimumStockAlertLevel || 1,
[Validators.required, Validators.min(0)],
],
imageUrl: [null],
salePrice: [this.initialData?.salePrice || 0, [Validators.required, Validators.min(0)]],
});
@@ -66,7 +70,7 @@ export class ProductFullFormComponent {
submit() {
this.form.markAsTouched();
if (this.form.valid) {
this.onSubmit.emit(this.form.value as unknown as IProductRequest);
this.onSubmit.emit(this.form.value as IProductRequest);
}
}
}
+7 -6
View File
@@ -1,6 +1,7 @@
import { Maybe } from '@/core';
import { IPaginatedQuery } from '@/core/models/service.model';
import { IProductBrand, IProductCategory } from './others';
import ISummary from '@/core/models/summary';
import { IProductStockBalanceSummary } from './types';
export interface IGetProductsQueryParams extends IPaginatedQuery {}
@@ -10,17 +11,17 @@ export interface IProductRawResponse {
description?: string;
barcode?: Maybe<string>;
sku: string;
// productType: string;
brand?: IProductBrand;
category?: IProductCategory;
// supplierId: number;
brand?: ISummary;
category?: ISummary;
salePrice?: string;
count?: number;
createdAt: Date;
updatedAt: Date;
deletedAt?: Maybe<Date>;
stock: number;
avgCost?: number;
stockBalances: IProductStockBalanceSummary[];
salesCount: number;
minimumStockAlertLevel: number;
}
export interface IProductResponse extends IProductRawResponse {}
@@ -1,8 +0,0 @@
export interface IProductCategory {
id: number;
name: string;
}
export interface IProductBrand {
id: number;
name: string;
}
+16
View File
@@ -0,0 +1,16 @@
import ISummary from '@/core/models/summary';
export interface IProductCategory {
id: number;
name: string;
}
export interface IProductBrand {
id: number;
name: string;
}
export interface IProductStockBalanceSummary {
avgCost?: number;
quantity: number;
inventory: ISummary;
}
@@ -1,7 +1,7 @@
<app-page-data-list
[pageTitle]="'مدیریت کالاها'"
[addNewCtaLabel]="'افزودن کالا جدید'"
[columns]="columns"
[columns]="columns()"
[showAdd]="true"
emptyPlaceholderTitle="کالای یافت نشد"
emptyPlaceholderDescription="برای افزودن کالا جدید، روی دکمهٔ بالا کلیک کنید."
@@ -14,6 +14,10 @@
(onAdd)="openAddForm()"
(onDetails)="toDetails($event)"
(pageChange)="onPageChange($event)"
/>
>
<ng-template #stockTpl let-item>
<app-shared-simple-stock-text-alert [stock]="item.stock" [minimumStockAlertLevel]="item.minimumStockAlertLevel" />
</ng-template>
</app-page-data-list>
<product-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
@@ -2,7 +2,8 @@ import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { SimpleStockTextAlertComponent } from '@/shared/components/simpleStockTextAlert/simple-stock-text-alert.component';
import { AfterViewInit, Component, signal, TemplateRef, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { ProductFormComponent } from '../components/form.component';
import { productsNamedRoutes } from '../constants';
@@ -13,10 +14,13 @@ import { ProductsStore } from '../store/main';
@Component({
selector: 'app-products',
templateUrl: './list.component.html',
imports: [PageDataListComponent, ProductFormComponent],
standalone: true,
imports: [PageDataListComponent, ProductFormComponent, SimpleStockTextAlertComponent],
})
export class ProductsComponent {
export class ProductsComponent implements AfterViewInit {
@ViewChild('stockTpl', { static: true }) stockTpl!: TemplateRef<any>;
columns = signal<IColumn[]>([]);
constructor(
private productService: ProductsService,
private router: Router,
@@ -25,31 +29,38 @@ export class ProductsComponent {
store.initial();
}
columns = [
{ field: 'sku', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{
field: 'brand',
header: 'برند',
customDataModel(item) {
return item.brand?.name || '-';
ngAfterViewInit() {
this.columns.set([
{ field: 'sku', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{
field: 'brand',
header: 'برند',
customDataModel(item) {
return item.brand?.name || '-';
},
},
},
{
field: 'category',
header: 'دسته‌بندی',
customDataModel(item) {
return item.category?.name || '-';
{
field: 'category',
header: 'دسته‌بندی',
customDataModel(item) {
return item.category?.name || '-';
},
},
},
{
field: 'salePrice',
header: 'قیمت پایه فروش',
type: 'price',
},
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' },
] as IColumn[];
{
field: 'salePrice',
header: 'قیمت پایه فروش',
type: 'price',
},
{
field: 'stockss',
header: 'موجودی کلی',
customDataModel: this.stockTpl,
},
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' },
]);
}
get loading() {
return this.store.loading;
@@ -1,76 +1,80 @@
<div class="flex flex-col gap-4">
<div class="flex items-center">
<div class="grow">
<div class="flex items-center gap-2">
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="['/products']" />
<h3 class="m-0">{{ product()?.name }}</h3>
@if (loading()) {
<p-progressSpinner />
} @else if (product()) {
<app-inner-pages-header [pageTitle]="product()?.name!" [backRoute]="['/products']">
<ng-template #actions>
<button pButton type="button" label="افزایش موجودی" icon="pi pi-plus" (click)="openChargeForm()"></button>
<button
pButton
type="button"
label="ویرایش"
icon="pi pi-pencil"
[routerLink]="['/products', productId, 'update']"
></button>
<button pButton icon="pi pi-trash" severity="danger" (click)="openDeleteConfirm()"></button>
</ng-template>
</app-inner-pages-header>
<div class="inline-flex items-center gap-5">
<div class="">
<app-key-value label="SKU" [value]="product()?.sku || '-'" />
</div>
<div class="">
<app-key-value label="بارکد" [value]="product()?.barcode || '-'" />
</div>
</div>
<div class="flex items-center gap-2">
<button pButton type="button" label="افزایش موجودی" icon="pi pi-plus" (click)="openChargeForm()"></button>
<button
pButton
type="button"
label="ویرایش"
icon="pi pi-pencil"
[routerLink]="['/products', productId, 'update']"
></button>
<button pButton icon="pi pi-trash" severity="danger" (click)="openDeleteConfirm()"></button>
</div>
</div>
<div class="inline-flex items-center gap-5">
<div class="">
<app-key-value label="SKU" [value]="product()?.sku || '-'" />
</div>
<div class="">
<app-key-value label="بارکد" [value]="product()?.barcode || '-'" />
</div>
</div>
<div class="flex gap-4">
<div class="flex grow flex-col gap-4">
<div class="grid grid-cols-4 gap-4">
@for (item of topBarCardDetails; track $index) {
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value.toString()" />
}
</div>
<p-card class="border border-primary-700">
<div class="flex gap-6">
<div class="flex flex-col gap-2 grow">
<span class="font-bold text-lg">توضیحات</span>
<p class="m-0">{{ product()?.description || "بدون توضیحات" }}</p>
</div>
<div class="shrink-0 w-xs">
<p-card class="border border-primary-700">
<div class="flex justify-between items-center p-2 gap-2">
<span class="">دسته‌بندی</span>
<p class="m-0 font-bold text-lg">{{ product()?.category?.name || "بدون دسته‌بندی" }}</p>
</div>
<hr class="m-0" />
<div class="flex justify-between items-center p-2 gap-2">
<span class="">برند</span>
<p class="m-0 font-bold text-lg">{{ product()?.brand?.name || "بدون برند" }}</p>
</div>
</p-card>
</div>
<div class="flex gap-4">
<div class="flex grow flex-col gap-4">
<div class="grid grid-cols-4 gap-4">
<details-info-card icon="pi pi-box" label="موجودی">
<app-shared-simple-stock-text-alert
[stock]="product()?.stock || 0"
[minimumStockAlertLevel]="product()?.minimumStockAlertLevel || 0"
/>
</details-info-card>
@for (item of topBarCardDetails; track $index) {
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value.toString()" />
}
</div>
</p-card>
<p-card class="border border-primary-700">
<div class="flex gap-6">
<div class="flex flex-col gap-2 grow">
<span class="font-bold text-lg">توضیحات</span>
<p class="m-0">{{ product()?.description || "بدون توضیحات" }}</p>
</div>
<div class="shrink-0 w-xs">
<p-card class="border border-primary-700">
<div class="flex justify-between items-center p-2 gap-2">
<span class="">دسته‌بندی</span>
<p class="m-0 font-bold text-lg">{{ product()?.category?.name || "بدون دسته‌بندی" }}</p>
</div>
<hr class="m-0" />
<div class="flex justify-between items-center p-2 gap-2">
<span class="">برند</span>
<p class="m-0 font-bold text-lg">{{ product()?.brand?.name || "بدون برند" }}</p>
</div>
</p-card>
</div>
</div>
</p-card>
</div>
<p-galleria
[value]="images()"
[showItemNavigators]="true"
[circular]="true"
[numVisible]="4"
class="shrink-0 w-md"
containerClass="max-w-md"
>
<ng-template #item let-item>
<img [src]="item.itemImageSrc" style="width: 100%; display: block" />
</ng-template>
<ng-template #thumbnail let-item>
<img [src]="item.itemImageSrc" style="display: block" />
</ng-template>
</p-galleria>
</div>
<p-galleria
[value]="images()"
[showItemNavigators]="true"
[circular]="true"
[numVisible]="4"
class="shrink-0 w-md"
containerClass="max-w-md"
>
<ng-template #item let-item>
<img [src]="item.itemImageSrc" style="width: 100%; display: block" />
</ng-template>
<ng-template #thumbnail let-item>
<img [src]="item.itemImageSrc" style="display: block" />
</ng-template>
</p-galleria>
</div>
<purchase-form [(visible)]="isOpenChargeForm" [product]="product()!" />
<purchase-form [(visible)]="isOpenChargeForm" [product]="product()!" />
}
</div>
@@ -2,12 +2,15 @@ import { Maybe } from '@/core';
import { PurchaseFormComponent } from '@/modules/purchases/components/form.component';
import { KeyValueComponent } from '@/shared/components';
import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component';
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import { SimpleStockTextAlertComponent } from '@/shared/components/simpleStockTextAlert/simple-stock-text-alert.component';
import priceMaskUtils from '@/utils/price-mask.utils';
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { Button, ButtonDirective } from 'primeng/button';
import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { GalleriaModule } from 'primeng/galleria';
import { ProgressSpinner } from 'primeng/progressspinner';
import { IProductResponse } from '../models';
import { ProductsService } from '../services/main.service';
@@ -20,9 +23,11 @@ import { ProductsService } from '../services/main.service';
GalleriaModule,
KeyValueComponent,
Card,
Button,
PurchaseFormComponent,
DetailsInfoCardComponent,
ProgressSpinner,
SimpleStockTextAlertComponent,
InnerPagesHeaderComponent,
],
})
export class ProductComponent {
@@ -79,11 +84,6 @@ export class ProductComponent {
get topBarCardDetails() {
return [
{
icon: 'pi pi-box',
label: 'موجودی',
value: this.product()?.stock || 0,
},
{
icon: 'pi pi-dollar',
label: 'قیمت پایه',
@@ -102,7 +102,7 @@ export class ProductComponent {
{
icon: 'pi pi-clock',
label: 'تعداد فروش',
value: '-',
value: this.product()?.salesCount || 0,
},
];
}
@@ -178,4 +178,8 @@
<button pButton type="button" label="لغو" outlined [disabled]="form.disabled" (click)="onCancel()"></button>
<button pButton type="button" label="ثبت رسید" [loading]="form.disabled" (click)="onSubmit()"></button>
</div>
@if (purchaseReceiptResponse()) {
<app-purchase-receipt-payment-wrapper [purchaseReceipt]="purchaseReceiptResponse()!" />
}
</form>
@@ -7,6 +7,7 @@ import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/
import { ISupplierResponse } from '@/modules/suppliers/models';
import { InlineEditComponent, InputComponent } from '@/shared/components';
import { IColumn } from '@/shared/components/pageDataList/page-data-list.component';
import { PurchaseReceiptPaymentWrapperComponent } from '@/shared/components/purchaseReceiptPayment/wrapper.component';
import { PriceAlphabetDirective, PriceMaskDirective } from '@/shared/directives';
import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { CommonModule } from '@angular/common';
@@ -16,7 +17,7 @@ import dayjs from 'dayjs';
import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { TableModule } from 'primeng/table';
import { IPurchaseRequest } from '../../models';
import { IPurchaseReceiptResponse, IPurchaseRequest } from '../../models';
import { PurchasesService } from '../../services/main.service';
import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-row.component';
@@ -39,6 +40,7 @@ import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-r
CommonModule,
ReactiveFormsModule,
UikitFlatpickrJalaliComponent,
PurchaseReceiptPaymentWrapperComponent,
],
})
export class PurchaseReceiptTemplateComponent {
@@ -142,6 +144,7 @@ export class PurchaseReceiptTemplateComponent {
}
formIsSubmitted = signal(false);
purchaseReceiptResponse = signal<Maybe<IPurchaseReceiptResponse>>(null);
ngOnInit() {
this.formIsSubmitted.set(false);
@@ -203,6 +206,7 @@ export class PurchaseReceiptTemplateComponent {
// this.form.reset();
this.form.controls.code.setValue(this.form.controls.code.value + '1');
this.formIsSubmitted.set(false);
this.purchaseReceiptResponse.set(res);
},
error: () => {
this.form.enable();
+2 -2
View File
@@ -1,6 +1,6 @@
import { Maybe } from '@/core';
export interface IPurchaseRawResponse {
export interface IPurchaseReceiptRawResponse {
id: number;
totalAmount: number;
inventoryId: number;
@@ -12,7 +12,7 @@ export interface IPurchaseRawResponse {
deletedAt?: string;
}
export interface IPurchaseResponse extends IPurchaseRawResponse {}
export interface IPurchaseReceiptResponse extends IPurchaseReceiptRawResponse {}
export interface IPurchaseItemRawResponse {
id: number;
@@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { PURCHASES_API_ROUTES } from '../constants';
import { IPurchaseRawResponse, IPurchaseRequest, IPurchaseResponse } from '../models';
import { IPurchaseReceiptRawResponse, IPurchaseReceiptResponse, IPurchaseRequest } from '../models';
@Injectable({
providedIn: 'root',
@@ -10,15 +10,15 @@ import { IPurchaseRawResponse, IPurchaseRequest, IPurchaseResponse } from '../mo
export class PurchasesService {
private http = inject(HttpClient);
getAll(): Observable<{ data: IPurchaseRawResponse[] }> {
return this.http.get<{ data: IPurchaseResponse[] }>(PURCHASES_API_ROUTES.getAll);
getAll(): Observable<{ data: IPurchaseReceiptRawResponse[] }> {
return this.http.get<{ data: IPurchaseReceiptResponse[] }>(PURCHASES_API_ROUTES.getAll);
}
getSingle(id: number): Observable<{ data: IPurchaseResponse }> {
return this.http.get<{ data: IPurchaseResponse }>(PURCHASES_API_ROUTES.getSingle(id));
getSingle(id: number): Observable<{ data: IPurchaseReceiptResponse }> {
return this.http.get<{ data: IPurchaseReceiptResponse }>(PURCHASES_API_ROUTES.getSingle(id));
}
create(data: IPurchaseRequest): Observable<{ data: IPurchaseRawResponse }> {
return this.http.post<{ data: IPurchaseResponse }>(PURCHASES_API_ROUTES.create, data);
create(data: IPurchaseRequest): Observable<IPurchaseReceiptResponse> {
return this.http.post<IPurchaseReceiptRawResponse>(PURCHASES_API_ROUTES.create, data);
}
}
@@ -0,0 +1,13 @@
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-inventory-bank-account-select [control]="form.controls.bankAccountId" [inventoryId]="inventoryId" />
<app-catalog-payment-method-type-select-field [control]="form.controls.paymentMethod" />
<app-input
label="مبلغ پرداختی"
[control]="form.controls.amount"
name="amount"
prefix="تومان"
type="price"
[hint]="amountFieldHint()"
/>
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
@@ -0,0 +1,57 @@
import { InventoryBankAccountSelectComponent } from '@/modules/inventories/components/bankAccounts/select.component';
import { PaymentMethodTypeSelectComponent } from '@/shared/catalog/paymentMethodTypes/components';
import { PaymentType } from '@/shared/catalog/paymentTypes';
import { InputComponent } from '@/shared/components';
import { AbstractForm } from '@/shared/components/abstract-form';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { formatNumber } from '@/utils/price-mask.utils';
import { Component, computed, inject, Input } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import dayjs from 'dayjs';
import { Dialog } from 'primeng/dialog';
import { IPayRequestPayload, IPayResponse } from '../../models/invoices.io';
import { SupplierInvoicesService } from '../../services';
@Component({
selector: 'app-supplier-invoice-pay-form-template',
templateUrl: './pay-form-template.component.html',
imports: [
Dialog,
ReactiveFormsModule,
InventoryBankAccountSelectComponent,
FormFooterActionsComponent,
InputComponent,
PaymentMethodTypeSelectComponent,
],
})
export class SupplierInvoicePayFormTemplateComponent extends AbstractForm<
IPayResponse,
IPayRequestPayload
> {
@Input() debtAmount!: number;
@Input() supplierId!: string;
@Input() invoiceId!: string;
@Input() inventoryId!: string;
private readonly service = inject(SupplierInvoicesService);
override defaultValues = {
type: PaymentType.PAYMENT,
payedAt: dayjs().format('YYYY-MM-DD'),
};
form = this.fb.group({
amount: [0, [Validators.required, Validators.max(this.debtAmount), Validators.min(1)]],
bankAccountId: [null, [Validators.required]],
paymentMethod: [null, [Validators.required]],
type: [PaymentType.PAYMENT, [Validators.required]],
payedAt: [dayjs().format('YYYY-MM-DD'), [Validators.required]],
description: [''],
});
amountFieldHint = computed(() => `بدهی شما مبلغ ${formatNumber(this.debtAmount)} ریال می‌باشد.`);
submitForm(payload: IPayRequestPayload) {
return this.service.pay(this.supplierId, this.invoiceId, payload);
}
}
@@ -6,18 +6,11 @@
appendTo="body"
[closable]="true"
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
{{ this.debtAmount }}
<app-inventory-bank-account-select [control]="form.controls.bankAccountId" [inventoryId]="inventoryId" />
<app-catalog-payment-method-type-select-field [control]="form.controls.paymentMethod" />
<app-input
label="مبلغ پرداختی"
[control]="form.controls.amount"
name="amount"
prefix="تومان"
type="price"
[hint]="amountFieldHint()"
/>
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
<app-supplier-invoice-pay-form-template
[initialValues]="initialValues"
[inventoryId]="inventoryId"
[supplierId]="supplierId"
[invoiceId]="invoiceId"
[debtAmount]="debtAmount"
/>
</p-dialog>
@@ -1,9 +1,5 @@
import { InventoryBankAccountSelectComponent } from '@/modules/inventories/components/bankAccounts/select.component';
import { PaymentMethodTypeSelectComponent } from '@/shared/catalog/paymentMethodTypes/components';
import { PaymentType } from '@/shared/catalog/paymentTypes';
import { InputComponent } from '@/shared/components';
import { AbstractFormDialog } from '@/shared/components/abstract-form-dialog.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { formatNumber } from '@/utils/price-mask.utils';
import { Component, computed, inject, Input } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms';
@@ -11,18 +7,12 @@ import dayjs from 'dayjs';
import { Dialog } from 'primeng/dialog';
import { IPayRequestPayload, IPayResponse } from '../../models/invoices.io';
import { SupplierInvoicesService } from '../../services';
import { SupplierInvoicePayFormTemplateComponent } from './pay-form-template.component';
@Component({
selector: 'app-supplier-invoice-pay-form',
templateUrl: './pay-form.component.html',
imports: [
Dialog,
ReactiveFormsModule,
InventoryBankAccountSelectComponent,
FormFooterActionsComponent,
InputComponent,
PaymentMethodTypeSelectComponent,
],
imports: [Dialog, ReactiveFormsModule, SupplierInvoicePayFormTemplateComponent],
})
export class SupplierInvoicePayFormComponent extends AbstractFormDialog<
IPayResponse,
@@ -36,6 +36,15 @@ export const suppliersNamedRoutes: NamedRoutes<any> = {
pagePath: () => '/suppliers/:supplierId/invoices/:invoiceId',
},
},
purchase: {
path: 'suppliers/:supplierId/purchase',
loadComponent: () =>
import('../../views/purchase.component').then((m) => m.InventoryPurchaseComponent),
meta: {
title: 'خرید',
pagePath: () => '/suppliers/:supplierId/purchase',
},
},
} as const;
export type TSuppliersRouteNames = (typeof suppliersNamedRoutes)[keyof typeof suppliersNamedRoutes];
@@ -20,6 +20,18 @@
}
</app-key-value>
</div>
<div class="">
<app-key-value label="مبلغ کل فاکتور">
<span [appPriceMask]="invoice()?.totalAmount"></span>
</app-key-value>
</div>
@if (debtAmount() > 0) {
<div class="">
<app-key-value label="مبلغ بدهی">
<span [appPriceMask]="debtAmount()"></span>
</app-key-value>
</div>
}
</div>
<app-invoice-payments [supplierId]="supplierId" [invoiceId]="invoiceId" />
@if (invoice() && supplier()) {
@@ -1,6 +1,8 @@
import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog';
import { KeyValueComponent } from '@/shared/components';
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import { PurchaseReceiptPaymentWrapperComponent } from '@/shared/components/purchaseReceiptPayment/wrapper.component';
import { PriceMaskDirective } from '@/shared/directives';
import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
@@ -18,6 +20,8 @@ import { SupplierInvoiceStore, SupplierStore } from '../store';
ButtonDirective,
SupplierInvoicePayFormComponent,
InvoicePaymentsComponent,
PriceMaskDirective,
PurchaseReceiptPaymentWrapperComponent,
],
})
export class SupplierInvoiceComponent {
@@ -0,0 +1,4 @@
@if (loading()) {
} @else {
<purchase-receipt-template [supplier]="supplier()" />
}
@@ -0,0 +1,22 @@
import { PurchaseReceiptTemplateComponent } from '@/modules/purchases/components';
import { Component, computed, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { SupplierStore } from '../store';
@Component({
selector: 'app-inventory-purchase',
templateUrl: './purchase.component.html',
imports: [PurchaseReceiptTemplateComponent],
})
export class InventoryPurchaseComponent {
private readonly route = inject(ActivatedRoute);
private readonly store = inject(SupplierStore);
supplierId = this.route.snapshot.paramMap.get('supplierId')!;
supplier = computed(() => this.store.entities()[this.supplierId]);
loading = this.store.loading;
constructor() {
this.store.getSingle(this.supplierId);
}
}
@@ -7,7 +7,13 @@
</div>
</div>
<div class="flex items-center gap-2">
<button pButton type="button" label="خرید کالا" icon="pi pi-plus"></button>
<button
pButton
type="button"
label="خرید کالا"
icon="pi pi-plus"
[routerLink]="['/suppliers', supplierId, 'purchase']"
></button>
<button pButton type="button" label="ویرایش" icon="pi pi-pencil"></button>
<button pButton icon="pi pi-trash" severity="danger"></button>
</div>
@@ -1,19 +1,21 @@
import { ToastService } from '@/core/services/toast.service';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { Subscribable } from 'rxjs';
import { ReactiveFormsModule } from '@angular/forms';
import { AbstractForm } from './abstract-form';
@Component({
selector: 'abstract-form-dialog',
template: '',
imports: [ReactiveFormsModule],
})
export abstract class AbstractFormDialog<Response, Request> {
@Input() initialValues?: Response;
export abstract class AbstractFormDialog<Response, Request> extends AbstractForm<
Response,
Request
> {
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
this.visibleChange.emit(!!v);
}
get visible() {
return this.visibleSignal();
@@ -21,58 +23,19 @@ export abstract class AbstractFormDialog<Response, Request> {
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<Response>();
protected readonly fb = inject(FormBuilder);
constructor(private toastService: ToastService) {
constructor() {
super(inject(ToastService));
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset(this.defaultValues);
});
this.submit.bind(() => {
this.close();
});
}
abstract form: ReturnType<FormBuilder['group']>;
defaultValues: Partial<Request> = {};
abstract submitForm(payload: Request): Subscribable<Response> | void;
submitLoading = signal(false);
submit() {
console.log('first');
this.form.markAllAsTouched();
if (this.form.valid) {
console.log('isValid');
console.log(this.submitForm);
this.form.disable();
this.submitLoading.set(true);
this.submitForm(this.form.value as Request)?.subscribe({
next: (res) => {
this.toastService.success({
text: `با موفقیت ایجاد شد`,
});
this.close();
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
complete: () => {
this.submitLoading.set(false);
this.form.enable();
console.log('end');
},
});
}
}
close() {
this.visibleChange.emit(false);
override close() {
this.visibleSignal.set(false);
}
}
@@ -0,0 +1,57 @@
import { ToastService } from '@/core/services/toast.service';
import { Component, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { Subscribable } from 'rxjs';
@Component({
selector: 'abstract-form-dialog',
template: '',
imports: [ReactiveFormsModule],
})
export abstract class AbstractForm<Response, Request> {
@Input() initialValues?: Response;
@Output() onSubmit = new EventEmitter<Response>();
@Output() onClose = new EventEmitter<void>();
protected readonly fb = inject(FormBuilder);
constructor(private toastService: ToastService) {}
abstract form: ReturnType<FormBuilder['group']>;
defaultValues: Partial<Request> = {};
abstract submitForm(payload: Request): Subscribable<Response> | void;
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.submitForm(this.form.value as Request)?.subscribe({
next: (res) => {
this.toastService.success({
text: `با موفقیت ایجاد شد`,
});
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
complete: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.onClose.emit();
}
}
@@ -0,0 +1,19 @@
<p-confirmdialog #cd>
<ng-template #headless let-message let-onAccept="onAccept" let-onReject="onReject">
@if (message) {
<div class="flex flex-col items-center p-8 bg-surface-0 dark:bg-surface-900 rounded">
<div
class="rounded-full bg-primary text-primary-contrast inline-flex justify-center items-center h-24 w-24 -mt-20"
>
<i class="pi pi-question text-5xl!"></i>
</div>
<span class="font-bold text-2xl block mb-2 mt-6">{{ message.header }}</span>
<p class="mb-0">{{ message.message }}</p>
<div class="flex items-center gap-2 mt-6">
<p-button label="{{ acceptLabel }}" (onClick)="onAccept()" styleClass="w-32"></p-button>
<p-button label="{{ rejectLabel }}" [outlined]="true" (onClick)="onReject()" styleClass="w-32"></p-button>
</div>
</div>
}
</ng-template>
</p-confirmdialog>
@@ -0,0 +1,48 @@
import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
import { ConfirmationService } from 'primeng/api';
import { Button } from 'primeng/button';
import { ConfirmDialog } from 'primeng/confirmdialog';
@Component({
selector: 'app-shared-confirmation-dialog',
templateUrl: './confirmation-dialog.component.html',
providers: [ConfirmationService],
imports: [ConfirmDialog, Button],
})
export class ConfirmationDialogComponent implements OnDestroy {
@Input() message!: string;
@Input() header: string = 'تأیید عملیات';
@Input() acceptLabel: string = 'بله';
@Input() rejectLabel: string = 'خیر';
@Output() onAccept = new EventEmitter<void>();
@Output() onReject = new EventEmitter<void>();
constructor(private confirmationService: ConfirmationService) {}
show() {
this.confirmationService.confirm({
header: this.header,
message: this.message,
acceptLabel: this.acceptLabel,
rejectLabel: this.rejectLabel,
accept: () => {
this.accept();
},
reject: () => {
this.reject();
},
});
}
accept() {
this.onAccept.emit();
}
reject() {
this.onReject.emit();
}
ngOnDestroy() {
// Cleanup if needed
}
}
@@ -0,0 +1,78 @@
import {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
Injectable,
Injector,
} from '@angular/core';
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
@Injectable({
providedIn: 'root',
})
export class ConfirmationDialogService {
private componentRef: ComponentRef<ConfirmationDialogComponent> | null = null;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private appRef: ApplicationRef,
private injector: Injector,
) {}
confirm(options: {
message: string;
header?: string;
acceptLabel?: string;
rejectLabel?: string;
accept?: () => void;
reject?: () => void;
}) {
// Create the component dynamically
const factory = this.componentFactoryResolver.resolveComponentFactory(
ConfirmationDialogComponent,
);
this.componentRef = factory.create(this.injector);
// Set inputs
this.componentRef.instance.message = options.message;
if (options.header) this.componentRef.instance.header = options.header;
if (options.acceptLabel) this.componentRef.instance.acceptLabel = options.acceptLabel;
if (options.rejectLabel) this.componentRef.instance.rejectLabel = options.rejectLabel;
// Subscribe to outputs and close after
if (options.accept) {
this.componentRef.instance.onAccept.subscribe(() => {
options.accept!();
this.close();
});
} else {
this.componentRef.instance.onAccept.subscribe(() => this.close());
}
if (options.reject) {
this.componentRef.instance.onReject.subscribe(() => {
options.reject!();
this.close();
});
} else {
this.componentRef.instance.onReject.subscribe(() => this.close());
}
// Attach to the app
this.appRef.attachView(this.componentRef.hostView);
// Append to body
document.body.appendChild(this.componentRef.location.nativeElement);
// Trigger change detection and show
this.componentRef.changeDetectorRef.detectChanges();
this.componentRef.instance.show();
}
close() {
if (this.componentRef) {
this.appRef.detachView(this.componentRef.hostView);
this.componentRef.destroy();
this.componentRef = null;
}
}
}
@@ -5,7 +5,9 @@
</div>
<div class="grow flex flex-col gap-1">
<span class="text-sm font-bold">{{ label }}</span>
<span class="text-xl font-bold text-primary">{{ value }}</span>
<ng-content>
<span class="text-xl font-bold text-primary">{{ value }}</span>
</ng-content>
</div>
</div>
</p-card>
@@ -1,4 +1,5 @@
import { Component, Input } from '@angular/core';
import { Maybe } from '@/core';
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Card } from 'primeng/card';
@Component({
@@ -10,5 +11,7 @@ export class DetailsInfoCardComponent {
@Input() icon: string = '';
@Input() label: string = '';
@Input() value: string = '';
@ViewChild('valueElement') valueElement: Maybe<ElementRef> = null;
constructor() {}
}
@@ -0,0 +1,9 @@
@if (showPaymentForm()) {
<app-supplier-invoice-pay-form
[(visible)]="showPaymentForm"
[debtAmount]="purchaseReceipt.totalAmount"
[supplierId]="purchaseReceipt.supplierId.toString()"
[inventoryId]="purchaseReceipt.inventoryId.toString()"
[invoiceId]="purchaseReceipt.id.toString()"
/>
}
@@ -0,0 +1,48 @@
import { ToastService } from '@/core/services/toast.service';
import { SupplierInvoicePayFormComponent } from '@/modules/suppliers/components/invoices/pay-form.component';
import { Component, Input, signal } from '@angular/core';
import { IPurchaseReceiptResponse } from '../../../modules/purchases/models';
import { ConfirmationDialogService } from '../confirmationDialog/confirmation-dialog.service';
@Component({
selector: 'app-purchase-receipt-payment-wrapper',
templateUrl: './wrapper.component.html',
imports: [SupplierInvoicePayFormComponent],
})
export class PurchaseReceiptPaymentWrapperComponent {
@Input() purchaseReceipt!: IPurchaseReceiptResponse;
showPaymentForm = signal(false);
constructor(
private confirmService: ConfirmationDialogService,
private toastService: ToastService,
) {
setTimeout(() => {
this.showConfirm();
}, 1);
}
showConfirm() {
this.confirmService.confirm({
message: 'آیا پرداخت فاکتور انجام شده است؟',
header: 'وضعیت پرداخت فاکتور',
acceptLabel: 'بله',
rejectLabel: 'خیر',
accept: () => {
this.toPaymentForm();
},
reject: () => {
this.toastService.info({
text: 'می‌توانید از طریق منوی پرداخت فاکتور، در آینده اقدام به پرداخت نمایید.',
title: 'پرداخت فاکتور انجام نشد',
});
},
});
}
submit() {}
toPaymentForm() {
this.showPaymentForm.set(true);
}
}
@@ -0,0 +1,7 @@
<span [class]="'text-xl font-bold ' + (stock! <= minimumStockAlertLevel! || !stock ? 'text-error' : 'text-primary')">
{{ stock || 0 }}
@if (stock! <= minimumStockAlertLevel! || !stock) {
<i class="pi pi-exclamation-triangle ml-2" pTooltip="موجودی کمتر از حداقل سطح هشدار است"></i>
}
</span>
@@ -0,0 +1,13 @@
import { Component, Input } from '@angular/core';
import { Tooltip } from 'primeng/tooltip';
@Component({
selector: 'app-shared-simple-stock-text-alert',
templateUrl: './simple-stock-text-alert.component.html',
imports: [Tooltip],
})
export class SimpleStockTextAlertComponent {
@Input() stock: number = 0;
@Input() minimumStockAlertLevel: number = 0;
constructor() {}
}
@@ -0,0 +1,16 @@
<div class="card mb-0">
<div class="flex justify-between mb-4">
<div>
<span class="block text-muted-color font-medium mb-4">Orders</span>
<div class="text-surface-900 dark:text-surface-0 font-medium text-xl">152</div>
</div>
<div
class="flex items-center justify-center bg-blue-100 dark:bg-blue-400/10 rounded-border"
style="width: 2.5rem; height: 2.5rem"
>
<i class="pi pi-shopping-cart text-blue-500 text-xl!"></i>
</div>
</div>
<span class="text-primary font-medium">24 new </span>
<span class="text-muted-color">since last visit</span>
</div>
@@ -0,0 +1,10 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
@Component({
standalone: true,
selector: 'app-shared-stat-widget',
imports: [CommonModule],
templateUrl: './stat-card.component.html',
})
export class StatWidget {}