add salesInvoice in pos of consumer domain
This commit is contained in:
@@ -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
|
||||||
@@ -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)
|
||||||
+38
@@ -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;"]
|
||||||
@@ -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"]
|
||||||
@@ -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;"]
|
||||||
@@ -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
|
||||||
+69
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-7
@@ -8,13 +8,6 @@
|
|||||||
>
|
>
|
||||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
<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.name" name="name" />
|
||||||
<app-input label="سریال دستگاه" [control]="form.controls.serial" name="serial" />
|
|
||||||
<!-- <app-input label="مدل دستگاه" [control]="form.controls.model" name="model" /> -->
|
|
||||||
<app-enum-select [control]="form.controls.pos_type" type="posType" name="pos_type" />
|
|
||||||
@if (form.controls.pos_type.value === "PSP") {
|
|
||||||
<catalog-device-select [control]="form.controls.device_id" />
|
|
||||||
<catalog-provider-select [control]="form.controls.provider_id" />
|
|
||||||
}
|
|
||||||
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
|
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
|
||||||
</form>
|
</form>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|||||||
+1
-28
@@ -34,35 +34,8 @@ export class ConsumerPosFormComponent extends AbstractFormDialog<IPosRequest, IP
|
|||||||
initForm = () => {
|
initForm = () => {
|
||||||
const form = this.fb.group({
|
const form = this.fb.group({
|
||||||
name: [this.initialValues?.name || '', [Validators.required]],
|
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<string>(this.initialValues?.device?.id ?? '', {
|
|
||||||
nonNullable: true,
|
|
||||||
validators: [Validators.required],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
form.addControl(
|
|
||||||
'provider_id',
|
|
||||||
this.fb.control<string>(this.initialValues?.provider?.id ?? '', {
|
|
||||||
nonNullable: true,
|
|
||||||
validators: [Validators.required],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
form.removeControl('device_id');
|
|
||||||
// @ts-ignore
|
|
||||||
form.removeControl('provider_id');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return form;
|
return form;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<app-page-data-list
|
||||||
|
pageTitle="لیست فاکتورهای صادر شده"
|
||||||
|
[columns]="columns"
|
||||||
|
emptyPlaceholderTitle="تا به حال فاکتور فروشی توسط این پایانه ایجاد نشده است."
|
||||||
|
[items]="items()"
|
||||||
|
[loading]="loading()"
|
||||||
|
>
|
||||||
|
</app-page-data-list>
|
||||||
+47
@@ -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<ISalesInvoicesResponse> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -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}`,
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './complexes_io';
|
export * from './complexes_io';
|
||||||
export * from './io';
|
export * from './io';
|
||||||
export * from './poses_io';
|
export * from './poses_io';
|
||||||
|
export * from './salesInvoices_io';
|
||||||
|
|||||||
@@ -15,10 +15,4 @@ export interface IPosResponse extends IPosRawResponse {}
|
|||||||
|
|
||||||
export interface IPosRequest {
|
export interface IPosRequest {
|
||||||
name: string;
|
name: string;
|
||||||
serial: string;
|
|
||||||
model?: string;
|
|
||||||
status: string;
|
|
||||||
pos_type: string;
|
|
||||||
device_id: string;
|
|
||||||
provider_id: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+51
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<IPaginatedResponse<ISalesInvoicesResponse>> {
|
||||||
|
return this.http.get<IPaginatedResponse<ISalesInvoicesRawResponse>>(
|
||||||
|
this.apiRoutes.list(complexId, posId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
getSingle(
|
||||||
|
complexId: string,
|
||||||
|
posId: string,
|
||||||
|
invoiceId: string,
|
||||||
|
): Observable<ISalesInvoicesResponse> {
|
||||||
|
return this.http.get<ISalesInvoicesRawResponse>(
|
||||||
|
this.apiRoutes.single(complexId, posId, invoiceId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,17 @@
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="grid grid-cols-3 gap-4 items-center">
|
<div class="grid grid-cols-3 gap-4 items-center">
|
||||||
<app-key-value label="عنوان" [value]="pos()?.name" />
|
<app-key-value label="عنوان" [value]="pos()?.name" />
|
||||||
|
<app-key-value label="شماره سریال" [value]="pos()?.serial" />
|
||||||
|
<app-key-value label="نوع پایانه" [value]="pos()?.pos_type" />
|
||||||
|
<app-key-value label="نوع دستگاه" [value]="pos()?.device?.name" />
|
||||||
|
<app-key-value label="مدل دستگاه" [value]="pos()?.model" />
|
||||||
|
<app-key-value label="ارایهدهنده" [value]="pos()?.provider?.name" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-card-data>
|
</app-card-data>
|
||||||
|
|
||||||
|
<consumer-salesInvoices-list [complexId]="complexId()" [posId]="posId()" />
|
||||||
|
|
||||||
<consumer-pos-form
|
<consumer-pos-form
|
||||||
[(visible)]="editMode"
|
[(visible)]="editMode"
|
||||||
[editMode]="true"
|
[editMode]="true"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { CookieService } from 'ngx-cookie-service';
|
import { CookieService } from 'ngx-cookie-service';
|
||||||
import { Button } from 'primeng/button';
|
import { Button } from 'primeng/button';
|
||||||
import { ConsumerPosFormComponent } from '../../components/poses/form.component';
|
import { ConsumerPosFormComponent } from '../../components/poses/form.component';
|
||||||
|
import { ConsumerSalesInvoicesComponent } from '../../components/salesInvoices/list.component';
|
||||||
import { BusinessActivityStore } from '../../store/businessActivity.store';
|
import { BusinessActivityStore } from '../../store/businessActivity.store';
|
||||||
import { ConsumerComplexStore } from '../../store/complex.store';
|
import { ConsumerComplexStore } from '../../store/complex.store';
|
||||||
import { PosStore } from '../../store/pos.store';
|
import { PosStore } from '../../store/pos.store';
|
||||||
@@ -13,7 +14,13 @@ import { PosStore } from '../../store/pos.store';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'superAdmin-user-pos',
|
selector: 'superAdmin-user-pos',
|
||||||
templateUrl: './single.component.html',
|
templateUrl: './single.component.html',
|
||||||
imports: [AppCardComponent, KeyValueComponent, ConsumerPosFormComponent, Button],
|
imports: [
|
||||||
|
AppCardComponent,
|
||||||
|
KeyValueComponent,
|
||||||
|
ConsumerPosFormComponent,
|
||||||
|
Button,
|
||||||
|
ConsumerSalesInvoicesComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class SuperAdminUserPosComponent {
|
export class SuperAdminUserPosComponent {
|
||||||
private readonly businessStore = inject(BusinessActivityStore);
|
private readonly businessStore = inject(BusinessActivityStore);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface IGoodRawResponse {
|
|||||||
unit_type: UnitType;
|
unit_type: UnitType;
|
||||||
pricing_model: string;
|
pricing_model: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
is_default_guild_good: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -24,23 +24,23 @@ export class CustomerIndividualFormComponent extends AbstractForm<
|
|||||||
override showSuccessMessage = false;
|
override showSuccessMessage = false;
|
||||||
form = this.fb.group({
|
form = this.fb.group({
|
||||||
first_name: [
|
first_name: [
|
||||||
this.store.customer().info?.customerIndividuals?.first_name || '',
|
this.store.customer().info?.customer_individual?.first_name || '',
|
||||||
[Validators.required],
|
[Validators.required],
|
||||||
],
|
],
|
||||||
last_name: [
|
last_name: [
|
||||||
this.store.customer().info?.customerIndividuals?.last_name || '',
|
this.store.customer().info?.customer_individual?.last_name || '',
|
||||||
[Validators.required],
|
[Validators.required],
|
||||||
],
|
],
|
||||||
national_id: [
|
national_id: [
|
||||||
this.store.customer().info?.customerIndividuals?.national_id || '',
|
this.store.customer().info?.customer_individual?.national_id || '',
|
||||||
[Validators.required, nationalIdValidator()],
|
[Validators.required, nationalIdValidator()],
|
||||||
],
|
],
|
||||||
economic_code: [
|
economic_code: [
|
||||||
this.store.customer().info?.customerIndividuals?.economic_code || '',
|
this.store.customer().info?.customer_individual?.economic_code || '',
|
||||||
[Validators.required],
|
[Validators.required],
|
||||||
],
|
],
|
||||||
postal_code: [
|
postal_code: [
|
||||||
this.store.customer().info?.customerIndividuals?.postal_code || '',
|
this.store.customer().info?.customer_individual?.postal_code || '',
|
||||||
[postalCodeValidator()],
|
[postalCodeValidator()],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -52,7 +52,7 @@ export class CustomerIndividualFormComponent extends AbstractForm<
|
|||||||
customer_id: '',
|
customer_id: '',
|
||||||
type: CustomerType.INDIVIDUAL,
|
type: CustomerType.INDIVIDUAL,
|
||||||
info: {
|
info: {
|
||||||
customerIndividuals: info,
|
customer_individual: info,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return observer.next(info);
|
return observer.next(info);
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILe
|
|||||||
|
|
||||||
form = this.fb.group({
|
form = this.fb.group({
|
||||||
company_name: [
|
company_name: [
|
||||||
this.store.customer().info?.customerLegals?.company_name || '',
|
this.store.customer().info?.customer_legal?.company_name || '',
|
||||||
[Validators.required],
|
[Validators.required],
|
||||||
],
|
],
|
||||||
registration_number: [
|
registration_number: [
|
||||||
this.store.customer().info?.customerLegals?.registration_number || '',
|
this.store.customer().info?.customer_legal?.registration_number || '',
|
||||||
[Validators.required],
|
[Validators.required],
|
||||||
],
|
],
|
||||||
economic_code: [
|
economic_code: [
|
||||||
this.store.customer().info?.customerLegals?.economic_code || '',
|
this.store.customer().info?.customer_legal?.economic_code || '',
|
||||||
[Validators.required],
|
[Validators.required],
|
||||||
],
|
],
|
||||||
postal_code: [
|
postal_code: [
|
||||||
this.store.customer().info?.customerLegals?.postal_code || '',
|
this.store.customer().info?.customer_legal?.postal_code || '',
|
||||||
[postalCodeValidator()],
|
[postalCodeValidator()],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -45,7 +45,7 @@ export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILe
|
|||||||
customer_id: '',
|
customer_id: '',
|
||||||
type: CustomerType.LEGAL,
|
type: CustomerType.LEGAL,
|
||||||
info: {
|
info: {
|
||||||
customerLegals: info,
|
customer_legal: info,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return observer.next(info);
|
return observer.next(info);
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ export class CustomerUnknownFormComponent extends AbstractForm<IUnknownCustomer,
|
|||||||
override showSuccessMessage = false;
|
override showSuccessMessage = false;
|
||||||
form = this.fb.group({
|
form = this.fb.group({
|
||||||
national_id: [
|
national_id: [
|
||||||
this.store?.customer().info?.customerUnknown?.national_id || '',
|
this.store?.customer().info?.customer_unknown?.national_id || '',
|
||||||
[nationalIdValidator()],
|
[nationalIdValidator()],
|
||||||
],
|
],
|
||||||
name: [this.store?.customer().info?.customerUnknown?.name || ''],
|
name: [this.store?.customer().info?.customer_unknown?.name || ''],
|
||||||
});
|
});
|
||||||
|
|
||||||
override submitForm() {
|
override submitForm() {
|
||||||
@@ -33,7 +33,7 @@ export class CustomerUnknownFormComponent extends AbstractForm<IUnknownCustomer,
|
|||||||
customer_id: '',
|
customer_id: '',
|
||||||
type: CustomerType.UNKNOWN,
|
type: CustomerType.UNKNOWN,
|
||||||
info: {
|
info: {
|
||||||
customerUnknown: info,
|
customer_unknown: info,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return observer.next(info);
|
return observer.next(info);
|
||||||
|
|||||||
@@ -12,11 +12,15 @@
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<span class="text-lg font-bold">
|
<span class="text-lg font-bold">
|
||||||
{{ good.name }}
|
{{ good.name }}
|
||||||
<!-- <small class="text-xs text-muted-color">({{ good.quantity }})</small> -->
|
@if (!good.is_default_guild_good) {
|
||||||
|
<small class="text-xs text-muted-color">*</small>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center justify-between mt-1">
|
<div class="flex items-center justify-between mt-1 w-full px-2 my-2">
|
||||||
<!-- <span [appPriceMask]="good.salePrice" class="text-base font-bold"></span> -->
|
<!-- <span [appPriceMask]="good.salePrice" class="text-base font-bold"></span> -->
|
||||||
<button pButton type="button" icon="pi pi-plus" size="small" (click)="addProduct(good)"></button>
|
<button pButton type="button" icon="pi pi-plus" size="small" class="w-full!" (click)="addProduct(good)">
|
||||||
|
انتخاب
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="text-lg font-bold">
|
<span class="text-lg font-bold">
|
||||||
{{ good.name }}
|
{{ good.name }}
|
||||||
|
@if (!good.is_default_guild_good) {
|
||||||
|
<small class="text-xs text-muted-color">*</small>
|
||||||
|
}
|
||||||
<!-- <small class="text-xs text-muted-color">({{ good.quantity }})</small> -->
|
<!-- <small class="text-xs text-muted-color">({{ good.quantity }})</small> -->
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm text-muted-color">
|
<span class="text-sm text-muted-color">
|
||||||
@@ -22,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="shrink-0 flex items-center justify-between gap-3">
|
<div class="shrink-0 flex items-center justify-between gap-3">
|
||||||
<!-- <span [appPriceMask]="good.salePrice" class="text-base font-bold"></span> -->
|
<!-- <span [appPriceMask]="good.salePrice" class="text-base font-bold"></span> -->
|
||||||
<button pButton type="button" icon="pi pi-plus" size="small" (click)="addGood(good)"></button>
|
<button pButton type="button" icon="pi pi-plus" size="small" (click)="addGood(good)">انتخاب</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<div class="bg-surface-card w-md shrink-0 h-full overflow-hidden rounded-xl p-4 pb-0 flex flex-col">
|
<div class="bg-surface-card w-md shrink-0 h-full overflow-hidden rounded-xl p-4 pb-0 flex flex-col">
|
||||||
<div class="grow overflow-auto flex flex-col">
|
<div class="grow flex flex-col overflow-hidden">
|
||||||
<div class="flex gap-2 items-center justify-between shrink-0">
|
|
||||||
<app-key-value label="مشتری" [value]="customerNameToShow()" />
|
|
||||||
<button pButton outlined icon="pi pi-pencil" size="small" (click)="openCustomerDialog()"></button>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div class="flex items-center justify-between shrink-0">
|
<div class="flex items-center justify-between shrink-0">
|
||||||
<div class="flex items-center gap-2 text-muted-color">
|
<div class="flex items-center gap-2 text-muted-color">
|
||||||
<i class="pi pi-shopping-cart"></i>
|
<i class="pi pi-shopping-cart"></i>
|
||||||
@@ -20,7 +15,7 @@
|
|||||||
<span class="text-lg text-muted-color">هیچ سفارشی ثبت نشده است.</span>
|
<span class="text-lg text-muted-color">هیچ سفارشی ثبت نشده است.</span>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="flex flex-col gap-2 mt-3">
|
<div class="flex flex-col gap-2 mt-3 overflow-auto">
|
||||||
@for (inOrderGood of inOrderGoods(); track inOrderGood.id) {
|
@for (inOrderGood of inOrderGoods(); track inOrderGood.id) {
|
||||||
@if (inOrderGood.good.pricing_model === "GOLD") {
|
@if (inOrderGood.good.pricing_model === "GOLD") {
|
||||||
<gold-payload-order-card [orderItem]="inOrderGood" />
|
<gold-payload-order-card [orderItem]="inOrderGood" />
|
||||||
@@ -31,20 +26,25 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col sticky bottom-0 py-2">
|
<hr />
|
||||||
|
<div class="flex flex-col gap-4 sticky bottom-0 py-2">
|
||||||
|
<p-card class="border border-surface-border">
|
||||||
|
<div class="flex gap-2 items-center justify-between shrink-0">
|
||||||
|
<app-key-value label="مشتری" [value]="customerNameToShow()" />
|
||||||
|
<button pButton outlined icon="pi pi-pencil" size="small" (click)="openCustomerDialog()"></button>
|
||||||
|
</div>
|
||||||
|
</p-card>
|
||||||
<pos-order-price-info-card />
|
<pos-order-price-info-card />
|
||||||
<div class="sticky bottom-0 pt-4">
|
<button
|
||||||
<button
|
pButton
|
||||||
pButton
|
type="button"
|
||||||
type="button"
|
label="ثبت و پرداخت"
|
||||||
label="ثبت و پرداخت"
|
severity="primary"
|
||||||
severity="primary"
|
icon="pi pi-check"
|
||||||
icon="pi pi-check"
|
class="w-full"
|
||||||
class="w-full"
|
[attr.disabled]="inOrderGoods().length === 0 ? true : null"
|
||||||
[attr.disabled]="inOrderGoods().length === 0 ? true : null"
|
(click)="submitAndPay()"
|
||||||
(click)="submitAndPay()"
|
></button>
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { KeyValueComponent } from '@/shared/components';
|
|||||||
import { Component, computed, inject, signal } from '@angular/core';
|
import { Component, computed, inject, signal } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ButtonDirective } from 'primeng/button';
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
import images from 'src/assets/images';
|
import images from 'src/assets/images';
|
||||||
import { PosLandingStore } from '../../store/main.store';
|
import { PosLandingStore } from '../../store/main.store';
|
||||||
import { PosOrderCustomerDialogComponent } from '../customers/customer-dialog.component';
|
import { PosOrderCustomerDialogComponent } from '../customers/customer-dialog.component';
|
||||||
@@ -24,6 +25,7 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
|
|||||||
PosOrderCustomerDialogComponent,
|
PosOrderCustomerDialogComponent,
|
||||||
KeyValueComponent,
|
KeyValueComponent,
|
||||||
PosPaymentFormDialogComponent,
|
PosPaymentFormDialogComponent,
|
||||||
|
Card,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PosOrderSectionComponent {
|
export class PosOrderSectionComponent {
|
||||||
@@ -44,13 +46,13 @@ export class PosOrderSectionComponent {
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'UNKNOWN':
|
case 'UNKNOWN':
|
||||||
customerNameToShow = `${info?.customerUnknown?.name || 'نامشخص'} (نوع دوم)`;
|
customerNameToShow = `${info?.customer_unknown?.name || 'نامشخص'} (نوع دوم)`;
|
||||||
break;
|
break;
|
||||||
case 'INDIVIDUAL':
|
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;
|
break;
|
||||||
case 'LEGAL':
|
case 'LEGAL':
|
||||||
customerNameToShow = `${info?.customerLegals?.company_name || 'نامشخص'} (نوع اول)`;
|
customerNameToShow = `${info?.customer_legal?.company_name || 'نامشخص'} (نوع اول)`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
submitLabel="ثبت اطلاعات پرداخت و ادامه"
|
submitLabel="ثبت اطلاعات پرداخت و ادامه"
|
||||||
[loading]="submitOrderLoading()"
|
[loading]="submitOrderLoading()"
|
||||||
(onCancel)="close()"
|
(onCancel)="close()"
|
||||||
(onSubmit)="submit()"
|
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Component, computed, inject, signal } from '@angular/core';
|
|||||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { ButtonDirective } from 'primeng/button';
|
import { ButtonDirective } from 'primeng/button';
|
||||||
import { Dialog } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
import { Observable } from 'rxjs';
|
import { catchError, throwError } from 'rxjs';
|
||||||
import { IPayment, TOrderPaymentTypes } from '../../models';
|
import { IPayment, TOrderPaymentTypes } from '../../models';
|
||||||
import { PosLandingStore } from '../../store/main.store';
|
import { PosLandingStore } from '../../store/main.store';
|
||||||
|
|
||||||
@@ -90,6 +90,8 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override showSuccessMessage = false;
|
||||||
|
|
||||||
override submitForm() {
|
override submitForm() {
|
||||||
if (this.remainedAmount() > 0) {
|
if (this.remainedAmount() > 0) {
|
||||||
return this.toastServices.warn({
|
return this.toastServices.warn({
|
||||||
@@ -100,16 +102,19 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
|||||||
const payment = this.form.value as IPayment;
|
const payment = this.form.value as IPayment;
|
||||||
this.store.setPayment(payment);
|
this.store.setPayment(payment);
|
||||||
|
|
||||||
this.store.submitOrder().then((res) => {
|
this.store
|
||||||
if (res) {
|
.submitOrder()
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
return throwError(() => err);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
this.close();
|
this.close();
|
||||||
this.toastServices.success({
|
this.toastServices.success({
|
||||||
text: 'فاکتور شما با موفقیت ایجاد شد',
|
text: 'فاکتور شما با موفقیت ایجاد شد',
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return new Observable<IPayment>((observer) => observer.next(payment));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override onSuccess(response: IPayment): void {}
|
override onSuccess(response: IPayment): void {}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ export interface IUnknownCustomer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ICustomer {
|
export interface ICustomer {
|
||||||
customerIndividuals?: IIndividualCustomer;
|
customer_individual?: IIndividualCustomer;
|
||||||
customerLegals?: ILegalCustomer;
|
customer_legal?: ILegalCustomer;
|
||||||
customerUnknown?: IUnknownCustomer;
|
customer_unknown?: IUnknownCustomer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCustomerInfo = IIndividualCustomer | ILegalCustomer | IUnknownCustomer;
|
export type TCustomerInfo = IIndividualCustomer | ILegalCustomer | IUnknownCustomer;
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ export interface IPosOrderRequest {
|
|||||||
items: IPosOrderItem[];
|
items: IPosOrderItem[];
|
||||||
notes?: string;
|
notes?: string;
|
||||||
customer_type?: CustomerType;
|
customer_type?: CustomerType;
|
||||||
customerId?: string;
|
customer_id?: string;
|
||||||
customer?: ICustomer;
|
customer?: ICustomer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good.
|
|||||||
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
||||||
import { createUUID, JALALI_DATE_FORMATS, nowJalali, parseJalali } from '@/utils';
|
import { createUUID, JALALI_DATE_FORMATS, nowJalali, parseJalali } from '@/utils';
|
||||||
import { computed, inject, Injectable, signal } from '@angular/core';
|
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 { ICustomer, IPayment, IPosOrderRequest, IPosOrderResponse } from '../models';
|
||||||
import { IPosInOrderGood, IPosOrderItem } from '../models/types';
|
import { IPosInOrderGood, IPosOrderItem } from '../models/types';
|
||||||
import { PosService } from '../services/main.service';
|
import { PosService } from '../services/main.service';
|
||||||
@@ -208,10 +208,10 @@ export class PosLandingStore {
|
|||||||
this.setState({ payments });
|
this.setState({ payments });
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitOrder() {
|
submitOrder() {
|
||||||
this.setState({ submitOrderLoading: true });
|
this.setState({ submitOrderLoading: true });
|
||||||
const orderPayload: IPosOrderRequest = {
|
const orderPayload: IPosOrderRequest = {
|
||||||
customerId: this.customer().customer_id,
|
customer_id: this.customer().customer_id,
|
||||||
customer_type: this.customer().type,
|
customer_type: this.customer().type,
|
||||||
customer: this.customer().info,
|
customer: this.customer().info,
|
||||||
items: this.inOrderGoods().map(({ good, ...rest }) => ({
|
items: this.inOrderGoods().map(({ good, ...rest }) => ({
|
||||||
@@ -226,24 +226,30 @@ export class PosLandingStore {
|
|||||||
|
|
||||||
let res: Maybe<IPosOrderResponse> = null;
|
let res: Maybe<IPosOrderResponse> = null;
|
||||||
|
|
||||||
await this.service
|
return this.service.submitOrder(orderPayload).pipe(
|
||||||
.submitOrder(orderPayload)
|
finalize(() => {
|
||||||
.pipe(
|
this.setState({ submitOrderLoading: false });
|
||||||
finalize(() => {
|
}),
|
||||||
this.setState({ submitOrderLoading: false });
|
catchError(() => {
|
||||||
}),
|
return new Observable((observer) => observer.error());
|
||||||
catchError(() => {
|
}),
|
||||||
return throwError('');
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe((_res) => {
|
|
||||||
this.reset();
|
|
||||||
res = _res;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
map((_res) => {
|
||||||
return new Observable<IPosOrderResponse>((observer) => observer.next(res!));
|
// this.setState({
|
||||||
}
|
// inOrderGoods: [],
|
||||||
return new Observable((observer) => observer.error());
|
// customerDetails: {
|
||||||
|
// type: CustomerType.UNKNOWN,
|
||||||
|
// },
|
||||||
|
// invoiceDate: nowJalali(JALALI_DATE_FORMATS.NUMERIC),
|
||||||
|
// payments: {
|
||||||
|
// cash: 0,
|
||||||
|
// set_off: 0,
|
||||||
|
// terminal: 0,
|
||||||
|
// },
|
||||||
|
// orderNote: '',
|
||||||
|
// });
|
||||||
|
return _res;
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export class POSStore {
|
|||||||
|
|
||||||
submitOrder() {
|
submitOrder() {
|
||||||
const orderPayload = {
|
const orderPayload = {
|
||||||
customerId: this.selectedCustomer()?.id,
|
customer_id: this.selectedCustomer()?.id,
|
||||||
items: this.inOrderProducts()?.map((item) => ({
|
items: this.inOrderProducts()?.map((item) => ({
|
||||||
productId: item.productId,
|
productId: item.productId,
|
||||||
count: item.count,
|
count: item.count,
|
||||||
|
|||||||
@@ -184,8 +184,6 @@ export class InputComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPriceInput($event: InputNumberInputEvent) {
|
onPriceInput($event: InputNumberInputEvent) {
|
||||||
console.log($event);
|
|
||||||
|
|
||||||
this.onInput($event.originalEvent, true);
|
this.onInput($event.originalEvent, true);
|
||||||
}
|
}
|
||||||
onInput($event: Event, isPriceFormat?: boolean) {
|
onInput($event: Event, isPriceFormat?: boolean) {
|
||||||
@@ -198,7 +196,6 @@ export class InputComponent {
|
|||||||
|
|
||||||
if (this.inputMode === 'numeric') {
|
if (this.inputMode === 'numeric') {
|
||||||
let newValue = parseFloat(value);
|
let newValue = parseFloat(value);
|
||||||
// console.log(value, newValue);
|
|
||||||
|
|
||||||
const minValidator = (this.min || this.min === 0) && parseFloat(value) < this.min!;
|
const minValidator = (this.min || this.min === 0) && parseFloat(value) < this.min!;
|
||||||
const maxValidator = (this.max || this.max === 0) && parseFloat(value) > this.max;
|
const maxValidator = (this.max || this.max === 0) && parseFloat(value) > this.max;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export class KeyValueComponent {
|
|||||||
}
|
}
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case 'simple':
|
case 'simple':
|
||||||
return this.value || '';
|
return this.value || '-';
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface IColumn<T = any> {
|
|||||||
width?: string;
|
width?: string;
|
||||||
minWidth?: string;
|
minWidth?: string;
|
||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
type?: 'text' | 'price' | 'boolean' | 'date' | 'nested' | 'index';
|
type?: 'text' | 'price' | 'boolean' | 'date' | 'nested' | 'index' | 'id';
|
||||||
nestedPath?: string;
|
nestedPath?: string;
|
||||||
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
|
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
|
||||||
}
|
}
|
||||||
@@ -139,6 +139,9 @@ export class PageDataListComponent<I> {
|
|||||||
}
|
}
|
||||||
const data = item[String(field)];
|
const data = item[String(field)];
|
||||||
switch (column.type) {
|
switch (column.type) {
|
||||||
|
case 'id':
|
||||||
|
if (!data) return '-';
|
||||||
|
return `${data.slice(0, 5)}...${data.slice(data.length - 5, data.length)}`;
|
||||||
case 'date':
|
case 'date':
|
||||||
if (!data) return '-';
|
if (!data) return '-';
|
||||||
return formatJalali(data);
|
return formatJalali(data);
|
||||||
|
|||||||
Reference in New Issue
Block a user