diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bfbc341 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +node_modules +npm-debug.log +dist +.git +.gitignore +.angular +.env +.env.local +.env.*.local +README.md +LICENSE.md +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store +karma.conf.js +tsconfig.spec.json +src/**/*.spec.ts diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..3a8d742 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,339 @@ +# Docker Setup Guide + +This project is now containerized with Docker. Below are instructions for building and running the application in Docker. + +## Quick Start + +### Production + +```bash +docker-compose up -d app +# Visit http://localhost +``` + +### Staging + +```bash +docker-compose --profile staging up -d staging +# Visit http://localhost:8080 +``` + +### Development + +```bash +docker-compose --profile dev up dev +# Visit http://localhost:4200 +``` + +## Prerequisites + +- Docker installed (version 20.10+) +- Docker Compose installed (version 2.0+) + +## Production Build + +### Build the Docker Image + +```bash +docker build -t sakai-ng:latest . +``` + +### Run the Container + +```bash +docker run -d -p 80:80 --name sakai-ng sakai-ng:latest +``` + +The application will be available at `http://localhost` + +### Run with Docker Compose + +```bash +docker-compose up -d app +``` + +## Staging Build + +### Build for Staging + +```bash +docker build -f Dockerfile.staging -t sakai-ng:staging . +``` + +### Run Staging Container + +```bash +docker run -d -p 8080:80 --name sakai-ng-staging sakai-ng:staging +``` + +### Run with Docker Compose + +```bash +docker-compose --profile staging up -d staging +``` + +The application will be available at `http://localhost:8080` + +## Development Mode + +### Using Docker + +For development with hot reload, use the `dev` profile: + +```bash +docker-compose --profile dev up dev +``` + +The application will be available at `http://localhost:4200` + +### Building Development Image Directly + +```bash +docker build -f Dockerfile.dev -t sakai-ng:dev . +docker run -it -v $(pwd):/app -p 4200:4200 sakai-ng:dev +``` + +## Environment Variables + +To use specific environment configurations, you can build with different targets. Currently, the Dockerfile builds for production. To build for staging: + +### Build for Staging + +Create a separate `Dockerfile.staging` if needed, or manually use: + +```bash +docker run -e NODE_ENV=staging sakai-ng:latest +``` + +## Common Commands + +### Docker Compose Commands + +```bash +# Start production +docker-compose up -d app + +# Start staging +docker-compose --profile staging up -d staging + +# Start development +docker-compose --profile dev up dev + +# Start all services +docker-compose --profile staging --profile dev up -d + +# Stop services +docker-compose down + +# View logs +docker-compose logs -f app # Production logs +docker-compose logs -f staging # Staging logs +docker-compose logs -f dev # Development logs + +# Rebuild images +docker-compose build --no-cache +``` + +### Docker CLI Commands + +```bash +# View Logs + +# Production +docker logs sakai-ng + +# Staging +docker logs sakai-ng-staging + +# Development +docker logs sakai-ng-dev + +# Stop Container + +# Production +docker stop sakai-ng +docker rm sakai-ng + +# Staging +docker stop sakai-ng-staging +docker rm sakai-ng-staging + +# Development +docker stop sakai-ng-dev +docker rm sakai-ng-dev + +# List running containers +docker ps + +# List all containers +docker ps -a +``` + +### Clean Up + +```bash +# Remove stopped containers +docker container prune + +# Remove unused images +docker image prune + +# Remove everything (careful!) +docker system prune -a +``` + +## Image Size Optimization + +The Docker setup uses a multi-stage build: +1. **Builder stage**: Compiles the Angular application +2. **Production stage**: Uses nginx to serve the compiled application + +This approach keeps the final image small (typically <50MB) by excluding build dependencies. + +## Nginx Configuration + +The `nginx.conf` file includes: +- Gzip compression for better performance +- Security headers (X-Frame-Options, X-Content-Type-Options, etc.) +- Proper caching headers for static assets +- Angular routing support (SPA routing) + +## Port Configuration + +- **Production**: Port 80 +- **Development**: Port 4200 + +To use different ports: + +```bash +docker run -d -p 8080:80 sakai-ng:latest +# App available at http://localhost:8080 +``` + +## Building for Different Environments + +This project includes specialized Dockerfiles for different environments: + +### Production + +```bash +docker build -t sakai-ng:prod . +docker run -d -p 80:80 sakai-ng:prod +``` + +**Dockerfile**: `Dockerfile` +**Base Image**: `nginx:alpine` +**Configuration**: Production optimized build + +### Staging + +```bash +docker build -f Dockerfile.staging -t sakai-ng:staging . +docker run -d -p 8080:80 sakai-ng:staging +``` + +**Dockerfile**: `Dockerfile.staging` +**Base Image**: `nginx:alpine` +**Configuration**: Staging configuration with debugging enabled +**Port**: 8080 + +### Development + +```bash +docker build -f Dockerfile.dev -t sakai-ng:dev . +docker run -it -v $(pwd):/app -p 4200:4200 sakai-ng:dev +``` + +**Dockerfile**: `Dockerfile.dev` +**Base Image**: `node:20-alpine` +**Features**: Hot reload, source maps, development server +**Port**: 4200 +**Volume**: Mounts current directory for live updates + +## Security Notes + +- The production image is minimal and only contains what's necessary to run the application +- Security headers are configured in nginx +- Health checks are built in to ensure container availability +- Update base images regularly: `docker pull nginx:alpine` and `docker pull node:20-alpine` + +## Troubleshooting + +### Container exits immediately + +Check logs for errors: +```bash +docker logs sakai-ng +``` + +### Port already in use + +Use a different port: +```bash +docker run -d -p 8080:80 sakai-ng:latest +``` + +### Build fails on pnpm install + +Ensure `pnpm-lock.yaml` is up to date: +```bash +pnpm install --frozen-lockfile +``` + +## CI/CD Integration + +### GitHub Actions Example + +```yaml +name: Build and Push Docker Image + +on: + push: + branches: [main, staging] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Build Docker image + run: | + if [ "${{ github.ref }}" = "refs/heads/staging" ]; then + docker build -f Dockerfile.staging -t sakai-ng:staging . + else + docker build -t sakai-ng:latest . + fi + + - name: Push to registry + run: | + docker tag sakai-ng:latest your-registry/sakai-ng:latest + docker push your-registry/sakai-ng:latest +``` + +### Generic CI/CD + +```bash +#!/bin/bash +BUILD_ID=${CI_COMMIT_SHA:0:8} +REGISTRY=${DOCKER_REGISTRY:-docker.io} + +# Build +docker build -t $REGISTRY/sakai-ng:$BUILD_ID . + +# Test (optional) +docker run --rm $REGISTRY/sakai-ng:$BUILD_ID wget --spider http://localhost + +# Push +docker push $REGISTRY/sakai-ng:$BUILD_ID + +# Tag as latest +docker tag $REGISTRY/sakai-ng:$BUILD_ID $REGISTRY/sakai-ng:latest +docker push $REGISTRY/sakai-ng:latest +``` + +## References + +- [Docker Documentation](https://docs.docker.com/) +- [Nginx Documentation](https://nginx.org/) +- [Angular Deployment Guide](https://angular.io/guide/build) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2118913 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the application for production +RUN pnpm run build + +# Production stage - serve with nginx +FROM nginx:alpine + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Copy built application from builder stage +COPY --from=builder /app/dist/pos.client/browser /usr/share/nginx/html + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..bb90a16 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,22 @@ +# Development Dockerfile with hot reload support +FROM node:20-alpine + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Expose port for Angular dev server +EXPOSE 4200 + +# Start Angular development server +CMD ["pnpm", "start"] diff --git a/Dockerfile.staging b/Dockerfile.staging new file mode 100644 index 0000000..61aa436 --- /dev/null +++ b/Dockerfile.staging @@ -0,0 +1,38 @@ +# Build stage for staging environment +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the application for staging +RUN pnpm run build -- --configuration staging + +# Production stage - serve with nginx +FROM nginx:alpine + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Copy built application from builder stage +COPY --from=builder /app/dist/pos.client/browser /usr/share/nginx/html + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6d4eb1e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,70 @@ +version: "3.8" + +services: + # Production service + app: + build: + context: . + dockerfile: Dockerfile + container_name: sakai-ng-prod + ports: + - "80:80" + environment: + - NODE_ENV=production + restart: unless-stopped + healthcheck: + test: + ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + networks: + - sakai-network + + # Staging service + staging: + build: + context: . + dockerfile: Dockerfile.staging + container_name: sakai-ng-staging + ports: + - "8080:80" + environment: + - NODE_ENV=staging + restart: unless-stopped + healthcheck: + test: + ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + profiles: + - staging + networks: + - sakai-network + + # Development service with hot reload + dev: + build: + context: . + dockerfile: Dockerfile.dev + container_name: sakai-ng-dev + working_dir: /app + volumes: + - .:/app + - /app/node_modules + ports: + - "4200:4200" + environment: + - NODE_ENV=development + restart: unless-stopped + profiles: + - dev + networks: + - sakai-network + +networks: + sakai-network: + driver: bridge diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..b4dd243 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,69 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 20M; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/rss+xml font/truetype font/opentype + application/vnd.ms-fontobject image/svg+xml; + + include /etc/nginx/conf.d/*.conf; + + server { + listen 80; + server_name localhost; + charset utf-8; + + root /usr/share/nginx/html; + index index.html index.htm; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Cache control for static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Angular routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } + + # Don't cache index.html + location = /index.html { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"; + } + } +} diff --git a/src/app/domains/consumer/modules/businessActivities/components/poses/form.component.html b/src/app/domains/consumer/modules/businessActivities/components/poses/form.component.html index d902c50..c4d25d4 100644 --- a/src/app/domains/consumer/modules/businessActivities/components/poses/form.component.html +++ b/src/app/domains/consumer/modules/businessActivities/components/poses/form.component.html @@ -8,13 +8,6 @@ >
- - - - @if (form.controls.pos_type.value === "PSP") { - - - } diff --git a/src/app/domains/consumer/modules/businessActivities/components/poses/form.component.ts b/src/app/domains/consumer/modules/businessActivities/components/poses/form.component.ts index 0a35407..20f11ab 100644 --- a/src/app/domains/consumer/modules/businessActivities/components/poses/form.component.ts +++ b/src/app/domains/consumer/modules/businessActivities/components/poses/form.component.ts @@ -34,35 +34,8 @@ export class ConsumerPosFormComponent extends AbstractFormDialog { const form = this.fb.group({ name: [this.initialValues?.name || '', [Validators.required]], - serial: [this.initialValues?.serial || '', [Validators.required]], - // model: [this.initialValues?.model || '', [Validators.required]], - pos_type: [this.initialValues?.pos_type || '', [Validators.required]], - device_id: [this.initialValues?.device?.id || ''], - provider_id: [this.initialValues?.provider?.id || ''], - }); - form.controls.pos_type.valueChanges.subscribe((value) => { - if (value === 'PSP') { - form.addControl( - 'device_id', - this.fb.control(this.initialValues?.device?.id ?? '', { - nonNullable: true, - validators: [Validators.required], - }), - ); - form.addControl( - 'provider_id', - this.fb.control(this.initialValues?.provider?.id ?? '', { - nonNullable: true, - validators: [Validators.required], - }), - ); - } else { - // @ts-ignore - form.removeControl('device_id'); - // @ts-ignore - form.removeControl('provider_id'); - } }); + return form; }; diff --git a/src/app/domains/consumer/modules/businessActivities/components/salesInvoices/list.component.html b/src/app/domains/consumer/modules/businessActivities/components/salesInvoices/list.component.html new file mode 100644 index 0000000..953e2e5 --- /dev/null +++ b/src/app/domains/consumer/modules/businessActivities/components/salesInvoices/list.component.html @@ -0,0 +1,8 @@ + + diff --git a/src/app/domains/consumer/modules/businessActivities/components/salesInvoices/list.component.ts b/src/app/domains/consumer/modules/businessActivities/components/salesInvoices/list.component.ts new file mode 100644 index 0000000..f882c48 --- /dev/null +++ b/src/app/domains/consumer/modules/businessActivities/components/salesInvoices/list.component.ts @@ -0,0 +1,47 @@ +// import { CatalogRoleTagComponent } from '@/shared/catalog/roles'; +import { AbstractList } from '@/shared/abstractClasses/abstract-list'; +import { + IColumn, + PageDataListComponent, +} from '@/shared/components/pageDataList/page-data-list.component'; +import { Component, inject, Input } from '@angular/core'; +import { ISalesInvoicesResponse } from '../../models'; +import { ConsumerSalesInvoicesService } from '../../services/invoices.service'; + +@Component({ + selector: 'consumer-salesInvoices-list', + templateUrl: './list.component.html', + imports: [PageDataListComponent], +}) +export class ConsumerSalesInvoicesComponent extends AbstractList { + @Input({ required: true }) complexId!: string; + @Input({ required: true }) posId!: string; + @Input() fullHeight?: boolean; + @Input() header: IColumn[] = [ + { field: 'id', header: 'شناسه', type: 'id' }, + { field: 'code', header: 'کد رهگیری' }, + { field: 'total_amount', header: 'مبلغ کل', type: 'price' }, + { + field: 'items_count', + header: 'تعداد کالاها', + customDataModel(item) { + return `${item.items.length} عدد`; + }, + }, + { + field: 'invoice_date', + header: 'تاریخ', + type: 'date', + }, + ]; + + private readonly service = inject(ConsumerSalesInvoicesService); + + override setColumns(): void { + this.columns = this.header; + } + + override getDataRequest() { + return this.service.getAll(this.complexId, this.posId); + } +} diff --git a/src/app/domains/consumer/modules/businessActivities/constants/apiRoutes/posSalesInvoices.ts b/src/app/domains/consumer/modules/businessActivities/constants/apiRoutes/posSalesInvoices.ts new file mode 100644 index 0000000..5948d61 --- /dev/null +++ b/src/app/domains/consumer/modules/businessActivities/constants/apiRoutes/posSalesInvoices.ts @@ -0,0 +1,8 @@ +const baseUrl = (complexId: string, posId: string) => + `/api/v1/consumer/complexes/${complexId}/poses/${posId}/sales_invoices`; + +export const POS_SALES_INVOICES_API_ROUTES = { + list: (complexId: string, posId: string) => `${baseUrl(complexId, posId)}`, + single: (complexId: string, posId: string, invoiceId: string) => + `${baseUrl(complexId, posId)}/${invoiceId}`, +}; diff --git a/src/app/domains/consumer/modules/businessActivities/models/index.ts b/src/app/domains/consumer/modules/businessActivities/models/index.ts index cf6f041..c505459 100644 --- a/src/app/domains/consumer/modules/businessActivities/models/index.ts +++ b/src/app/domains/consumer/modules/businessActivities/models/index.ts @@ -1,3 +1,4 @@ export * from './complexes_io'; export * from './io'; export * from './poses_io'; +export * from './salesInvoices_io'; diff --git a/src/app/domains/consumer/modules/businessActivities/models/poses_io.d.ts b/src/app/domains/consumer/modules/businessActivities/models/poses_io.d.ts index 1c76cae..17d9029 100644 --- a/src/app/domains/consumer/modules/businessActivities/models/poses_io.d.ts +++ b/src/app/domains/consumer/modules/businessActivities/models/poses_io.d.ts @@ -15,10 +15,4 @@ export interface IPosResponse extends IPosRawResponse {} export interface IPosRequest { name: string; - serial: string; - model?: string; - status: string; - pos_type: string; - device_id: string; - provider_id: string; } diff --git a/src/app/domains/consumer/modules/businessActivities/models/salesInvoices_io.d.ts b/src/app/domains/consumer/modules/businessActivities/models/salesInvoices_io.d.ts new file mode 100644 index 0000000..b054f03 --- /dev/null +++ b/src/app/domains/consumer/modules/businessActivities/models/salesInvoices_io.d.ts @@ -0,0 +1,51 @@ +import ISummary from '@/core/models/summary'; +import { TCustomerInfo, TPosOrderGoodPayload } from '@/domains/pos/modules/landing/models'; +import { UnitType } from '@/utils'; + +export interface ISalesInvoicesRawResponse { + id: string; + code: string; + invoice_date: string; + total_amount: string; + items: Item[]; + payments: Payment[]; + customer?: TCustomerInfo; + notes?: string; + unknown_customer?: UnknownCustomer; +} +export interface ISalesInvoicesResponse extends ISalesInvoicesRawResponse {} + +export interface ISalesInvoicesRequest {} + +interface UnknownCustomer { + name?: string; + national_code?: string; +} + +interface Payment { + amount: string; + paid_at: string; + payment_method: string; +} + +interface Item { + unit_type: UnitType; + discount: string; + quantity: string; + total_amount: string; + unit_price: string; + payload: TPosOrderGoodPayload; + good: Good; + notes?: string; +} + +interface Good { + id: string; + name: string; + sku: string; + barcode: null; + local_sku: null; + pricing_model: string; + unit_type: string; + category: ISummary; +} diff --git a/src/app/domains/consumer/modules/businessActivities/services/invoices.service.ts b/src/app/domains/consumer/modules/businessActivities/services/invoices.service.ts new file mode 100644 index 0000000..aa58367 --- /dev/null +++ b/src/app/domains/consumer/modules/businessActivities/services/invoices.service.ts @@ -0,0 +1,28 @@ +import { IPaginatedResponse } from '@/core/models/service.model'; +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { POS_SALES_INVOICES_API_ROUTES } from '../constants/apiRoutes/posSalesInvoices'; +import { ISalesInvoicesRawResponse, ISalesInvoicesResponse } from '../models'; + +@Injectable({ providedIn: 'root' }) +export class ConsumerSalesInvoicesService { + constructor(private http: HttpClient) {} + + private apiRoutes = POS_SALES_INVOICES_API_ROUTES; + + getAll(complexId: string, posId: string): Observable> { + return this.http.get>( + this.apiRoutes.list(complexId, posId), + ); + } + getSingle( + complexId: string, + posId: string, + invoiceId: string, + ): Observable { + return this.http.get( + this.apiRoutes.single(complexId, posId, invoiceId), + ); + } +} diff --git a/src/app/domains/consumer/modules/businessActivities/views/poses/single.component.html b/src/app/domains/consumer/modules/businessActivities/views/poses/single.component.html index afdc3f2..165f996 100644 --- a/src/app/domains/consumer/modules/businessActivities/views/poses/single.component.html +++ b/src/app/domains/consumer/modules/businessActivities/views/poses/single.component.html @@ -6,10 +6,17 @@
+ + + + +
+ + {{ good.name }} - + @if (!good.is_default_guild_good) { + * + } -
+
- +
diff --git a/src/app/domains/pos/modules/landing/components/list-view.component.html b/src/app/domains/pos/modules/landing/components/list-view.component.html index 4f4588a..e700d65 100644 --- a/src/app/domains/pos/modules/landing/components/list-view.component.html +++ b/src/app/domains/pos/modules/landing/components/list-view.component.html @@ -13,6 +13,9 @@
{{ good.name }} + @if (!good.is_default_guild_good) { + * + } @@ -22,7 +25,7 @@
- +
} diff --git a/src/app/domains/pos/modules/landing/components/order/order-section.component.html b/src/app/domains/pos/modules/landing/components/order/order-section.component.html index 9b113f5..d8baa15 100644 --- a/src/app/domains/pos/modules/landing/components/order/order-section.component.html +++ b/src/app/domains/pos/modules/landing/components/order/order-section.component.html @@ -1,10 +1,5 @@
-
-
- - -
-
+
@@ -20,7 +15,7 @@ هیچ سفارشی ثبت نشده است.
} @else { -
+
@for (inOrderGood of inOrderGoods(); track inOrderGood.id) { @if (inOrderGood.good.pricing_model === "GOLD") { @@ -31,20 +26,25 @@
}
-
+
+
+ +
+ + +
+
-
- -
+
diff --git a/src/app/domains/pos/modules/landing/components/order/order-section.component.ts b/src/app/domains/pos/modules/landing/components/order/order-section.component.ts index c271765..300ffc2 100644 --- a/src/app/domains/pos/modules/landing/components/order/order-section.component.ts +++ b/src/app/domains/pos/modules/landing/components/order/order-section.component.ts @@ -4,6 +4,7 @@ import { KeyValueComponent } from '@/shared/components'; import { Component, computed, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ButtonDirective } from 'primeng/button'; +import { Card } from 'primeng/card'; import images from 'src/assets/images'; import { PosLandingStore } from '../../store/main.store'; import { PosOrderCustomerDialogComponent } from '../customers/customer-dialog.component'; @@ -24,6 +25,7 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component'; PosOrderCustomerDialogComponent, KeyValueComponent, PosPaymentFormDialogComponent, + Card, ], }) export class PosOrderSectionComponent { @@ -44,13 +46,13 @@ export class PosOrderSectionComponent { switch (type) { case 'UNKNOWN': - customerNameToShow = `${info?.customerUnknown?.name || 'نامشخص'} (نوع دوم)`; + customerNameToShow = `${info?.customer_unknown?.name || 'نامشخص'} (نوع دوم)`; break; case 'INDIVIDUAL': - customerNameToShow = `${info?.customerIndividuals ? info?.customerIndividuals.first_name + ' ' + info?.customerIndividuals.last_name : 'نامشخص'} (نوع اول)`; + customerNameToShow = `${info?.customer_individual ? info?.customer_individual.first_name + ' ' + info?.customer_individual.last_name : 'نامشخص'} (نوع اول)`; break; case 'LEGAL': - customerNameToShow = `${info?.customerLegals?.company_name || 'نامشخص'} (نوع اول)`; + customerNameToShow = `${info?.customer_legal?.company_name || 'نامشخص'} (نوع اول)`; break; } diff --git a/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.html b/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.html index 3006830..bd03078 100644 --- a/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.html +++ b/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.html @@ -43,7 +43,6 @@ submitLabel="ثبت اطلاعات پرداخت و ادامه" [loading]="submitOrderLoading()" (onCancel)="close()" - (onSubmit)="submit()" /> diff --git a/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.ts b/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.ts index e6371b7..56523dd 100644 --- a/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.ts +++ b/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.ts @@ -6,7 +6,7 @@ import { Component, computed, inject, signal } from '@angular/core'; import { ReactiveFormsModule, Validators } from '@angular/forms'; import { ButtonDirective } from 'primeng/button'; import { Dialog } from 'primeng/dialog'; -import { Observable } from 'rxjs'; +import { catchError, throwError } from 'rxjs'; import { IPayment, TOrderPaymentTypes } from '../../models'; import { PosLandingStore } from '../../store/main.store'; @@ -90,6 +90,8 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog 0) { return this.toastServices.warn({ @@ -100,16 +102,19 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog { - if (res) { + this.store + .submitOrder() + .pipe( + catchError((err) => { + return throwError(() => err); + }), + ) + .subscribe(() => { this.close(); this.toastServices.success({ text: 'فاکتور شما با موفقیت ایجاد شد', }); - } - }); - - return new Observable((observer) => observer.next(payment)); + }); } override onSuccess(response: IPayment): void {} diff --git a/src/app/domains/pos/modules/landing/models/customer.ts b/src/app/domains/pos/modules/landing/models/customer.ts index 0df00e5..953cd64 100644 --- a/src/app/domains/pos/modules/landing/models/customer.ts +++ b/src/app/domains/pos/modules/landing/models/customer.ts @@ -20,9 +20,9 @@ export interface IUnknownCustomer { } export interface ICustomer { - customerIndividuals?: IIndividualCustomer; - customerLegals?: ILegalCustomer; - customerUnknown?: IUnknownCustomer; + customer_individual?: IIndividualCustomer; + customer_legal?: ILegalCustomer; + customer_unknown?: IUnknownCustomer; } export type TCustomerInfo = IIndividualCustomer | ILegalCustomer | IUnknownCustomer; diff --git a/src/app/domains/pos/modules/landing/models/io.d.ts b/src/app/domains/pos/modules/landing/models/io.d.ts index 8f0bfb1..a780ca3 100644 --- a/src/app/domains/pos/modules/landing/models/io.d.ts +++ b/src/app/domains/pos/modules/landing/models/io.d.ts @@ -10,7 +10,7 @@ export interface IPosOrderRequest { items: IPosOrderItem[]; notes?: string; customer_type?: CustomerType; - customerId?: string; + customer_id?: string; customer?: ICustomer; } diff --git a/src/app/domains/pos/modules/landing/store/main.store.ts b/src/app/domains/pos/modules/landing/store/main.store.ts index 9c348f5..3f5c1a8 100644 --- a/src/app/domains/pos/modules/landing/store/main.store.ts +++ b/src/app/domains/pos/modules/landing/store/main.store.ts @@ -4,7 +4,7 @@ import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good. import { CustomerType } from '@/shared/localEnum/constants/customerTypes'; import { createUUID, JALALI_DATE_FORMATS, nowJalali, parseJalali } from '@/utils'; import { computed, inject, Injectable, signal } from '@angular/core'; -import { catchError, finalize, map, Observable, throwError } from 'rxjs'; +import { catchError, finalize, map, Observable } from 'rxjs'; import { ICustomer, IPayment, IPosOrderRequest, IPosOrderResponse } from '../models'; import { IPosInOrderGood, IPosOrderItem } from '../models/types'; import { PosService } from '../services/main.service'; @@ -208,10 +208,10 @@ export class PosLandingStore { this.setState({ payments }); } - async submitOrder() { + submitOrder() { this.setState({ submitOrderLoading: true }); const orderPayload: IPosOrderRequest = { - customerId: this.customer().customer_id, + customer_id: this.customer().customer_id, customer_type: this.customer().type, customer: this.customer().info, items: this.inOrderGoods().map(({ good, ...rest }) => ({ @@ -226,24 +226,30 @@ export class PosLandingStore { let res: Maybe = null; - await this.service - .submitOrder(orderPayload) - .pipe( - finalize(() => { - this.setState({ submitOrderLoading: false }); - }), - catchError(() => { - return throwError(''); - }), - ) - .subscribe((_res) => { - this.reset(); - res = _res; - }); + return this.service.submitOrder(orderPayload).pipe( + finalize(() => { + this.setState({ submitOrderLoading: false }); + }), + catchError(() => { + return new Observable((observer) => observer.error()); + }), - if (res) { - return new Observable((observer) => observer.next(res!)); - } - return new Observable((observer) => observer.error()); + map((_res) => { + // this.setState({ + // inOrderGoods: [], + // customerDetails: { + // type: CustomerType.UNKNOWN, + // }, + // invoiceDate: nowJalali(JALALI_DATE_FORMATS.NUMERIC), + // payments: { + // cash: 0, + // set_off: 0, + // terminal: 0, + // }, + // orderNote: '', + // }); + return _res; + }), + ); } } diff --git a/src/app/modules/pos/store/index.ts b/src/app/modules/pos/store/index.ts index d63476f..0588075 100644 --- a/src/app/modules/pos/store/index.ts +++ b/src/app/modules/pos/store/index.ts @@ -251,7 +251,7 @@ export class POSStore { submitOrder() { const orderPayload = { - customerId: this.selectedCustomer()?.id, + customer_id: this.selectedCustomer()?.id, items: this.inOrderProducts()?.map((item) => ({ productId: item.productId, count: item.count, diff --git a/src/app/shared/components/input/input.component.ts b/src/app/shared/components/input/input.component.ts index 360085f..f553fca 100644 --- a/src/app/shared/components/input/input.component.ts +++ b/src/app/shared/components/input/input.component.ts @@ -184,8 +184,6 @@ export class InputComponent { } onPriceInput($event: InputNumberInputEvent) { - console.log($event); - this.onInput($event.originalEvent, true); } onInput($event: Event, isPriceFormat?: boolean) { @@ -198,7 +196,6 @@ export class InputComponent { if (this.inputMode === 'numeric') { let newValue = parseFloat(value); - // console.log(value, newValue); const minValidator = (this.min || this.min === 0) && parseFloat(value) < this.min!; const maxValidator = (this.max || this.max === 0) && parseFloat(value) > this.max; diff --git a/src/app/shared/components/key-value.component/key-value.component.ts b/src/app/shared/components/key-value.component/key-value.component.ts index 50e7455..18d3995 100644 --- a/src/app/shared/components/key-value.component/key-value.component.ts +++ b/src/app/shared/components/key-value.component/key-value.component.ts @@ -89,7 +89,7 @@ export class KeyValueComponent { } switch (this.type) { case 'simple': - return this.value || ''; + return this.value || '-'; } return value; } diff --git a/src/app/shared/components/pageDataList/page-data-list.component.ts b/src/app/shared/components/pageDataList/page-data-list.component.ts index c126315..39ff44f 100644 --- a/src/app/shared/components/pageDataList/page-data-list.component.ts +++ b/src/app/shared/components/pageDataList/page-data-list.component.ts @@ -26,7 +26,7 @@ export interface IColumn { width?: string; minWidth?: string; canCopy?: boolean; - type?: 'text' | 'price' | 'boolean' | 'date' | 'nested' | 'index'; + type?: 'text' | 'price' | 'boolean' | 'date' | 'nested' | 'index' | 'id'; nestedPath?: string; customDataModel?: TemplateRef | ((item: T) => string | number | boolean); } @@ -139,6 +139,9 @@ export class PageDataListComponent { } const data = item[String(field)]; switch (column.type) { + case 'id': + if (!data) return '-'; + return `${data.slice(0, 5)}...${data.slice(data.length - 5, data.length)}`; case 'date': if (!data) return '-'; return formatJalali(data);