This commit is contained in:
2026-03-07 15:14:05 +03:30
parent d0979be722
commit 45d48f79dc
269 changed files with 0 additions and 8960 deletions
@@ -1,9 +0,0 @@
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.name" name="name" />
<bank-branches-select-field [control]="form.controls.branchId" name="branchId" [showClear]="false" />
<app-input label="شماره کارت" [control]="form.controls.cardNumber" name="cardNumber" />
<app-input label="شماره حساب" [control]="form.controls.accountNumber" name="accountNumber" />
<app-input label="شماره شبا" [control]="form.controls.iban" name="iban" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
@@ -1,59 +0,0 @@
import { BankBranchesSelectComponent } from '@/modules/bankBranches/components/select/select.component';
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { IBankAccountsRequest, IBankAccountsResponse } from '../models';
@Component({
selector: 'bank-account-form-template',
templateUrl: './form-template.component.html',
imports: [
ReactiveFormsModule,
InputComponent,
FormFooterActionsComponent,
BankBranchesSelectComponent,
],
})
export class BankAccountFormTemplateComponent {
@Input() initialValues?: IBankAccountsResponse;
@Output() onSubmitForm = new EventEmitter<IBankAccountsRequest>();
@Output() onCancel = new EventEmitter<void>();
fb = inject(FormBuilder);
form = this.fb.group({
name: this.fb.control<string>(this.initialValues?.name || '', {
nonNullable: true,
validators: Validators.required,
}),
branchId: this.fb.control<number>(this.initialValues?.branch?.id || 0, {
nonNullable: true,
validators: Validators.required,
}),
cardNumber: this.fb.control<string>(this.initialValues?.cardNumber || '', {
nonNullable: true,
validators: Validators.required,
}),
accountNumber: this.fb.control<string>(this.initialValues?.accountNumber || '', {
nonNullable: true,
validators: Validators.required,
}),
iban: this.fb.control<string>(this.initialValues?.iban || '', {
nonNullable: true,
validators: Validators.required,
}),
});
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.onSubmitForm.emit(this.form.value as IBankAccountsRequest);
}
}
close() {
this.onCancel.emit();
}
}
@@ -1,10 +0,0 @@
<p-dialog
header="فرم حساب بانکی"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '600px', zIndex: '100000' }"
appendTo="body"
[closable]="true"
>
<bank-account-form-template [initialValues]="initialValues" (onSubmitForm)="submit($event)" (onCancel)="close()" />
</p-dialog>
@@ -1,54 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IBankAccountsRequest, IBankAccountsResponse } from '../models';
import { BankAccountsService } from '../services/main.service';
import { BankAccountFormTemplateComponent } from './form-template.component';
@Component({
selector: 'bank-account-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, BankAccountFormTemplateComponent],
})
export class BankAccountFormComponent {
@Input() initialValues?: IBankAccountsResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IBankAccountsResponse>();
constructor(
private service: BankAccountsService,
private toastService: ToastService,
) {}
submitLoading = signal(false);
submit(payload: IBankAccountsRequest) {
this.submitLoading.set(true);
this.service.create(payload).subscribe({
next: (res) => {
this.toastService.success({
text: `حساب بانکی ${payload.name} با موفقیت ایجاد شد`,
});
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
},
});
}
close() {
this.visibleChange.emit(false);
}
}
@@ -1,30 +0,0 @@
<uikit-field label="حساب بانکی" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[loading]="loading()"
[options]="items()"
optionLabel="name"
[optionValue]="selectOptionValue"
placeholder="انتخاب حساب بانکی"
[formControl]="control"
[showClear]="showClear"
[filter]="true"
appendTo="body"
>
@if (canInsert) {
<ng-template #footer>
<div class="p-3">
<p-button
label="افزودن حساب بانکی جدید"
fluid
severity="secondary"
text
size="small"
icon="pi pi-plus"
(onClick)="onOpenForm()"
/>
</div>
</ng-template>
}
</p-select>
<bank-account-form [(visible)]="isOpenFormDialog" (onSubmit)="refresh()" />
</uikit-field>
@@ -1,29 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Button } from 'primeng/button';
import { Select } from 'primeng/select';
import { IBankAccountsResponse } from '../../models';
import { BankAccountsService } from '../../services/main.service';
import { BankAccountFormComponent } from '../form.component';
@Component({
selector: 'bank-accounts-select-field',
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, BankAccountFormComponent],
})
export class BankAccountsSelectComponent extends AbstractSelectComponent<
IBankAccountsResponse,
IPaginatedResponse<IBankAccountsResponse>
> {
constructor(private service: BankAccountsService) {
super();
this.getData();
}
getDataService() {
return this.service.getAll();
}
}
@@ -1,7 +0,0 @@
const baseUrl = '/api/v1/bank-accounts';
export const BANK_ACCOUNTS_API_ROUTES = {
list: () => `${baseUrl}`,
single: (bankAccountId: string) => `${baseUrl}/${bankAccountId}`,
transactions: (bankAccountId: string) => `${baseUrl}/${bankAccountId}/transactions`,
};
@@ -1,2 +0,0 @@
export * from './apiRoutes';
export * from './routes';
@@ -1,25 +0,0 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TBankAccountsRouteNames = 'bankAccounts' | 'bankAccount';
export const bankAccountsNamedRoutes: NamedRoutes<TBankAccountsRouteNames> = {
bankAccounts: {
path: 'bankAccounts',
loadComponent: () => import('../../views/list.component').then((m) => m.BankAccountsComponent),
meta: {
title: 'حساب‌های بانکی',
pagePath: () => '/bankAccounts',
},
},
bankAccount: {
path: 'bankAccounts/:bankAccountId',
loadComponent: () => import('../../views/single.component').then((m) => m.BankAccountComponent),
meta: {
title: 'حساب‌ بانکی',
pagePath: () => '/bankAccounts/:bankAccountId',
},
},
};
export const BANK_ACCOUNTS_ROUTES: Routes = Object.values(bankAccountsNamedRoutes);
@@ -1 +0,0 @@
export * from './io';
-48
View File
@@ -1,48 +0,0 @@
import { Maybe } from '@/core';
import {
IBankAccountBranchSummary,
IReferencePos,
IReferencePosRaw,
IReferencePurchase,
IReferencePurchaseRaw,
} from './types';
export interface IBankAccountsRawResponse {
id: number;
name: string;
branch: IBankAccountBranchSummary;
currentBalance: number;
accountNumber?: string;
iban?: string;
cardNumber?: string;
createdAt?: string;
updatedAt?: string;
deletedAt?: string;
}
export interface IBankAccountsResponse extends IBankAccountsRawResponse {}
export interface IBankAccountsRequest {
name: string;
branchId: number;
accountNumber?: string;
iban?: string;
cardNumber?: string;
}
export interface IBankAccountTransactionRawResponse {
id: number;
bankAccountId: number;
type: string;
amount: string;
balanceAfter: string;
referenceId: number;
referenceType: string;
description: null;
createdAt: string;
reference: Maybe<IReferencePurchaseRaw | IReferencePosRaw>;
}
export interface IBankAccountTransactionResponse extends IBankAccountTransactionRawResponse {
reference: Maybe<IReferencePurchase | IReferencePos>;
}
@@ -1,46 +0,0 @@
export interface IBankAccountBranchSummary {
id: number;
name: string;
code: string;
bank: Bank;
}
interface Bank {
id: number;
name: string;
shortName: string;
}
interface IReferenceCommon {
id: number;
code: string;
totalAmount: string;
}
export interface IReferencePurchaseRaw extends IReferenceCommon {
supplier: IPersonSummary;
}
export interface IReferencePosRaw extends IReferenceCommon {
customer: IPersonSummary;
}
export interface IReferencePurchase extends IReferenceCommon {
supplier: {
id: number;
name: string;
};
}
export interface IReferencePos extends IReferenceCommon {
customer: {
id: number;
name: string;
};
}
interface IPersonSummary {
id: number;
firstName: string;
lastName: string;
}
@@ -1,99 +0,0 @@
import { Maybe } from '@/core';
import { IPaginatedResponse } from '@/core/models/service.model';
import { BankTransactionReferenceType } from '@/shared/catalog/transactionReferenceTypes';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { BANK_ACCOUNTS_API_ROUTES } from '../constants';
import {
IBankAccountsRawResponse,
IBankAccountsRequest,
IBankAccountsResponse,
IBankAccountTransactionRawResponse,
IBankAccountTransactionResponse,
} from '../models';
import {
IReferencePos,
IReferencePosRaw,
IReferencePurchase,
IReferencePurchaseRaw,
} from '../models/types';
@Injectable({ providedIn: 'root' })
export class BankAccountsService {
constructor(private http: HttpClient) {}
private apiRoutes = BANK_ACCOUNTS_API_ROUTES;
getAll(): Observable<IPaginatedResponse<IBankAccountsResponse>> {
return this.http.get<IPaginatedResponse<IBankAccountsRawResponse>>(this.apiRoutes.list());
}
getSingle(bankAccountId: string): Observable<IBankAccountsResponse> {
return this.http.get<IBankAccountsRawResponse>(this.apiRoutes.single(bankAccountId));
}
create(data: IBankAccountsRequest): Observable<IBankAccountsResponse> {
return this.http.post<IBankAccountsRawResponse>(this.apiRoutes.list(), data);
}
getTransactions(
bankAccountId: string,
): Observable<IPaginatedResponse<IBankAccountTransactionResponse>> {
return this.http
.get<
IPaginatedResponse<IBankAccountTransactionRawResponse>
>(this.apiRoutes.transactions(bankAccountId))
.pipe(
map((res) => ({
meta: res.meta,
data: res.data.map((transaction) => this.transformTransaction(transaction)),
})),
);
}
private transformTransaction(
transaction: IBankAccountTransactionRawResponse,
): IBankAccountTransactionResponse {
const { reference, referenceType, ...rest } = transaction;
let transformedReference: Maybe<IReferencePurchase | IReferencePos> = null;
if (reference) {
if (
referenceType === BankTransactionReferenceType.PURCHASE_PAYMENT ||
referenceType === BankTransactionReferenceType.PURCHASE_REFUND
) {
const purchaseRef = reference as IReferencePurchaseRaw;
transformedReference = {
id: purchaseRef.id,
code: purchaseRef.code,
totalAmount: purchaseRef.totalAmount,
supplier: {
id: purchaseRef.supplier.id,
name: `${purchaseRef.supplier.firstName} ${purchaseRef.supplier.lastName}`,
},
};
} else if (
referenceType === BankTransactionReferenceType.POS_SALE ||
referenceType === BankTransactionReferenceType.POS_REFUND
) {
const posRef = reference as IReferencePosRaw;
transformedReference = {
id: posRef.id,
code: posRef.code,
totalAmount: posRef.totalAmount,
customer: {
id: posRef.customer.id,
name: `${posRef.customer.firstName} ${posRef.customer.lastName}`,
},
};
}
}
return {
...rest,
referenceType,
reference: transformedReference,
};
}
}
@@ -1,76 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { EntityState, EntityStore } from '@/core/state';
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IBankAccountsResponse } from '../models';
import { BankAccountsService } from '../services/main.service';
export interface BankAccountState extends EntityState<IBankAccountsResponse> {}
@Injectable({
providedIn: 'root',
})
export class BankAccountStore extends EntityStore<IBankAccountsResponse, BankAccountState> {
constructor(
private activeRoute: ActivatedRoute,
private service: BankAccountsService,
private toastService: ToastService,
) {
super({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
entities: {},
selectedId: null,
ids: [],
});
this.initial();
}
supplierId!: string;
initial() {}
getSingle(bankAccountId: string) {
if (this.entities()[bankAccountId]) {
return;
}
this.patchState({ loading: true });
this.service.getSingle(bankAccountId).subscribe({
next: (res) => {
this.patchState({
entities: { [res.id]: res },
loading: false,
});
},
error: (err) => {
this.patchState({ loading: false, error: err });
this.toastService.error({
text: 'خطا در دریافت اطلاعات حساب بانکی',
});
},
});
}
refresh(bankAccountId: string) {
const { bankAccountId: _, ...entities } = this.entities();
this.patchState({
entities,
});
this.getSingle(bankAccountId);
}
reset(): void {
const queryParams = this.activeRoute.snapshot.queryParams;
this.setState({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
entities: {},
selectedId: null,
ids: [],
});
}
}
@@ -1,2 +0,0 @@
export * from './list.component';
export * from './single.component';
@@ -1,15 +0,0 @@
<app-page-data-list
[pageTitle]="'مدیریت حساب‌های بانک'"
[addNewCtaLabel]="'افزودن حساب جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="حسابی یافت نشد"
emptyPlaceholderDescription="برای افزودن حساب جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
(onAdd)="openAddForm()"
(onDetails)="toDetails($event)"
/>
<bank-account-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
@@ -1,89 +0,0 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { Router } from '@angular/router';
import { BankAccountFormComponent } from '../components/form.component';
import { IBankAccountsResponse } from '../models';
import { BankAccountsService } from '../services/main.service';
@Component({
selector: 'app-bank-accounts',
templateUrl: './list.component.html',
imports: [PageDataListComponent, BankAccountFormComponent],
})
export class BankAccountsComponent {
constructor(
private service: BankAccountsService,
private router: Router,
) {
this.getData();
}
columns = [
{ field: 'id', header: 'شناسه', width: '60px' },
{
field: 'name',
header: 'نام',
minWidth: '160px',
},
{
field: 'bank',
header: 'شعبه',
customDataModel(item) {
return `شعبه‌ی ${item.branch?.name} بانک ${item.branch?.bank?.name}`;
},
minWidth: '200px',
},
{
field: 'currentBalance',
header: 'مانده حساب',
type: 'price',
minWidth: '180px',
},
{
field: 'accountNumber',
header: 'شماره حساب بانکی',
canCopy: true,
minWidth: '160px',
},
{
field: 'iban',
header: 'شماره شبا',
canCopy: true,
minWidth: '200px',
},
{
field: 'cardNumber',
header: 'شماره کارت بانکی',
canCopy: true,
minWidth: '160px',
},
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date', minWidth: '120px' },
] as IColumn[];
loading = signal(false);
items = signal<IBankAccountsResponse[]>([]);
visibleForm = signal(false);
refresh() {
this.getData();
}
getData() {
this.loading.set(true);
this.service.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res.data);
});
}
openAddForm() {
this.visibleForm.set(true);
}
toDetails(item: IBankAccountsResponse) {
this.router.navigate(['/bankAccounts', item.id]);
}
}
@@ -1,48 +0,0 @@
<div class="flex flex-col gap-4">
<app-inner-pages-header [pageTitle]="pageTitle()" [backRoute]="['/bankAccounts']" />
@if (!loading()) {
<div class="inline-flex items-center gap-5">
<div class="">
<app-key-value label="بانک" [value]="bankAccount().branch.bank.name" />
</div>
<div class="">
<app-key-value label="شعبه‌ی بانک" [value]="bankAccount().branch.name" />
</div>
<div class="">
<app-key-value label="موجودی حساب">
<span [appPriceMask]="bankAccount().currentBalance"></span>
</app-key-value>
</div>
</div>
}
<app-page-data-list
[pageTitle]="'تراکنش‌های حساب بانکی'"
[columns]="transactionsColumn"
[items]="transactions()"
[loading]="transactionsLoading()"
[totalRecords]="transactionsPagination()?.totalRecords || 0"
[perPage]="transactionsPagination()?.perPage || 0"
[currentPage]="transactionsPagination()?.page || 1"
emptyPlaceholderTitle="تراکنشی یافت نشد"
emptyPlaceholderDescription="این حساب بانکی تاکنون تراکنشی نداشته است."
>
<ng-template #typeTpl let-item>
<catalog-bank-account-transaction-type-tag [type]="item.type" />
</ng-template>
<ng-template #referenceTypeTpl let-item>
<catalog-bank-account-transaction-reference-type-tag [type]="item.referenceType" />
</ng-template>
<ng-template #counterpartyTpl let-item>
@if (item.referenceType === "PURCHASE_PAYMENT" || item.referenceType === "PURCHASE_REFUND") {
{{ item.reference.supplier?.name || "-" }}
} @else if (item.referenceType === "CUSTOMER_PAYMENT" || item.referenceType === "CUSTOMER_REFUND") {
{{ item.reference.customer?.name || "-" }}
} @else {
{{ item.referenceType }}
}
</ng-template>
</app-page-data-list>
<!-- (onPageChange)="getTransactions($event.page + 1)" -->
</div>
@@ -1,85 +0,0 @@
import { Maybe } from '@/core';
import { IResponseMetadata } from '@/core/models/service.model';
import { CatalogBankAccountTransactionReferenceTypeTagComponent } from '@/shared/catalog/transactionReferenceTypes';
import { CatalogBankAccountTransactionTypeTagComponent } from '@/shared/catalog/transactionTypes';
import { KeyValueComponent } from '@/shared/components';
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { PriceMaskDirective } from '@/shared/directives';
import { Component, computed, inject, signal, TemplateRef, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IBankAccountTransactionResponse } from '../models';
import { BankAccountsService } from '../services/main.service';
import { BankAccountStore } from '../store/bankAccount.store';
@Component({
selector: 'app-bank-account',
templateUrl: './single.component.html',
imports: [
InnerPagesHeaderComponent,
KeyValueComponent,
PriceMaskDirective,
PageDataListComponent,
CatalogBankAccountTransactionTypeTagComponent,
CatalogBankAccountTransactionReferenceTypeTagComponent,
],
})
export class BankAccountComponent {
private route = inject(ActivatedRoute);
private store = inject(BankAccountStore);
@ViewChild('typeTpl', { static: true }) typeTpl!: TemplateRef<any>;
@ViewChild('referenceTypeTpl', { static: true }) referenceTypeTpl!: TemplateRef<any>;
@ViewChild('counterpartyTpl', { static: true }) counterpartyTpl!: TemplateRef<any>;
bankAccountId = this.route.snapshot.paramMap.get('bankAccountId')!;
transactionsColumn = [] as IColumn[];
bankAccount = computed(() => this.store.entities()[this.bankAccountId]);
loading = this.store.loading;
transactions = signal<IBankAccountTransactionResponse[]>([]);
transactionsLoading = signal<boolean>(false);
transactionsPagination = signal<Maybe<IResponseMetadata>>(null);
constructor(private readonly service: BankAccountsService) {
this.store.getSingle(this.bankAccountId);
this.getTransactions();
}
pageTitle = computed(() => {
const account = this.bankAccount();
return account ? `حساب بانک: ${account.name}` : 'حساب بانک';
});
ngOnInit() {
this.transactionsColumn = [
{ field: 'id', header: 'شناسه', width: '60px' },
{ field: 'type', header: 'نوع تراکنش', customDataModel: this.typeTpl },
{ field: 'amount', header: 'مبلغ', type: 'price' },
{ field: 'balanceAfter', header: 'مانده حساب', type: 'price' },
{ field: 'type', header: 'نوع رسید', customDataModel: this.referenceTypeTpl },
{ field: 'counterparty', header: 'طرف حساب', customDataModel: this.counterpartyTpl },
{ field: 'description', header: 'توضیحات' },
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
];
}
getTransactions() {
this.transactionsLoading.set(true);
this.service.getTransactions(this.bankAccountId).subscribe({
next: (res) => {
this.transactions.set(res.data);
this.transactionsPagination.set(res.meta);
this.transactionsLoading.set(false);
},
error: () => {
this.transactionsLoading.set(false);
},
});
}
}
@@ -1,15 +0,0 @@
<p-dialog
header="فرم شعبه بانک"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '600px', zIndex: '100000' }"
appendTo="body"
[closable]="true"
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="عنوان" [control]="form.controls.name" name="name" />
<app-input label="کد شعبه" [control]="form.controls.code" name="code" />
<banks-select-field [control]="form.controls.bankId" name="bankId" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -1,92 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { BanksSelectComponent } from '@/shared/catalog/banks';
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IBankBranchRequest, IBankBranchResponse } from '../models';
import { BankBranchesService } from '../services/main.service';
@Component({
selector: 'bank-branch-form',
templateUrl: './form.component.html',
imports: [
ReactiveFormsModule,
Dialog,
InputComponent,
FormFooterActionsComponent,
BanksSelectComponent,
],
})
export class BankBranchFormComponent {
@Input() initialValues?: IBankBranchResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IBankBranchResponse>();
private fb = inject(FormBuilder);
constructor(
private service: BankBranchesService,
private toastService: ToastService,
) {
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset();
});
}
form = this.fb.group({
name: this.fb.control<string>(this.initialValues?.name || '', {
nonNullable: true,
validators: Validators.required,
}),
code: this.fb.control<string>(this.initialValues?.code || '', {
nonNullable: true,
validators: Validators.required,
}),
bankId: this.fb.control<number>(this.initialValues?.bank?.id || 0, {
nonNullable: true,
validators: Validators.required,
}),
});
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.service.create(this.form.value as IBankBranchRequest).subscribe({
next: (res) => {
this.toastService.success({
text: `شعبه بانک ${this.form.value.name} با موفقیت ایجاد شد`,
});
this.close();
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -1,30 +0,0 @@
<uikit-field label="شعبه‌ی بانک" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[loading]="loading()"
[options]="items()"
optionLabel="name"
[optionValue]="selectOptionValue"
placeholder="انتخاب شعبه‌ی بانک"
[formControl]="control"
[showClear]="showClear"
[filter]="true"
appendTo="body"
>
@if (canInsert) {
<ng-template #footer>
<div class="p-3">
<p-button
label="افزودن شعبه‌ی جدید"
fluid
severity="secondary"
text
size="small"
icon="pi pi-plus"
(onClick)="onOpenForm()"
/>
</div>
</ng-template>
}
</p-select>
<bank-branch-form [(visible)]="isOpenFormDialog" (onSubmit)="refresh()" />
</uikit-field>
@@ -1,29 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Button } from 'primeng/button';
import { Select } from 'primeng/select';
import { IBankBranchResponse } from '../../models';
import { BankBranchesService } from '../../services/main.service';
import { BankBranchFormComponent } from '../form.component';
@Component({
selector: 'bank-branches-select-field',
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, BankBranchFormComponent],
})
export class BankBranchesSelectComponent extends AbstractSelectComponent<
IBankBranchResponse,
IPaginatedResponse<IBankBranchResponse>
> {
constructor(private service: BankBranchesService) {
super();
this.getData();
}
getDataService() {
return this.service.getAll();
}
}
@@ -1,5 +0,0 @@
const baseUrl = '/api/v1/bank-branches';
export const BANK_BRANCHES_API_ROUTES = {
list: () => `${baseUrl}`,
};
@@ -1,2 +0,0 @@
export * from './apiRoutes';
export * from './routes';
@@ -1,17 +0,0 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TBankBranchesRouteNames = 'bankBranches';
export const bankBranchesNamedRoutes: NamedRoutes<TBankBranchesRouteNames> = {
bankBranches: {
path: 'bankBranches',
loadComponent: () => import('../../views/list.component').then((m) => m.BankBranchesComponent),
meta: {
title: 'شعب بانک',
pagePath: () => '/bankBranches',
},
},
};
export const BANK_BRANCHES_ROUTES: Routes = Object.values(bankBranchesNamedRoutes);
@@ -1 +0,0 @@
export * from './io';
-19
View File
@@ -1,19 +0,0 @@
import { IBankResponse } from '@/shared/catalog/banks/models';
export interface IBankBranchRawResponse {
id: number;
name: string;
code: string;
bank: IBankResponse;
createdAt: Date;
updatedAt: Date;
deletedAt: Date;
}
export interface IBankBranchResponse extends IBankBranchRawResponse {}
export interface IBankBranchRequest {
name: string;
code: string;
bankId: number;
}
@@ -1,21 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { BANK_BRANCHES_API_ROUTES } from '../constants';
import { IBankBranchRawResponse, IBankBranchRequest, IBankBranchResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class BankBranchesService {
constructor(private http: HttpClient) {}
private apiRoutes = BANK_BRANCHES_API_ROUTES;
getAll(): Observable<IPaginatedResponse<IBankBranchResponse>> {
return this.http.get<IPaginatedResponse<IBankBranchRawResponse>>(this.apiRoutes.list());
}
create(data: IBankBranchRequest): Observable<IBankBranchResponse> {
return this.http.post<IBankBranchRawResponse>(this.apiRoutes.list(), data);
}
}
@@ -1,2 +0,0 @@
export * from './list.component';
export * from './single.component';
@@ -1,14 +0,0 @@
<app-page-data-list
[pageTitle]="'مدیریت شعب بانک'"
[addNewCtaLabel]="'افزودن شعبه‌ی جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="شعبه‌ای یافت نشد"
emptyPlaceholderDescription="برای افزودن شعبه جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
(onAdd)="openAddForm()"
/>
<bank-branch-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
@@ -1,48 +0,0 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { BankBranchFormComponent } from '../components/form.component';
import { IBankBranchResponse } from '../models';
import { BankBranchesService } from '../services/main.service';
@Component({
selector: 'app-bank-branches',
templateUrl: './list.component.html',
imports: [PageDataListComponent, BankBranchFormComponent],
})
export class BankBranchesComponent {
constructor(private service: BankBranchesService) {
this.getData();
}
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{ field: 'code', header: 'کد شعبه' },
{ field: 'bank', header: 'بانک', type: 'nested', nestedPath: 'name' },
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' },
] as IColumn[];
loading = signal(false);
items = signal<IBankBranchResponse[]>([]);
visibleForm = signal(false);
refresh() {
this.getData();
}
getData() {
this.loading.set(true);
this.service.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res.data);
});
}
openAddForm() {
this.visibleForm.set(true);
}
}
@@ -1 +0,0 @@
<div class=""></div>
@@ -1,9 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-customer',
templateUrl: './single.component.html',
})
export class CustomerComponent {
constructor() {}
}
@@ -1,19 +0,0 @@
<form [formGroup]="form" class="flex items-end gap-4">
<div class="grow grid grid-cols-4 gap-2">
<inventories-select-field
[control]="form.controls.inventoryId"
(onChange)="onChangeInventory($event)"
(onSetDefaultDataItem)="setDefaultInventory($event)"
/>
<products-select-field
[control]="form.controls.productId"
(onChange)="onChangeProduct($event)"
(onSetDefaultDataItem)="setDefaultProduct($event)"
/>
<uikit-datepicker [control]="form.controls.startDate" label="از تاریخ" />
<uikit-datepicker [control]="form.controls.endDate" label="تا تاریخ" />
</div>
<div class="shrink-0">
<button pButton (click)="search()">جستجو</button>
</div>
</form>
@@ -1,68 +0,0 @@
import { Maybe } from '@/core';
import { InventoriesSelectComponent } from '@/modules/inventories/components';
import { IInventorySummaryResponse } from '@/modules/inventories/models';
import { ProductsSelectComponent } from '@/modules/products/components/select/select.component';
import { IProductResponse } from '@/modules/products/models';
import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { Component, EventEmitter, inject, Output } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
import {
ICardexInventorySummary,
ICardexProductSummary,
ICardexRequestPayload,
} from '../../models';
import { CardexStore } from '../../store/main.store';
@Component({
selector: 'cardex-filters',
templateUrl: './filters.component.html',
imports: [
ReactiveFormsModule,
InventoriesSelectComponent,
ProductsSelectComponent,
UikitFlatpickrJalaliComponent,
ButtonDirective,
],
})
export class CardexFiltersComponent {
private route = inject(ActivatedRoute);
private fb = inject(FormBuilder);
private store = inject(CardexStore);
@Output() onSearch = new EventEmitter<void>();
filters = {} as Partial<ICardexRequestPayload>;
constructor(private router: Router) {
this.filters = this.store.filters();
this.form.patchValue(this.filters);
}
form = this.fb.group({
inventoryId: [this.filters.inventoryId || 0],
productId: [this.filters.productId || 0],
startDate: [this.filters.startDate || ''],
endDate: [this.filters.endDate || ''],
});
onChangeInventory(inventory: Maybe<ICardexInventorySummary>) {
this.store.setInventory(inventory);
}
onChangeProduct(product: Maybe<ICardexProductSummary>) {
this.store.setProduct(product);
}
search() {
this.store.updateFilter(this.form.value as ICardexRequestPayload);
this.store.getCardex();
}
setDefaultInventory(inventory: IInventorySummaryResponse) {
this.store.setDefaultInventory(inventory);
}
setDefaultProduct(product: IProductResponse) {
this.store.setDefaultProduct(product);
}
}
@@ -1 +0,0 @@
export * from './filters/filters.component';
@@ -1,81 +0,0 @@
<p-table
stripedRows
[value]="items"
[loading]="loading"
showGridlines
tableStyleClass="table-fixed border-collapse"
scrollable
>
<ng-template #header>
<tr>
<th rowspan="2" [style]="{ textAlign: 'center', width: '4rem' }">ردیف</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '8rem' }">تاریخ</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">شرح</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '6rem' }">شماره</th>
<th rowspan="2" [style]="{ textAlign: 'center', minWidth: '14rem', width: '14rem' }">طرف حساب</th>
@if (variant === "inventory") {
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">کالا</th>
} @else if (variant === "product") {
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">انبار</th>
}
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">ورودی</th>
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">خروجی</th>
<th colspan="1" class="text-center!" [style]="{ width: '5rem' }">مانده</th>
</tr>
<tr>
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
<th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
<th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
<!-- <th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
<th [style]="{ textAlign: 'center', width: '10rem' }">مجموع قیمت</th> -->
</tr>
</ng-template>
<ng-template #body let-item let-i="rowIndex">
<tr>
<td class="text-center!">{{ i + 1 }}</td>
<td class="text-center!" [jalaliDate]="item.createdAt"></td>
<td class="text-center!">
<catalog-movement-reference-tag
[type]="item.referenceType"
[movementType]="item.type"
[counterName]="item.counterInventory?.name"
/>
</td>
<td class="text-center!">{{ item.referenceId || "-" }}</td>
<td class="text-center!">
{{ (item.referenceType === "SALES" ? item.customer?.name : item.supplier?.name) || "-" }}
</td>
@if (variant === "inventory") {
<td class="text-center!">{{ item.product.name || "-" }}</td>
} @else if (variant === "product") {
<td class="text-center!">{{ item.inventory.name || "-" }}</td>
}
<td class="text-center!">
{{ item.type === "IN" ? item.quantity : 0 }}
</td>
<td class="text-center!">
@if (item.type === "IN") {
<span [appPriceMask]="item.unitPrice"></span>
} @else {
0
}
</td>
<td class="text-center!">
{{ item.type === "OUT" ? item.quantity : 0 }}
</td>
<td class="text-center!">
@if (item.type === "OUT") {
<span [appPriceMask]="item.unitPrice"></span>
} @else {
0
}
</td>
<td class="text-center!">{{ item.remainedInStock }}</td>
<!-- <td class="text-center!"><span [appPriceMask]="item.unitPrice"></span></td>
<td class="text-center!"><span [appPriceMask]="item.totalCost"></span></td> -->
</tr>
</ng-template>
</p-table>
@@ -1,23 +0,0 @@
import { CatalogMovementReferenceTagComponent } from '@/shared/catalog/movementReferenceTypes';
import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives';
import { Component, Input } from '@angular/core';
import { TableModule } from 'primeng/table';
import { IInventoryCardex } from './types';
@Component({
selector: 'cardex-templates-inventory',
templateUrl: './inventory.component.html',
imports: [
TableModule,
JalaliDateDirective,
CatalogMovementReferenceTagComponent,
PriceMaskDirective,
],
hostDirectives: [PriceMaskDirective],
})
export class CardexComponent {
@Input() items: IInventoryCardex[] = [];
@Input() loading: boolean = false;
@Input() variant: 'inventory' | 'product' | 'general' = 'inventory';
constructor() {}
}
@@ -1,3 +0,0 @@
import { ICardexResponse } from '../../models';
export interface IInventoryCardex extends ICardexResponse {}
@@ -1,5 +0,0 @@
const baseUrl = '/api/v1/cardex';
export const CARDEX_API_ROUTES = {
cardex: () => `${baseUrl}`,
};
@@ -1,2 +0,0 @@
export * from './apiRoutes';
export * from './routes';
@@ -1,17 +0,0 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TCARDEXRouteNames = 'CARDEX';
export const cardexNamedRoutes: NamedRoutes<TCARDEXRouteNames> = {
CARDEX: {
path: 'cardex',
loadComponent: () => import('../../views/cardex.component').then((m) => m.CardexPageComponent),
meta: {
title: 'کاردکس',
pagePath: () => '/cardex',
},
},
};
export const CARDEX_ROUTES: Routes = Object.values(cardexNamedRoutes);
-2
View File
@@ -1,2 +0,0 @@
export * from './io';
export * from './types';
-44
View File
@@ -1,44 +0,0 @@
import { ICardexInventorySummary, ICardexProductSummary } from './types';
export interface ICardexRawResponse {
id: number;
type: string;
quantity: number;
unitPrice: number;
totalCost: number;
referenceType: string;
referenceId: string;
createdAt: string;
avgCost: number;
product: ICardexProductSummary;
inventory: ICardexInventorySummary;
supplier?: {
id: number;
firstName: string;
lastName: string;
};
customer?: {
id: number;
firstName: string;
lastName: string;
};
counterInventory?: ICardexInventorySummary;
}
export interface ICardexResponse extends ICardexRawResponse {
supplier?: {
id: number;
name: string;
};
customer?: {
id: number;
name: string;
};
}
export interface ICardexRequestPayload {
startDate?: string;
endDate?: string;
inventoryId?: number;
productId?: number;
}
-10
View File
@@ -1,10 +0,0 @@
export interface ICardexInventorySummary {
name: string;
id: number;
}
export interface ICardexProductSummary {
name: string;
sku: string;
id: number;
}
@@ -1,43 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { getFullName } from '@/utils';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { CARDEX_API_ROUTES } from '../constants';
import { ICardexRawResponse, ICardexRequestPayload, ICardexResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class CardexService {
constructor(private http: HttpClient) {}
private apiRoutes = CARDEX_API_ROUTES;
getCardex(params: ICardexRequestPayload): Observable<IPaginatedResponse<ICardexResponse>> {
return this.http
.get<IPaginatedResponse<ICardexRawResponse>>(this.apiRoutes.cardex(), {
params: { ...params },
})
.pipe(
map((res) => {
return {
meta: res.meta,
data: res.data.map((item) => ({
...item,
supplier: item.supplier
? {
...item.supplier,
name: getFullName(item.supplier),
}
: undefined,
customer: item.customer
? {
...item.customer,
name: getFullName(item.customer),
}
: undefined,
})),
};
}),
);
}
}
-220
View File
@@ -1,220 +0,0 @@
import { Maybe } from '@/core';
import { ToastService } from '@/core/services/toast.service';
import { PaginatedState, PaginatedStore } from '@/core/state';
import { computed, Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {
ICardexInventorySummary,
ICardexProductSummary,
ICardexRequestPayload,
ICardexResponse,
} from '../models';
import { CardexService } from '../services/main.service';
export interface CardexState extends PaginatedState<ICardexResponse> {
filters: ICardexRequestPayload;
selectedInventory: Maybe<ICardexInventorySummary>;
selectedProduct: Maybe<ICardexProductSummary>;
variant: 'inventory' | 'product' | 'general';
canChangeVariant: boolean;
cardexTitle: string;
}
@Injectable({
providedIn: 'root',
})
export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
private readonly _cardexState = {
isRefreshing: false,
filters: {},
selectedInventory: null,
selectedProduct: null,
variant: 'general',
canChangeVariant: true,
};
constructor(
private router: Router,
private route: ActivatedRoute,
private service: CardexService,
private toastService: ToastService,
) {
const queryParams = route.snapshot.queryParams;
super({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
items: [],
totalCount: 0,
currentPage: Number(queryParams['page']) || 1,
pageSize: Number(queryParams['pageSize']) || 30,
totalPages: 0,
hasMore: false,
filters: {
inventoryId: queryParams['inventoryId']
? parseInt(queryParams['inventoryId'], 10)
: undefined,
productId: queryParams['productId'] ? parseInt(queryParams['productId'], 10) : undefined,
startDate: queryParams['startDate'],
endDate: queryParams['endDate'],
},
selectedInventory: null,
selectedProduct: null,
variant: 'general',
canChangeVariant: true,
cardexTitle: 'کاردکس',
});
this.initial();
}
readonly filters = computed(() => this._state().filters);
readonly variant = computed(() => this._state().variant);
readonly canChangeVariant = computed(() => this._state().canChangeVariant);
readonly cardexTitle = computed(() => this._state().cardexTitle);
/**
* Reset state to initial values
*/
reset(): void {
const queryParams = this.route.snapshot.queryParams;
this.setState({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
items: [],
totalCount: 0,
currentPage: Number(queryParams['page']) || 1,
pageSize: Number(queryParams['pageSize']) || 30,
totalPages: 0,
hasMore: false,
filters: {
inventoryId: queryParams['inventoryId'],
productId: queryParams['productId'],
startDate: queryParams['startDate'],
endDate: queryParams['endDate'],
},
selectedInventory: null,
selectedProduct: null,
variant: 'general',
canChangeVariant: this.state().canChangeVariant,
cardexTitle: 'کاردکس',
});
}
initial() {
this.getCardex();
}
getCardex() {
const filters = this.validateFilters();
if (filters) {
this.patchState({ initialized: true });
this.setLoading(true);
this.service.getCardex(filters).subscribe({
next: (res) => {
this.setItems(res.data, res.meta);
},
error: (err) => {
this.setError(err);
},
});
}
}
private validateFilters() {
const { productId, inventoryId, startDate, endDate } = this.filters();
const filters = {} as Partial<ICardexRequestPayload>;
Object.entries(this.filters()).forEach(([key, value]) => {
if (value && value !== '0') {
filters[key as keyof ICardexRequestPayload] = value;
}
});
if (!productId && !inventoryId) {
this.toastService.warn({ text: 'برای مشاهده‌ی کاردکس حداقل انبار و یا کالا را مشخص کنید' });
return false;
}
return filters;
}
onPageChange(page: number) {
this.setCurrentPage(page);
this.getCardex();
}
updateFilter(filters: ICardexRequestPayload) {
this.updateVariant();
const cleanedFilters = this.validateFilters() || {};
this.router
.navigate([], {
relativeTo: this.route,
queryParams: cleanedFilters,
})
.then((res) => console.log(res))
.catch((err) => {
console.log(err);
});
this.patchState({ filters });
this.updateCardexTitle();
}
setProduct(product: Maybe<ICardexProductSummary>) {
this.patchState({ selectedProduct: product });
}
setInventory(inventory: Maybe<ICardexInventorySummary>) {
this.patchState({ selectedInventory: inventory });
}
setCanChangeVariant(status: boolean) {
this.patchState({ canChangeVariant: status });
}
setDefaultInventory(inventory: ICardexInventorySummary) {
this.patchState({ selectedInventory: inventory });
this.updateCardexTitle();
}
setDefaultProduct(product: ICardexProductSummary) {
this.patchState({ selectedProduct: product });
this.updateCardexTitle();
}
private updateVariant() {
const inventoryIsSelected = Boolean(this._state().selectedInventory);
const productIsSelected = Boolean(this._state().selectedProduct);
let variant = 'general' as 'product' | 'inventory' | 'general';
if (inventoryIsSelected) {
if (!productIsSelected) {
variant = 'inventory';
}
} else if (productIsSelected) {
variant = 'product';
}
this.updateCardexTitle();
this.patchState({ variant });
}
updateCardexTitle() {
let title = '';
switch (this._state().variant) {
case 'product':
title = `کاردکس کالای ${this._state().selectedProduct?.name}`;
break;
case 'inventory':
title = `کاردکس انبار ${this._state().selectedInventory?.name}`;
break;
default:
title = `کاردکس ${this._state().selectedProduct ? 'کالای ' + this._state().selectedProduct?.name : ''} ${
this._state().selectedInventory ? 'در انبار ' + this._state().selectedInventory?.name : ''
}`;
break;
}
this.patchState({ cardexTitle: title });
}
}
@@ -1,14 +0,0 @@
<div class="flex flex-col gap-4">
<p-card>
<cardex-filters />
</p-card>
<p-card>
@if (!initialized) {
<uikit-empty-state title="برای مشاهده‌ی کاردکس اطلاعات بالا را تکمیل کنید"> </uikit-empty-state>
} @else {
<h3 class="mb-4">{{ cardexTitle }}</h3>
<cardex-templates-inventory [items]="items" [loading]="loading" [variant]="variant" />
}
</p-card>
</div>
@@ -1,31 +0,0 @@
import { UikitEmptyStateComponent } from '@/uikit';
import { Component } from '@angular/core';
import { Card } from 'primeng/card';
import { CardexFiltersComponent } from '../components';
import { CardexComponent } from '../components/templates/inventory.component';
import { CardexStore } from '../store/main.store';
@Component({
selector: 'app-cardex',
templateUrl: './cardex.component.html',
imports: [Card, CardexFiltersComponent, UikitEmptyStateComponent, CardexComponent],
})
export class CardexPageComponent {
constructor(private store: CardexStore) {}
get cardexTitle() {
return this.store.cardexTitle();
}
get initialized() {
return this.store.initialized();
}
get loading() {
return this.store.loading();
}
get variant() {
return this.store.variant();
}
get items() {
return this.store.items();
}
}
@@ -1,21 +0,0 @@
<p-dialog
header="فرم مشتری"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '600px', zIndex: '100000' }"
appendTo="body"
[closable]="true"
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.firstName" name="firstName" />
<app-input label="نام خانوادگی" [control]="form.controls.lastName" name="lastName" />
<app-input label="ایمیل" [control]="form.controls.email" name="email" type="email" />
<app-input label="شماره موبایل" [control]="form.controls.mobileNumber" name="mobileNumber" type="mobile" />
<app-input label="آدرس" [control]="form.controls.address" name="address" />
<app-input label="شهر" [control]="form.controls.city" name="city" />
<app-input label="استان" [control]="form.controls.state" name="state" />
<app-input label="کشور" [control]="form.controls.country" name="country" />
<app-input label="وضعیت" [control]="form.controls.isActive" name="isActive" type="switch" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -1,82 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { ICustomerRequest, ICustomerResponse } from '../models';
import { CustomersService } from '../services/main.service';
@Component({
selector: 'customer-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
})
export class CustomerFormComponent {
@Input() initialValues?: ICustomerResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<ICustomerResponse>();
private fb = inject(FormBuilder);
constructor(
private service: CustomersService,
private toastService: ToastService,
) {
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset();
});
}
form = this.fb.group({
firstName: [this.initialValues?.firstName || null, [Validators.required]],
lastName: [this.initialValues?.lastName || null, [Validators.required]],
email: [this.initialValues?.email || null, [Validators.required, Validators.email]],
mobileNumber: [this.initialValues?.mobileNumber || null, [Validators.required]],
address: [this.initialValues?.address || null, [Validators.required]],
city: [this.initialValues?.city || null, [Validators.required]],
state: [this.initialValues?.state || null, [Validators.required]],
country: [this.initialValues?.country || null, [Validators.required]],
isActive: [this.initialValues?.isActive || false, [Validators.required]],
});
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.service.create(this.form.value as ICustomerRequest).subscribe({
next: (res) => {
this.toastService.success({
text: `مشتری ${this.form.value.firstName} با موفقیت ایجاد شد`,
});
this.close();
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -1,30 +0,0 @@
<uikit-field label="مشتری" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[loading]="loading()"
[options]="items()"
optionLabel="fullName"
[optionValue]="selectOptionValue"
placeholder="انتخاب مشتری"
[formControl]="control"
[showClear]="true"
[filter]="true"
appendTo="body"
>
@if (canInsert) {
<ng-template #footer>
<div class="p-3">
<p-button
label="افزودن مشتری جدید"
fluid
severity="secondary"
text
size="small"
icon="pi pi-plus"
(onClick)="onOpenForm()"
/>
</div>
</ng-template>
}
</p-select>
<customer-form [(visible)]="isOpenFormDialog" (onSubmit)="refresh()" />
</uikit-field>
@@ -1,29 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Button } from 'primeng/button';
import { Select } from 'primeng/select';
import { ICustomerResponse } from '../../models';
import { CustomersService } from '../../services/main.service';
import { CustomerFormComponent } from '../form.component';
@Component({
selector: 'customers-select-field',
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, CustomerFormComponent],
})
export class CustomersSelectComponent extends AbstractSelectComponent<
ICustomerResponse,
IPaginatedResponse<ICustomerResponse>
> {
constructor(private service: CustomersService) {
super();
this.getData();
}
getDataService() {
return this.service.getAll();
}
}
@@ -1,6 +0,0 @@
const baseUrl = '/api/v1/customers';
export const CUSTOMERS_API_ROUTES = {
list: () => `${baseUrl}`,
single: (customerId: string) => `${baseUrl}/${customerId}`,
};
@@ -1,2 +0,0 @@
export * from './apiRoutes';
export * from './routes';
@@ -1,25 +0,0 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TCustomersRouteNames = 'customers' | 'customer';
export const customersNamedRoutes: NamedRoutes<TCustomersRouteNames> = {
customers: {
path: 'customers',
loadComponent: () => import('../../views/list.component').then((m) => m.CustomersComponent),
meta: {
title: 'مشتریان',
pagePath: () => '/customers',
},
},
customer: {
path: 'customers/:customerId',
loadComponent: () => import('../../views/single.component').then((m) => m.CustomerComponent),
meta: {
title: 'مشتری',
pagePath: () => '/customers/:customerId',
},
},
};
export const CUSTOMERS_ROUTES: Routes = Object.values(customersNamedRoutes);
@@ -1 +0,0 @@
export * from './io';
-29
View File
@@ -1,29 +0,0 @@
export interface ICustomerRawResponse {
id: number;
firstName: string;
lastName: string;
email: string;
mobileNumber: string;
address: string;
city: string;
state: string;
country: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
deletedAt: Date;
}
export interface ICustomerResponse extends ICustomerRawResponse {}
export interface ICustomerRequest {
firstName: string;
lastName: string;
email: string;
mobileNumber: string;
address: string;
city: string;
state: string;
country: string;
isActive: boolean;
}
@@ -1,37 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { getFullName } from '@/utils';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { CUSTOMERS_API_ROUTES } from '../constants';
import { ICustomerRawResponse, ICustomerRequest, ICustomerResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class CustomersService {
constructor(private http: HttpClient) {}
private apiRoutes = CUSTOMERS_API_ROUTES;
getAll(): Observable<IPaginatedResponse<ICustomerResponse>> {
return this.http.get<IPaginatedResponse<ICustomerRawResponse>>(this.apiRoutes.list()).pipe(
map((res) => {
return {
meta: res.meta,
data: res.data.map((item) => ({ ...item, fullName: getFullName(item) })),
};
}),
);
}
getSingle(customerId: string): Observable<ICustomerResponse> {
return this.http.get<ICustomerRawResponse>(this.apiRoutes.single(customerId)).pipe(
map((res) => {
return { ...res, fullName: getFullName(res) };
}),
);
}
create(data: ICustomerRequest): Observable<ICustomerResponse> {
return this.http.post<ICustomerRawResponse>(this.apiRoutes.list(), data);
}
}
-2
View File
@@ -1,2 +0,0 @@
export * from './list.component';
export * from './single.component';
@@ -1,14 +0,0 @@
<app-page-data-list
[pageTitle]="'مدیریت مشتریان'"
[addNewCtaLabel]="'افزودن مشتری جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="مشتری‌ای یافت نشد"
emptyPlaceholderDescription="برای افزودن مشتری جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
(onAdd)="openAddForm()"
/>
<customer-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
@@ -1,55 +0,0 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { CustomerFormComponent } from '../components/form.component';
import { ICustomerResponse } from '../models';
import { CustomersService } from '../services/main.service';
@Component({
selector: 'app-customers',
templateUrl: './list.component.html',
imports: [PageDataListComponent, CustomerFormComponent],
})
export class CustomersComponent {
constructor(private customerService: CustomersService) {
this.getData();
}
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'firstName', header: 'نام' },
{ field: 'lastName', header: 'نام خانوادگی' },
{ field: 'email', header: 'ایمیل' },
{ field: 'mobileNumber', header: 'شماره موبایل' },
{ field: 'address', header: 'آدرس' },
{ field: 'city', header: 'شهر' },
{ field: 'state', header: 'استان' },
{ field: 'country', header: 'کشور' },
{ field: 'isActive', header: 'فعال' },
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' },
,
] as IColumn[];
loading = signal(false);
items = signal<ICustomerResponse[]>([]);
visibleForm = signal(false);
refresh() {
this.getData();
}
getData() {
this.loading.set(true);
this.customerService.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res.data);
});
}
openAddForm() {
this.visibleForm.set(true);
}
}
@@ -1 +0,0 @@
<div class=""></div>
@@ -1,9 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-customer',
templateUrl: './single.component.html',
})
export class CustomerComponent {
constructor() {}
}
@@ -1,10 +0,0 @@
<p-dialog
header="فرم حساب بانکی"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '600px', zIndex: '100000' }"
appendTo="body"
[closable]="true"
>
<bank-account-form-template [initialValues]="initialValues" (onSubmitForm)="submit($event)" (onCancel)="close()" />
</p-dialog>
@@ -1,57 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { BankAccountFormTemplateComponent } from '@/modules/bankAccounts/components/form-template.component';
import { IBankAccountsResponse } from '@/modules/bankAccounts/models';
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IInventoryBankAccountRequest } from '../../models/bankAccounts.io';
import { InventoryBankAccountsService } from '../../services';
@Component({
selector: 'inventory-create-bank-account-form',
templateUrl: './create-bank-account-form.component.html',
imports: [ReactiveFormsModule, Dialog, BankAccountFormTemplateComponent],
})
export class InventoryCreateBankAccountFormComponent {
@Input() inventoryId!: number;
@Input() initialValues?: IBankAccountsResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IBankAccountsResponse>();
constructor(
private service: InventoryBankAccountsService,
private toastService: ToastService,
) {}
submitLoading = signal(false);
submit(payload: IInventoryBankAccountRequest) {
this.submitLoading.set(true);
this.service.create(this.inventoryId, payload).subscribe({
next: (res) => {
this.toastService.success({
text: `حساب بانکی ${payload.name} با موفقیت ایجاد شد`,
});
this.close();
this.onSubmit.emit();
},
error: () => {
this.submitLoading.set(false);
},
});
}
close() {
this.visibleChange.emit(false);
}
}
@@ -1,6 +0,0 @@
<p-dialog header="الصاق حساب بانکی" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<bank-accounts-select-field [control]="form.controls.bankAccountId" [showClear]="false" />
<app-form-footer-actions [submitLabel]="'الصاق'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -1,37 +0,0 @@
import { BankAccountsSelectComponent } from '@/modules/bankAccounts/components/select/select.component';
import { AbstractFormDialog } from '@/shared/components/abstract-form-dialog.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Dialog } from 'primeng/dialog';
import {
IInventoryAssignBankAccountRequest,
IInventoryBankAccountResponse,
} from '../../models/bankAccounts.io';
import { InventoryBankAccountsService } from '../../services';
@Component({
selector: 'app-inventory-bank-account-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, BankAccountsSelectComponent, FormFooterActionsComponent],
})
export class InventoryBankAccountFormComponent extends AbstractFormDialog<
IInventoryBankAccountResponse,
IInventoryAssignBankAccountRequest
> {
private readonly route = inject(ActivatedRoute);
private readonly service = inject(InventoryBankAccountsService);
form = this.fb.group({
bankAccountId: this.fb.control<number>(0, {
nonNullable: true,
validators: [Validators.required],
}),
});
submitForm(payload: IInventoryAssignBankAccountRequest) {
const inventoryId = this.route.snapshot.paramMap.get('inventoryId')!;
return this.service.assign(Number(inventoryId), payload);
}
}
@@ -1,33 +0,0 @@
<p-card class="">
<ng-template #header>
<div class="p-4 flex items-center justify-between">
<span class="text-xl font-bold">حساب‌های بانکی متصل به انبار</span>
<div class="flex gap-2">
<button pButton outlined (click)="openAddForm()">الصاق حساب بانکی جدید</button>
<button pButton outlined (click)="openCreateForm()">ایجاد و الصاق حساب بانکی</button>
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getItems()"></button>
</div>
</div>
</ng-template>
<app-page-data-list
[columns]="columns"
emptyPlaceholderTitle="حساب بانکی‌ای به این انبار متصل نشده است"
emptyPlaceholderDescription="برای الصاق حساب بانکی، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
(onAdd)="openAddForm()"
>
<ng-template #actions let-item>
<button pButton icon="pi pi-trash" severity="danger" (click)="unassignBankAccount($event, item)"></button>
</ng-template>
</app-page-data-list>
<app-inventory-bank-account-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
<inventory-create-bank-account-form
[(visible)]="visibleBankAccountForm"
[inventoryId]="inventoryId"
(onSubmit)="refresh()"
/>
<p-confirmdialog />
</p-card>
<!-- <inventory-form [(visible)]="visibleForm" (onSubmit)="refresh()" /> -->
@@ -1,150 +0,0 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, inject, signal, TemplateRef, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { ConfirmDialog } from 'primeng/confirmdialog';
import { IInventoryBankAccountResponse } from '../../models/bankAccounts.io';
import { InventoryBankAccountsService } from '../../services';
import { InventoryCreateBankAccountFormComponent } from './create-bank-account-form.component';
import { InventoryBankAccountFormComponent } from './form.component';
@Component({
selector: 'app-inventory-bank-accounts-list',
templateUrl: './list.component.html',
imports: [
PageDataListComponent,
Card,
ButtonDirective,
ConfirmDialog,
InventoryBankAccountFormComponent,
InventoryCreateBankAccountFormComponent,
],
providers: [ConfirmationService],
})
export class InventoryBankAccountsListComponent {
private confirmationService = inject(ConfirmationService);
private messageService = inject(MessageService);
private readonly route = inject(ActivatedRoute);
private readonly service = inject(InventoryBankAccountsService);
@ViewChild('actions', { static: true }) actionsTpl!: TemplateRef<any>;
items = signal<IInventoryBankAccountResponse[]>([]);
loading = signal<boolean>(false);
visibleForm = signal<boolean>(false);
visibleBankAccountForm = signal<boolean>(false);
inventoryId = this.route.snapshot.params['inventoryId'];
columns = [] as IColumn<IInventoryBankAccountResponse>[];
ngOnInit() {
this.getItems();
this.columns = [
{
field: 'name',
header: 'عنوان حساب',
customDataModel(item) {
return item.name;
},
},
{
field: 'bankName',
header: 'نام بانک',
customDataModel(item) {
return `${item.branch.bank.name} - شعبه‌ی ${item.name}`;
},
},
{
field: 'accountNumber',
header: 'شماره حساب',
customDataModel(item) {
return item.accountNumber;
},
},
{
field: 'relatedPOSes',
header: 'نقاط فروش متصل',
customDataModel(item) {
return item.posAccounts.length || '0';
},
},
{
field: 'actions',
header: '',
customDataModel: this.actionsTpl,
width: '100px',
},
];
}
getItems() {
this.loading.set(true);
this.service.getBankAccounts(this.inventoryId).subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
refresh() {
this.getItems();
}
openAddForm() {
this.visibleForm.set(true);
}
openCreateForm() {
this.visibleBankAccountForm.set(true);
}
showConfirmation(event: MouseEvent, item: IInventoryBankAccountResponse) {
this.confirmationService.confirm({
target: event.target as EventTarget,
message: 'از حذف این حساب بانکی اطمینان دارید؟',
header: 'تأییدیه',
closable: true,
closeOnEscape: true,
icon: 'pi pi-exclamation-triangle',
rejectButtonProps: {
label: 'لغو',
severity: 'secondary',
outlined: true,
},
acceptButtonProps: {
label: 'تایید',
},
accept: () => {
this.doUnassignBankAccount(item);
},
});
}
unassignBankAccount(event: MouseEvent, item: IInventoryBankAccountResponse) {
this.showConfirmation(event, item);
}
doUnassignBankAccount(item: IInventoryBankAccountResponse) {
this.loading.set(true);
this.service.unassign(this.inventoryId, { bankAccountId: item.id }).subscribe({
next: () => {
this.messageService.add({
severity: 'success',
summary: 'موفق',
detail: 'حساب بانکی با موفقیت حذف شد',
});
this.loading.set(false);
this.getItems();
},
error: () => {
this.loading.set(false);
},
});
}
}
@@ -1,30 +0,0 @@
<uikit-field label="حساب بانکی" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[loading]="loading()"
[options]="items()"
optionLabel="name"
[optionValue]="selectOptionValue"
placeholder="انتخاب حساب بانک"
[formControl]="control"
[showClear]="showClear"
[filter]="true"
appendTo="body"
>
@if (canInsert) {
<ng-template #footer>
<div class="p-3">
<p-button
label="افزودن حساب جدید"
fluid
severity="secondary"
text
size="small"
icon="pi pi-plus"
(onClick)="onOpenForm()"
/>
</div>
</ng-template>
}
</p-select>
<app-inventory-bank-account-form [(visible)]="isOpenFormDialog" (onSubmit)="refresh()" />
</uikit-field>
@@ -1,42 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component, Input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Button } from 'primeng/button';
import { Select } from 'primeng/select';
import { IInventoryBankAccountResponse } from '../../models';
import { InventoryBankAccountsService } from '../../services';
import { InventoryBankAccountFormComponent } from './form.component';
@Component({
selector: 'app-inventory-bank-account-select',
templateUrl: './select.component.html',
imports: [
UikitFieldComponent,
Select,
Button,
InventoryBankAccountFormComponent,
ReactiveFormsModule,
],
})
export class InventoryBankAccountSelectComponent extends AbstractSelectComponent<
IInventoryBankAccountResponse,
IPaginatedResponse<IInventoryBankAccountResponse>
> {
@Input() inventoryId!: string;
@Input() override showClear: boolean = false;
constructor(private service: InventoryBankAccountsService) {
super();
}
ngOnInit() {
console.log(this.inventoryId);
this.getData();
}
getDataService() {
return this.service.getBankAccounts(Number(this.inventoryId));
}
}
@@ -1,8 +0,0 @@
<p-dialog header="فرم انبار" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.name" name="name" />
<app-input label="موقعیت" [control]="form.controls.location" name="location" />
<app-input label="وضعیت" [control]="form.controls.isActive" name="isActive" type="switch" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -1,76 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IInventoryRequest, IInventorySummaryResponse } from '../models';
import { InventoriesService } from '../services/main.service';
@Component({
selector: 'inventory-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
})
export class InventoryFormComponent {
@Input() initialValues?: IInventorySummaryResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IInventorySummaryResponse>();
private fb = inject(FormBuilder);
constructor(
private service: InventoriesService,
private toastService: ToastService,
) {
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset();
});
}
form = this.fb.group({
name: [this.initialValues?.name || null, [Validators.required]],
location: [this.initialValues?.location || null, [Validators.required]],
isActive: [this.initialValues?.isActive || false, [Validators.required]],
});
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.service.create(this.form.value as IInventoryRequest).subscribe({
next: (res) => {
this.toastService.success({
text: `انبار ${this.form.value.name} با موفقیت ایجاد شد`,
});
this.close();
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -1,4 +0,0 @@
export * from './form.component';
export * from './movementList/movement-list.component';
export * from './select/select.component';
export * from './transfer/transfer-form.component';
@@ -1,117 +0,0 @@
<p-card>
<ng-template pTemplate="header">
<div class="p-4 flex items-center justify-between">
<span class="text-xl font-bold">{{ title }}</span>
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getAll()"></button>
</div>
</ng-template>
<ng-template pTemplate="content">
<p-table [value]="movements()" [loading]="loading()" dataKey="receiptId" [expandedRowKeys]="expandedRows">
<ng-template pTemplate="header">
<tr>
<th [style]="{ width: '5rem' }"></th>
<th>شناسه رسید</th>
<th>نوع گردش</th>
<th>انواع کالاها</th>
<th>تعداد کالاها</th>
<th>مجموع قیمت</th>
<th>تاریخ</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-movement let-expanded="expanded">
<tr>
<td>
<p-button
type="button"
pRipple
[pRowToggler]="movement"
text
severity="secondary"
rounded
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-left'"
/>
</td>
<td>{{ movement.receiptId }}</td>
<td>
<catalog-movement-reference-tag
[type]="movement.info.referenceType"
[movementType]="movement.info.type"
[counterName]="movement.info.counterInventory?.name"
/>
</td>
<td>{{ movement.count }}</td>
<td>{{ movement.info.quantity }}</td>
<td><span [appPriceMask]="movement.info.totalCost"></span></td>
<td><span [jalaliDate]="movement.info.createdAt"></span></td>
</tr>
</ng-template>
<ng-template #expandedrow let-movement>
<tr>
<td colspan="7">
<p-card class="p-4 pt-0 border border-primary-700">
<p-table [value]="movement.movements" dataKey="receiptId">
<ng-template #caption>
<h5 class="mb-4">کالاهای جابجا شده در این گردش</h5>
</ng-template>
<ng-template #header>
<tr>
<th pSortableColumn="product.id">
<div class="flex items-center gap-2">
شناسه کالا
<p-sortIcon field="product.id" />
</div>
</th>
<th pSortableColumn="product.name">
<div class="flex items-center gap-2">
عنوان کالا
<p-sortIcon field="product.name" />
</div>
</th>
<th pSortableColumn="quantity">
<div class="flex items-center gap-2">
تعداد
<p-sortIcon field="quantity" />
</div>
</th>
<th pSortableColumn="unitPrice">
<div class="flex items-center gap-2">
قیمت واحد
<p-sortIcon field="unitPrice" />
</div>
</th>
<th pSortableColumn="totalCost">
<div class="flex items-center gap-2">
قیمت نهایی
<p-sortIcon field="totalCost" />
</div>
</th>
<th style="width: 4rem"></th>
</tr>
</ng-template>
<ng-template #body let-movement>
<tr>
<td>{{ movement.product.id }}</td>
<td>{{ movement.product.name }}</td>
<td>{{ movement.quantity }}</td>
<td><span [appPriceMask]="movement.unitPrice"></span></td>
<td><span [appPriceMask]="movement.totalCost"></span></td>
<td>
<p-button type="button" icon="pi pi-search" [routerLink]="'/products/' + movement.product.id" />
</td>
</tr>
</ng-template>
</p-table>
</p-card>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="7" class="text-center! p-10!">هیچ گردش موجودی یافت نشد.</td>
</tr>
</ng-template>
</p-table>
</ng-template>
</p-card>
@@ -1,68 +0,0 @@
import { MovementType } from '@/shared/catalog';
import { CatalogMovementReferenceTagComponent } from '@/shared/catalog/movementReferenceTypes';
import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives';
import { Component, Input, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Button, ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { Ripple } from 'primeng/ripple';
import { TableModule } from 'primeng/table';
import { IInventoryStockMovementResponse } from '../../models';
import { InventoriesService } from '../../services/main.service';
@Component({
selector: 'inventory-movement-list',
templateUrl: './movement-list.component.html',
imports: [
Card,
TableModule,
ButtonDirective,
Ripple,
Button,
RouterLink,
CatalogMovementReferenceTagComponent,
PriceMaskDirective,
JalaliDateDirective,
],
})
export class InventoryMovementListComponent {
@Input() inventoryId!: string;
@Input() movementType?: MovementType;
loading = signal(false);
movements = signal<IInventoryStockMovementResponse[]>([]);
expandedRows = {};
constructor(private service: InventoriesService) {}
get title() {
switch (this.movementType) {
case 'IN':
return 'گردش ورودی کالاها';
case 'OUT':
return 'گردش خروجی کالاها';
case 'ADJUST':
return 'گردش تعدیل کالاها';
default:
return 'تمام گردش کالاها';
}
}
ngOnInit() {
this.getAll();
}
getAll() {
this.loading.set(true);
return this.service.getMovements(this.inventoryId, this.movementType).subscribe({
next: (res) => {
this.movements.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
}
@@ -1,15 +0,0 @@
<p-dialog
header="فرم نقطه فروش"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '600px', zIndex: '100000' }"
appendTo="body"
[closable]="true"
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="عنوان" [control]="form.controls.name" name="name" />
<app-input label="کد" [control]="form.controls.code" name="code" />
<app-inventory-bank-account-select [control]="form.controls.bankAccountId" [inventoryId]="inventoryId" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -1,51 +0,0 @@
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 { Component, inject, Input } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IInventoryPosAccountRequest, IInventoryPosAccountResponse } from '../../models';
import { InventoryPosAccountsService } from '../../services/posAccounts.service';
import { InventoryBankAccountSelectComponent } from '../bankAccounts/select.component';
@Component({
selector: 'app-inventory-pos-account-form',
templateUrl: './form.component.html',
imports: [
Dialog,
ReactiveFormsModule,
InputComponent,
FormFooterActionsComponent,
InventoryBankAccountSelectComponent,
],
})
export class InventoryPosAccountFormComponent extends AbstractFormDialog<
IInventoryPosAccountResponse,
IInventoryPosAccountRequest
> {
@Input() inventoryId!: string;
private readonly service = inject(InventoryPosAccountsService);
form = this.fb.group({
name: this.fb.control<string>(this.initialValues?.name || '', {
nonNullable: true,
validators: [Validators.required],
}),
code: this.fb.control<string>(this.initialValues?.code || '', {
nonNullable: true,
validators: [Validators.required],
}),
description: this.fb.control<string>(this.initialValues?.description || '', {
nonNullable: true,
}),
bankAccountId: this.fb.control<number>(this.initialValues?.bankAccount?.id || 0, {
nonNullable: true,
validators: [Validators.required],
}),
});
submitForm(payload: IInventoryPosAccountRequest) {
return this.service.create(Number(this.inventoryId), payload);
}
}
@@ -1,14 +0,0 @@
<uikit-field label="انبار" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[options]="items()"
optionLabel="name"
[optionValue]="selectOptionValue"
placeholder="انتخاب انبار"
[formControl]="control"
[showClear]="showClear"
[filter]="true"
appendTo="body"
(onChange)="change($event.value)"
>
</p-select>
</uikit-field>
@@ -1,27 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { IInventorySummaryResponse } from '../../models';
import { InventoriesService } from '../../services/main.service';
@Component({
selector: 'inventories-select-field',
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
})
export class InventoriesSelectComponent extends AbstractSelectComponent<
IInventorySummaryResponse,
IPaginatedResponse<IInventorySummaryResponse>
> {
constructor(private service: InventoriesService) {
super();
this.getData();
}
getDataService() {
return this.service.getAll();
}
}
@@ -1,142 +0,0 @@
<p-card class="">
<ng-template #title> انتقال موجودی بین انبارها </ng-template>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4 mt-4">
<div class="flex flex-col gap-4">
<div class="max-w-xs">
<app-input [control]="form.controls.code" label="شماره انتقال" />
</div>
<uikit-field [control]="form.controls.description" label="توضیحات">
<textarea
pTextarea
rows="4"
cols="30"
[formControl]="form.controls.description"
placeholder="توضیحات"
></textarea>
</uikit-field>
<div class="flex items-stretch gap-4">
<p-card class="flex-1 border border-primary-300">
<ng-template #title> انبار مبدا </ng-template>
<div class="mt-5">
<p-select
[options]="inventories()"
optionLabel="name"
[loading]="inventoriesLoading()"
[formControl]="form.controls.fromInventory"
placeholder="انتخاب انبار مبدا"
class="w-full"
></p-select>
<p-card class="mt-10 border border-primary-700">
<ng-template #title> کالاهای موجود در انبار مبدا </ng-template>
<div class="mt-5">
@if (!form.controls.fromInventory.value) {
<div class="text-center text-gray-500 py-10">لطفا انبار مبدا را انتخاب کنید</div>
} @else {
<p-table
[value]="originInventoryStock()"
[loading]="originInventoryStockLoading()"
selectionMode="multiple"
[(selection)]="selectedProducts"
(onRowSelect)="setSelectedProductsInForm($event)"
(onRowUnselect)="clearSelectedProductsInForm($event)"
dataKey="id"
class="w-full"
>
<ng-template #header>
<tr>
<th pSelectableRowCheckbox></th>
<th>نام کالا</th>
<th>موجودی</th>
<th>میانگین قیمت</th>
</tr>
</ng-template>
<ng-template let-item pTemplate="body">
<tr>
<td>
<p-tableCheckbox [disabled]="item.quantity === 0" [value]="item"></p-tableCheckbox>
</td>
<td>{{ item.product.name }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.avgCost }}</td>
</tr>
</ng-template>
</p-table>
}
</div>
</p-card>
</div>
</p-card>
<div class="shrink-0">
<div class="flex items-center h-100">
<i class="pi pi-arrow-left text-2xl"></i>
</div>
</div>
<p-card class="flex-1 border border-primary-300">
<ng-template #title> انبار مقصد </ng-template>
<div class="mt-5">
<p-select
[options]="toInventories"
optionLabel="name"
[loading]="inventoriesLoading()"
[formControl]="form.controls.toInventory"
placeholder="انتخاب انبار مقصد"
class="w-full"
></p-select>
<p-card class="mt-10 border border-primary-700">
<ng-template #title> کالاهای انتخاب شده </ng-template>
<div class="mt-5">
@if (!form.controls.fromInventory.value) {
<div class="text-center text-gray-500 py-10">لطفا انبار مبدا را انتخاب کنید</div>
} @else {
<p-table [value]="form.controls.items.controls" class="w-full">
<ng-template #header>
<tr>
<th>نام کالا</th>
<th>مقدار برای انتقال</th>
</tr>
</ng-template>
<ng-template let-item let-rowIndex="rowIndex" pTemplate="body">
<tr>
<td>{{ item.controls.product.value?.name }}</td>
<td>
<app-uikit-counter [min]="1" [max]="10" [control]="item.controls.count" />
</td>
</tr>
</ng-template>
</p-table>
}
</div>
</p-card>
</div>
</p-card>
</div>
<div class="flex items-center gap-2">
<button
pButton
label="انصراف"
type="button"
outlined
icon="pi pi-times"
routerLink="/products"
[disabled]="form.disabled || submitLoading()"
></button>
<button
pButton
label="ذخیره"
type="submit"
icon="pi pi-check"
[loading]="submitLoading()"
(click)="submit()"
></button>
</div>
</div>
</form>
</p-card>
@@ -1,151 +0,0 @@
import { Maybe } from '@/core';
import { InputComponent } from '@/shared/components';
import { UikitCounterComponent, UikitFieldComponent } from '@/uikit';
import { Component, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { Select } from 'primeng/select';
import { TableModule, TableRowSelectEvent, TableRowUnSelectEvent } from 'primeng/table';
import { Textarea } from 'primeng/textarea';
import {
IInventoryProduct,
IInventoryStockResponse,
IInventorySummaryResponse,
IInventoryTransferRequest,
} from '../../models';
import { InventoriesService } from '../../services/main.service';
@Component({
selector: 'inventories-transfer-form',
templateUrl: './transfer-form.component.html',
imports: [
ReactiveFormsModule,
Card,
InputComponent,
Textarea,
UikitFieldComponent,
Select,
TableModule,
ButtonDirective,
UikitCounterComponent,
],
})
export class InventoriesTransferFormComponent {
fb = inject(FormBuilder);
form = this.fb.group({
fromInventory: [null as Maybe<IInventorySummaryResponse>, [Validators.required]],
toInventory: [null as Maybe<IInventorySummaryResponse>, [Validators.required]],
code: ['', [Validators.required]],
description: [''],
items: this.fb.array(
[
this.fb.group({
product: [null as Maybe<IInventoryProduct>, [Validators.required]],
count: [1, [Validators.required, Validators.min(1)]],
}),
],
Validators.required,
),
});
inventoriesLoading = signal<boolean>(false);
inventories = signal<Maybe<IInventorySummaryResponse>[]>([]);
get toInventories() {
const fromInventoryId = this.form.controls.fromInventory.value;
return this.inventories().filter((inv) => inv?.id !== fromInventoryId?.id);
}
originInventoryStock = signal<any[]>([]);
originInventoryStockLoading = signal(false);
selectedProducts = signal<Maybe<IInventoryStockResponse>[]>([]);
constructor(private service: InventoriesService) {
this.form.controls.toInventory.disable();
this.form.controls.fromInventory.valueChanges.subscribe((val) => {
if (val && val.id) {
this.form.controls.toInventory.enable();
this.getSelectedInventoryStock();
}
});
this.getInventories();
}
setSelectedProductsInForm($e: TableRowSelectEvent<IInventoryStockResponse>) {
const stock = $e.data as Maybe<IInventoryStockResponse>;
if (stock && stock.product) {
if (
this.form.controls.items.length === 1 &&
!this.form.controls.items.value?.at(0)?.product
) {
this.form.controls.items.removeAt(0);
}
this.form.controls.items.push(
// @ts-ignore
this.fb.group({
product: [stock.product, [Validators.required]],
count: [1, [Validators.required, Validators.min(1)]],
}),
);
}
}
clearSelectedProductsInForm($e: TableRowUnSelectEvent<IInventoryProduct>) {
if ($e.index != null) {
this.form.controls.items.removeAt($e.index);
}
}
getInventories() {
this.inventoriesLoading.set(true);
this.service.getAll().subscribe({
next: (res) => {
this.inventories.set(res.data);
this.inventoriesLoading.set(false);
},
error: () => {
this.inventoriesLoading.set(false);
},
});
}
getSelectedInventoryStock() {
this.originInventoryStockLoading.set(true);
this.service.getStock(String(this.form.controls.fromInventory.value?.id)).subscribe({
next: (res) => {
this.originInventoryStock.set(res.data);
this.originInventoryStockLoading.set(false);
},
error: () => {
this.originInventoryStockLoading.set(false);
},
});
}
submitLoading = signal(false);
submit() {
this.submitLoading.set(true);
const payload = {
fromInventoryId: this.form.controls.fromInventory.value?.id,
toInventoryId: this.form.controls.toInventory.value?.id,
code: this.form.controls.code.value || '',
description: this.form.controls.description.value || '',
items: this.form.controls.items.value.map((item) => ({
productId: item.product?.id,
count: item.count,
})),
} as IInventoryTransferRequest;
this.service.transfer(payload).subscribe({
next: () => {
this.submitLoading.set(false);
// this.form.reset();
},
error: () => {
this.submitLoading.set(false);
},
});
}
}
@@ -1,18 +0,0 @@
import { INVENTORY_POS_API_ROUTES } from './pos';
const baseUrl = '/api/v1/inventories';
export const INVENTORIES_API_ROUTES = {
list: () => `${baseUrl}`,
single: (inventoryId: string) => `${baseUrl}/${inventoryId}`,
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`,
assignBankAccounts: (inventoryId: number) => `${baseUrl}/${inventoryId}/bank-accounts/assign`,
unassignBankAccounts: (inventoryId: number) => `${baseUrl}/${inventoryId}/bank-accounts/unassign`,
pos: INVENTORY_POS_API_ROUTES(baseUrl),
};
@@ -1,8 +0,0 @@
export const INVENTORY_POS_API_ROUTES = (_baseUrl: string) => {
const baseUrl = (inventoryId: string) => `${_baseUrl}/${inventoryId}/pos-accounts`;
return {
list: (inventoryId: string) => `${baseUrl(inventoryId)}`,
single: (inventoryId: string, posAccountId: string) =>
`${baseUrl(inventoryId)}/${posAccountId}`,
};
};
@@ -1,2 +0,0 @@
export * from './apiRoutes';
export * from './routes';
@@ -1,82 +0,0 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export const inventoriesNamedRoutes: NamedRoutes<any> = {
inventories: {
path: 'inventories',
loadComponent: () => import('../../views/list.component').then((m) => m.InventoriesComponent),
meta: {
title: 'انبارها',
pagePath: () => '/inventories',
},
},
transfer: {
path: 'inventories/transfer',
loadComponent: () =>
import('../../views/transfer.component').then((m) => m.InventoriesTransferComponent),
meta: {
title: 'انتقال بین انبارها',
pagePath: () => '/inventories/transfer',
},
},
inventory: {
path: 'inventories/:inventoryId',
loadComponent: () => import('../../views/single.component').then((m) => m.InventoryComponent),
meta: {
title: 'انبار',
pagePath: () => '/inventories/:inventoryId',
},
},
products: {
path: 'inventories/:inventoryId/products',
loadComponent: () =>
import('../../views/products.component').then((m) => m.InventoryProductsComponent),
meta: {
title: 'محصولات انبار',
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: 'کاردکس انبار محصول',
pagePath: () => '/inventories/:inventoryId/products/:productId/cardex',
},
},
posAccounts: {
path: 'inventories/:inventoryId/pos-accounts',
loadComponent: () =>
import('../../views/posAccounts/list.component').then(
(m) => m.InventoryPosAccountListComponent,
),
meta: {
title: 'نقاط فروش متصل به انبار',
pagePath: () => '/inventories/:inventoryId/pos-accounts',
},
},
purchase: {
path: 'inventories/:inventoryId/purchase',
loadComponent: () =>
import('../../views/purchase.component').then((m) => m.InventoryPurchaseComponent),
meta: {
title: 'ثبت خرید در انبار',
pagePath: () => '/inventories/:inventoryId/purchase',
},
},
} as const;
export type TInventoriesRouteNames = keyof typeof inventoriesNamedRoutes;
export const INVENTORIES_ROUTES: Routes = Object.values(inventoriesNamedRoutes);
@@ -1,44 +0,0 @@
import { IBankAccountsRequest } from '@/modules/bankAccounts/models';
import { IBankAccountBranch, IInventoryPosAccountItemSummary } from './types';
export interface IInventoryBankAccountRawResponse {
id: number;
accountNumber: string;
cardNumber: null;
name: string;
iban: string;
branchId: number;
createdAt: string;
updatedAt: string;
deletedAt: null;
branch: IBankAccountBranch;
posAccounts: IInventoryPosAccountItemSummary[];
}
export interface IInventoryBankAccountResponse extends IInventoryBankAccountRawResponse {}
export interface IInventoryBankAccountRequest extends IBankAccountsRequest {}
export interface IInventoryAssignBankAccountRequest {
bankAccountId: number;
}
export interface IBankAccountSummary {
branch: Branch;
accountNumber: string;
cardNumber: string;
name: string;
id: number;
iban: string;
}
interface Branch {
id: number;
name: string;
bank: Bank;
}
interface Bank {
id: number;
name: string;
shortName: string;
}
@@ -1,4 +0,0 @@
export * from './bankAccounts.io';
export * from './io';
export * from './posAccounts.io';
export * from './types';
-67
View File
@@ -1,67 +0,0 @@
import { ICardexResponse } from '@/modules/cardex/models';
import {
IInventoryInfo,
IInventoryMovement,
IInventoryStockProduct,
IInventoryTransferRequestItem,
} from './types';
export interface IInventorySummaryRawResponse {
id: number;
name: string;
location: string;
isActive: boolean;
isPointOfSale: boolean;
createdAt: string;
updatedAt: string;
deletedAt: Maybe<string>;
}
export interface IInventorySummaryResponse extends IInventorySummaryRawResponse {}
export interface IInventoryDetailRawResponse extends IInventorySummaryRawResponse {
availableProductTypes: number;
availableProductCount: number;
availableProductsCost: number;
}
export interface IInventoryDetailResponse extends IInventoryDetailRawResponse {}
export interface IInventoryRequest {
name: string;
location: string;
isActive: boolean;
}
export interface IInventoryStockMovementRawResponse {
receiptId: string;
count: number;
info: IInventoryInfo;
movements: IInventoryMovement[];
}
export interface IInventoryStockMovementResponse extends IInventoryStockMovementRawResponse {}
export interface IInventoryTransferRequest {
code: string;
description: string;
fromInventoryId: number;
toInventoryId: number;
items: IInventoryTransferRequestItem[];
}
export interface IInventoryStockRawResponse {
quantity: number;
avgCost: number;
product: IInventoryStockProduct;
}
export interface IInventoryStockResponse extends IInventoryStockRawResponse {}
export interface IInventoryStockQuery {
isAvailable?: boolean;
}
export interface IInventoryProductCardexRawResponse extends ICardexResponse {}
export interface IInventoryProductCardexResponse extends IInventoryProductCardexRawResponse {}
@@ -1,24 +0,0 @@
import ISummary from '@/core/models/summary';
import { IInventoryBankAccountSummary } from './types';
export interface IInventoryPosAccountRawResponse {
id: number;
name: string;
code: string;
description: string;
bankAccountId: number;
inventory: ISummary;
createdAt: string;
updatedAt: string;
deletedAt: null;
bankAccount: IInventoryBankAccountSummary;
}
export interface IInventoryPosAccountResponse extends IInventoryPosAccountRawResponse {}
export interface IInventoryPosAccountRequest {
name: string;
code: string;
description: string;
bankAccountId: number;
}
@@ -1,84 +0,0 @@
import { IProductRawResponse } from '@/modules/products/models';
export interface IInventoryMovement {
id: number;
quantity: number;
unitPrice: number;
totalCost: number;
avgCost: number;
product: IInventoryProduct;
remainedInStock: number;
}
export interface IInventoryProduct {
id: number;
name: string;
description: null;
sku: string;
barcode: string;
createdAt: string;
updatedAt: string;
deletedAt: null;
brandId: number;
categoryId: number;
}
export interface IInventoryInfo {
date: string;
type: string;
quantity: number;
unitPrice: number;
totalCost: number;
referenceType: string;
referenceId: string;
createdAt: string;
}
export interface IInventoryTransferRequestItem {
productId: number;
count: number;
}
export interface IInventoryStockProduct extends IProductRawResponse {}
export interface IBankAccountBranch {
id: number;
name: string;
code: string;
address: null;
createdAt: string;
updatedAt: string;
deletedAt: null;
bankId: number;
bank: IBankAccountBranchBank;
}
interface IBankAccountBranchBank {
id: number;
name: string;
shortName: string;
}
export interface IInventoryPosAccountItemSummary {
id: string;
name: string;
code: string;
}
export interface IInventoryBankAccountSummary {
id: number;
name: string;
accountNumber: string;
branch: IInventoryBankAccountBranch;
}
interface IInventoryBankAccountBranch {
id: number;
name: string;
bank: IInventoryBankAccountBranchBank;
}
interface IInventoryBankAccountBranchBank {
id: number;
name: string;
}
@@ -1,55 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { INVENTORIES_API_ROUTES } from '../constants';
import {
IInventoryAssignBankAccountRequest,
IInventoryBankAccountRawResponse,
IInventoryBankAccountRequest,
IInventoryBankAccountResponse,
} from '../models/bankAccounts.io';
@Injectable({ providedIn: 'root' })
export class InventoryBankAccountsService {
constructor(private http: HttpClient) {}
private apiRoutes = INVENTORIES_API_ROUTES;
getBankAccounts(
inventoryId: number,
): Observable<IPaginatedResponse<IInventoryBankAccountResponse>> {
return this.http.get<IPaginatedResponse<IInventoryBankAccountRawResponse>>(
`${this.apiRoutes.bankAccounts(inventoryId)}`,
);
}
create(
inventoryId: number,
payload: IInventoryBankAccountRequest,
): Observable<IInventoryBankAccountResponse> {
return this.http.post<IInventoryBankAccountResponse>(
`${this.apiRoutes.bankAccounts(inventoryId)}`,
payload,
);
}
assign(
inventoryId: number,
payload: IInventoryAssignBankAccountRequest,
): Observable<IInventoryBankAccountResponse> {
return this.http.post<IInventoryBankAccountResponse>(
`${this.apiRoutes.assignBankAccounts(inventoryId)}`,
payload,
);
}
unassign(
inventoryId: number,
payload: IInventoryAssignBankAccountRequest,
): Observable<IInventoryBankAccountResponse> {
return this.http.post<IInventoryBankAccountResponse>(
`${this.apiRoutes.unassignBankAccounts(inventoryId)}`,
payload,
);
}
}
@@ -1,2 +0,0 @@
export * from './bankAccounts.service';
export * from './main.service';
@@ -1,80 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { MovementType } from '@/shared/catalog';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { INVENTORIES_API_ROUTES } from '../constants';
import {
IInventoryDetailRawResponse,
IInventoryDetailResponse,
IInventoryProductCardexRawResponse,
IInventoryProductCardexResponse,
IInventoryRequest,
IInventoryStockMovementRawResponse,
IInventoryStockMovementResponse,
IInventoryStockQuery,
IInventoryStockRawResponse,
IInventoryStockResponse,
IInventorySummaryRawResponse,
IInventorySummaryResponse,
IInventoryTransferRequest,
} from '../models';
@Injectable({ providedIn: 'root' })
export class InventoriesService {
constructor(private http: HttpClient) {}
private apiRoutes = INVENTORIES_API_ROUTES;
getAll(): Observable<IPaginatedResponse<IInventorySummaryResponse>> {
return this.http.get<IPaginatedResponse<IInventorySummaryRawResponse>>(this.apiRoutes.list());
}
getSingle(inventoryId: string): Observable<IInventoryDetailResponse> {
return this.http.get<IInventoryDetailRawResponse>(this.apiRoutes.single(inventoryId));
}
create(data: IInventoryRequest): Observable<IInventorySummaryResponse> {
return this.http.post<IInventorySummaryRawResponse>(this.apiRoutes.list(), data);
}
getMovements(
inventoryId: string,
movementType?: MovementType,
): Observable<IPaginatedResponse<IInventoryStockMovementResponse>> {
return this.http.get<IPaginatedResponse<IInventoryStockMovementRawResponse>>(
this.apiRoutes.inventoryMovements(inventoryId),
{
params: movementType ? { type: movementType } : {},
},
);
}
transfer(payload: IInventoryTransferRequest): Observable<void> {
return this.http.post<void>(this.apiRoutes.transfer(), payload);
}
getStock(
inventoryId: string,
params?: IInventoryStockQuery,
): Observable<IPaginatedResponse<IInventoryStockResponse>> {
return this.http.get<IPaginatedResponse<IInventoryStockRawResponse>>(
this.apiRoutes.stock(inventoryId),
{ params: params ? { ...params } : {} },
);
}
getProductCardex(
inventoryId: string,
productId: string,
): Observable<IPaginatedResponse<IInventoryProductCardexResponse>> {
return this.http.get<IPaginatedResponse<IInventoryProductCardexRawResponse>>(
`${this.apiRoutes.productCardex(inventoryId, productId)}`,
);
}
getCardex(inventoryId: string): Observable<IPaginatedResponse<IInventoryProductCardexResponse>> {
return this.http.get<IPaginatedResponse<IInventoryProductCardexRawResponse>>(
`${this.apiRoutes.cardex(inventoryId)}`,
);
}
}
@@ -1,33 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { INVENTORIES_API_ROUTES } from '../constants';
import {
IInventoryPosAccountRawResponse,
IInventoryPosAccountRequest,
IInventoryPosAccountResponse,
} from '../models';
@Injectable({ providedIn: 'root' })
export class InventoryPosAccountsService {
constructor(private http: HttpClient) {}
private apiRoutes = INVENTORIES_API_ROUTES;
getList(inventoryId: number): Observable<IPaginatedResponse<IInventoryPosAccountResponse>> {
return this.http.get<IPaginatedResponse<IInventoryPosAccountRawResponse>>(
`${this.apiRoutes.pos.list(String(inventoryId))}`,
);
}
create(
inventoryId: number,
payload: IInventoryPosAccountRequest,
): Observable<IInventoryPosAccountResponse> {
return this.http.post<IInventoryPosAccountResponse>(
`${this.apiRoutes.pos.list(String(inventoryId))}`,
payload,
);
}
}
@@ -1,83 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { EntityState, EntityStore } from '@/core/state';
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IInventoryDetailResponse } from '../models';
import { InventoriesService } from '../services';
export interface InventoryState extends EntityState<IInventoryDetailResponse> {}
@Injectable({
providedIn: 'root',
})
export class InventoryStore extends EntityStore<IInventoryDetailResponse, InventoryState> {
private readonly _inventoryState = {
isRefreshing: false,
};
constructor(
private activeRoute: ActivatedRoute,
private service: InventoriesService,
private toastService: ToastService,
) {
super({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
entities: {},
selectedId: null,
ids: [],
});
this.initial();
}
supplierId!: string;
initial() {}
getSingle(inventoryId: string) {
if (this.entities()[inventoryId]) {
return;
}
this.patchState({ loading: true });
this.service.getSingle(inventoryId).subscribe({
next: (res) => {
console.log('first');
this.patchState({
entities: { [res.id]: res },
loading: false,
});
console.log(this.entities());
},
error: (err) => {
this.patchState({ loading: false, error: err });
this.toastService.error({
text: 'خطا در دریافت اطلاعات انبار',
});
},
});
}
refresh(inventoryId: string) {
const { inventoryId: _, ...entities } = this.entities();
this.patchState({
entities,
});
this.getSingle(inventoryId);
}
reset(): void {
const queryParams = this.activeRoute.snapshot.queryParams;
this.setState({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
entities: {},
selectedId: null,
ids: [],
});
}
}
@@ -1,15 +0,0 @@
@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>
}

Some files were not shown because too many files have changed in this diff Show More