update many things

This commit is contained in:
2026-05-04 20:02:10 +03:30
parent 797aecd489
commit ec452bca22
72 changed files with 1375 additions and 606 deletions
+2
View File
@@ -0,0 +1,2 @@
TENANT=default
DIST_DIR=default
+2
View File
@@ -0,0 +1,2 @@
TENANT=tis
DIST_DIR=tis
+16 -8
View File
@@ -1,16 +1,15 @@
# Build stage # ---------- Build stage ----------
FROM node:20 AS builder FROM node:20-alpine AS builder
ARG TENANT=tis ARG TENANT=tis
ARG DIST_DIR=tis ARG DIST_DIR=tis
WORKDIR /app WORKDIR /app
# RUN npm config set registry "https://hub.megan.ir/npm/"
RUN npm install -g pnpm RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml ./
RUN pnpm install RUN pnpm install --frozen-lockfile
COPY . . COPY . .
@@ -20,14 +19,23 @@ RUN if [ "$TENANT" = "default" ]; then \
pnpm run build:$TENANT; \ pnpm run build:$TENANT; \
fi fi
# ---------- Runtime stage ----------
FROM nginx:alpine FROM nginx:alpine
# Remove default config
RUN rm -rf /etc/nginx/conf.d/*
# Copy optimized config
COPY nginx.conf /etc/nginx/conf.d/default.conf
ARG DIST_DIR=tis ARG DIST_DIR=tis
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist/${DIST_DIR}/browser /usr/share/nginx/html COPY --from=builder /app/dist/${DIST_DIR}/browser /usr/share/nginx/html
EXPOSE 5000 # Optional: reduce image size
# RUN apk add --no-cache brotli
EXPOSE 8090
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+46 -1
View File
@@ -10,6 +10,43 @@
"build": { "build": {
"builder": "@angular/build:application", "builder": "@angular/build:application",
"configurations": { "configurations": {
"default": {
"assets": [
{
"glob": "**/*",
"input": "public-default"
}
],
"budgets": [
{
"maximumError": "3MB",
"maximumWarning": "2MB",
"type": "initial"
},
{
"maximumError": "8kB",
"maximumWarning": "4kB",
"type": "anyComponentStyle"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.default.ts"
},
{
"replace": "src/app.routes.ts",
"with": "src/tenants/default/app.routes.ts"
},
{
"replace": "src/app/branding/branding.config.ts",
"with": "src/tenants/default/branding.config.ts"
}
],
"outputHashing": "all",
"outputPath": "dist/default",
"serviceWorker": "ngsw-config.json"
},
"development": { "development": {
"extractLicenses": false, "extractLicenses": false,
"optimization": false, "optimization": false,
@@ -32,10 +69,18 @@
{ {
"replace": "src/environments/environment.ts", "replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts" "with": "src/environments/environment.prod.ts"
},
{
"replace": "src/app.routes.ts",
"with": "src/tenants/default/app.routes.ts"
},
{
"replace": "src/app/branding/branding.config.ts",
"with": "src/tenants/tis/branding.config.ts"
} }
], ],
"outputHashing": "all", "outputHashing": "all",
"outputPath": "dist/pos.client", "outputPath": "dist/production",
"serviceWorker": "ngsw-config.json" "serviceWorker": "ngsw-config.json"
}, },
"staging": { "staging": {
+40 -61
View File
@@ -1,72 +1,51 @@
version: "3.8" version: "3.8"
services: services:
# Production service app_tis:
app:
platform: ${DOCKER_PLATFORM:-linux/amd64}
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
TENANT: ${TENANT:-tis} TENANT: tis
DIST_DIR: ${DIST_DIR:-tis} DIST_DIR: tis
container_name: psp_panel_prod
ports:
- "5000:5000"
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5000/" ]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
networks:
- psp_panel_network
# Staging service
# staging:
# build:
# context: .
# dockerfile: Dockerfile.staging
# container_name: psp_panel_staging
# ports:
# - "5050:5000"
# 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:
# - psp_panel_network
# # Development service with hot reload ports:
# dev: - "8090:8090"
# build: restart: unless-stopped
# context: .
# dockerfile: Dockerfile.dev app_default:
# container_name: psp_panel-dev build:
# working_dir: /app context: .
# volumes: dockerfile: Dockerfile
# - .:/app args:
# - /app/node_modules TENANT: default
# ports: DIST_DIR: default
# - "4200:4200"
# environment: ports:
# - NODE_ENV=development - "8090:8090"
# restart: unless-stopped restart: unless-stopped
# profiles:
# - dev # labels:
# networks: # - "traefik.enable=true"
# - psp_panel_network
# # 🌐 Router
# - "traefik.http.routers.psp.rule=Host(`your-domain.com`)"
# - "traefik.http.routers.psp.entrypoints=websecure"
# - "traefik.http.routers.psp.tls=true"
# # 🔐 SSL (auto with Traefik)
# - "traefik.http.routers.psp.tls.certresolver=letsencrypt"
# # ⚡ Service port (IMPORTANT)
# - "traefik.http.services.psp.loadbalancer.server.port=80"
# # 🚀 Compression at edge (optional)
# - "traefik.http.middlewares.compress.compress=true"
# - "traefik.http.routers.psp.middlewares=compress@docker"
networks:
- web
networks: networks:
psp_panel_network: web:
driver: bridge driver: bridge
+16 -68
View File
@@ -1,80 +1,28 @@
user nginx; server {
worker_processes auto; listen 8090;
error_log /var/log/nginx/error.log warn; server_name _;
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 5000;
server_name localhost;
charset utf-8;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html;
# Security headers # 🔥 Gzip
add_header X-Frame-Options "SAMEORIGIN" always; gzip on;
add_header X-Content-Type-Options "nosniff" always; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
add_header X-XSS-Protection "1; mode=block" always; gzip_comp_level 6;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Cache control for static assets # 🔥 Brotli (better than gzip)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { # brotli on;
# brotli_comp_level 6;
# brotli_types text/plain text/css application/javascript application/json image/svg+xml;
# 🔥 Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
} }
# Service worker files should be revalidated on each request # 🔥 Angular routing (SPA fallback)
location = /ngsw.json {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
location = /ngsw-worker.js {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Angular routing - serve index.html for all routes
location / { location / {
try_files $uri $uri/ /index.html; 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";
}
}
} }
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "./node_modules/@angular/service-worker/config/schema.json", "$schema": "./node_modules/@angular/service-worker/config/schema.json",
"appData": { "appData": {
"appVersion": "0.0.0", "appVersion": "0.0.0",
"buildDate": "2026-04-27T06:02:22.249Z" "buildDate": "2026-05-03T16:17:50.761Z"
}, },
"assetGroups": [ "assetGroups": [
{ {
+1 -1
View File
@@ -78,7 +78,7 @@ Displays: name, pos_type, serial_number, device, model, provider (6 fields - sam
| `variant` | 'consumer' \| 'partner' \| 'full' | 'full' | Display variant mode | | `variant` | 'consumer' \| 'partner' \| 'full' | 'full' | Display variant mode |
| `pos` | Signal<IPosEntity \| undefined> | undefined | POS entity data | | `pos` | Signal<IPosEntity \| undefined> | undefined | POS entity data |
| `editMode` | Signal<boolean> | false | Edit mode state | | `editMode` | Signal<boolean> | false | Edit mode state |
| `cardTitle` | string | 'اطلاعات پایانه‌ی فروش' | Card header title | | `cardTitle` | string | 'اطلاعات پایانه‌ فروش' | Card header title |
| `showMoreActions` | boolean | true | Show more actions button | | `showMoreActions` | boolean | true | Show more actions button |
## Outputs ## Outputs
@@ -14,7 +14,7 @@ export class PosDisplayComponent {
@Input() variant: PosVariant = 'full'; @Input() variant: PosVariant = 'full';
@Input() pos = signal<IPosEntity | undefined>(undefined); @Input() pos = signal<IPosEntity | undefined>(undefined);
@Input() editMode = signal<boolean>(false); @Input() editMode = signal<boolean>(false);
@Input() cardTitle = 'اطلاعات پایانه‌ی فروش'; @Input() cardTitle = 'اطلاعات پایانه‌ فروش';
@Input() showMoreActions = true; @Input() showMoreActions = true;
@Output() editModeChange = new EventEmitter<boolean>(); @Output() editModeChange = new EventEmitter<boolean>();
@@ -4,24 +4,32 @@
<uikit-empty-state> </uikit-empty-state> <uikit-empty-state> </uikit-empty-state>
} @else { } @else {
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<app-card-data cardTitle="اطلاعات فاکتور" [editable]="false"> <app-card-data cardTitle="اطلاعات فاکتور" [editable]="false" [backRoute]="backRoute">
<ng-template #moreActions> <ng-template #moreActions>
<button pButton type="button" label="چاپ" icon="pi pi-print" outlined (click)="printInvoice()"></button> <button
pButton
type="button"
label="چاپ"
icon="pi pi-print"
outlined
size="small"
(click)="printInvoice()"
></button>
</ng-template> </ng-template>
<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="listKeyValue">
<app-key-value label="کد رهگیری" [value]="invoice.code" /> <app-key-value label="کد رهگیری" [value]="invoice.code" />
<app-key-value label="تاریخ فاکتور" [value]="invoice.invoice_date" type="dateTime" /> <app-key-value label="تاریخ فاکتور" [value]="invoice.invoice_date" type="dateTime" />
<app-key-value label="تاریخ ایجاد فاکتور" [value]="invoice.created_at" type="dateTime" /> <app-key-value label="تاریخ ایجاد فاکتور" [value]="invoice.created_at" type="dateTime" />
<app-key-value label="وضعیت صدور به سامانه‌ی مودیان"> <app-key-value label="وضعیت صدور به سامانه‌ی مودیان">
<catalog-tax-provider-status-tag [status]="invoice.status.value" [translate]="invoice.status.translate" /> <catalog-tax-provider-status-tag [status]="invoice.status.value" [translate]="invoice.status.translate" />
</app-key-value> </app-key-value>
<div class="col-span-3"> <div class="col-span-full">
<app-key-value label="توضیحات" [value]="invoice.notes" /> <app-key-value label="توضیحات" [value]="invoice.notes" />
</div> </div>
</div> </div>
<p-divider align="center"> اطلاعات پرداخت </p-divider> <p-divider align="center"> اطلاعات پرداخت </p-divider>
<div class="grid grid-cols-3 gap-4 items-center"> <div class="grid md:grid-cols-3 sm:grid-cols-2 sm:gap-4 gap-3 items-center">
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" /> <app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
@for (payment of invoice.payments; track $index) { @for (payment of invoice.payments; track $index) {
<app-key-value <app-key-value
@@ -33,7 +41,7 @@
</div> </div>
<p-divider align="center"> اطلاعات صادر کننده </p-divider> <p-divider align="center"> اطلاعات صادر کننده </p-divider>
<div class="grid grid-cols-3 gap-4 items-center"> <div class="listKeyValue">
<app-key-value label="کسب و کار" [value]="invoice.pos!.complex.business_activity.name" /> <app-key-value label="کسب و کار" [value]="invoice.pos!.complex.business_activity.name" />
<app-key-value label="مجموعه" [value]="invoice.pos!.complex.name" /> <app-key-value label="مجموعه" [value]="invoice.pos!.complex.name" />
<app-key-value label="پایانه‌ی فروش" [value]="invoice.pos!.name" /> <app-key-value label="پایانه‌ی فروش" [value]="invoice.pos!.name" />
@@ -43,7 +51,7 @@
@if (variant !== "customer") { @if (variant !== "customer") {
<p-divider align="center"> اطلاعات مشتری </p-divider> <p-divider align="center"> اطلاعات مشتری </p-divider>
@if (invoice.customer) { @if (invoice.customer) {
<div class="grid grid-cols-3 gap-4 items-center"> <div class="listKeyValue">
@if (invoice.customer.type === "INDIVIDUAL") { @if (invoice.customer.type === "INDIVIDUAL") {
<app-key-value label="نوع مشتری" value="حقیقی" /> <app-key-value label="نوع مشتری" value="حقیقی" />
<app-key-value label="نام" [value]="invoice.customer.individual?.first_name" /> <app-key-value label="نام" [value]="invoice.customer.individual?.first_name" />
@@ -60,7 +68,7 @@
} }
</div> </div>
} @else if (invoice.unknown_customer) { } @else if (invoice.unknown_customer) {
<div class="grid grid-cols-3 gap-4 items-center"> <div class="listKeyValue">
<app-key-value label="نوع مشتری" value="نوع دوم" /> <app-key-value label="نوع مشتری" value="نوع دوم" />
<app-key-value label="عنوان" [value]="invoice.unknown_customer.name" /> <app-key-value label="عنوان" [value]="invoice.unknown_customer.name" />
<app-key-value label="کد ثبتی" [value]="invoice.unknown_customer.national_code" /> <app-key-value label="کد ثبتی" [value]="invoice.unknown_customer.national_code" />
@@ -19,6 +19,7 @@ import {
TemplateRef, TemplateRef,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { UrlTree } from '@angular/router';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Divider } from 'primeng/divider'; import { Divider } from 'primeng/divider';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
@@ -48,6 +49,7 @@ export class ConsumerSaleInvoiceSharedComponent {
@Input({ required: true }) loading!: boolean; @Input({ required: true }) loading!: boolean;
@Input() invoice?: Maybe<ISaleInvoiceFullResponse>; @Input() invoice?: Maybe<ISaleInvoiceFullResponse>;
@Input() variant: TConsumerSaleInvoice = 'full'; @Input() variant: TConsumerSaleInvoice = 'full';
@Input() backRoute?: UrlTree | string | any[];
@Output() onRefresh = new EventEmitter<void>(); @Output() onRefresh = new EventEmitter<void>();
@@ -21,7 +21,7 @@ export class ConsumerBusinessActivityGoodsListComponent extends AbstractList<ICo
@Input() header: IColumn[] = [ @Input() header: IColumn[] = [
{ field: 'id', header: 'شناسه', type: 'id' }, { field: 'id', header: 'شناسه', type: 'id' },
{ field: 'name', header: 'عنوان' }, { field: 'name', header: 'عنوان' },
{ field: 'sku', header: 'شناسه‌ی عمومی' }, { field: 'sku', header: 'شناسه‌ عمومی' },
{ {
field: 'category', field: 'category',
header: 'دسته‌بندی', header: 'دسته‌بندی',
@@ -1,10 +1,10 @@
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<app-card-data cardTitle="اطلاعات پایانه‌ی فروش" [editable]="true" [(editMode)]="editMode"> <app-card-data cardTitle="اطلاعات پایانه‌ فروش" [editable]="true" [(editMode)]="editMode">
<ng-template #moreActions> <ng-template #moreActions>
<p-button type="button" variant="outlined" (onClick)="toPosLanding()"> ورود به پایانه </p-button> <p-button type="button" variant="outlined" size="small" (onClick)="toPosLanding()"> ورود به پایانه </p-button>
</ng-template> </ng-template>
<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="listKeyValue">
<app-key-value label="عنوان" [value]="pos()?.name" /> <app-key-value label="عنوان" [value]="pos()?.name" />
<app-key-value label="نوع پایانه" [value]="pos()?.pos_type" /> <app-key-value label="نوع پایانه" [value]="pos()?.pos_type" />
<app-key-value label="شماره سریال" [value]="pos()?.serial_number" /> <app-key-value label="شماره سریال" [value]="pos()?.serial_number" />
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="آخرین فاکتورهای صادر شده‌ی امروز" pageTitle="آخرین فاکتورهای صادر شده امروز"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="تا به این لحظه فاکتوری صادر نشده است." emptyPlaceholderTitle="تا به این لحظه فاکتوری صادر نشده است."
[items]="items()" [items]="items()"
@@ -1,10 +1,10 @@
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<app-card-data cardTitle="اطلاعات پایانه‌ی فروش" [editable]="true" [(editMode)]="editMode"> <app-card-data cardTitle="اطلاعات پایانه‌ فروش" [editable]="true" [(editMode)]="editMode">
<ng-template #moreActions> <ng-template #moreActions>
<p-button type="button" variant="outlined" (onClick)="toPosLanding()"> ورود به پایانه </p-button> <p-button type="button" variant="outlined" size="small" (onClick)="toPosLanding()"> ورود به پایانه </p-button>
</ng-template> </ng-template>
<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="listKeyValue">
<app-key-value label="عنوان" [value]="pos()?.name" /> <app-key-value label="عنوان" [value]="pos()?.name" />
<app-key-value label="نوع پایانه" [value]="pos()?.pos_type" /> <app-key-value label="نوع پایانه" [value]="pos()?.pos_type" />
<app-key-value label="شماره سریال" [value]="pos()?.serial_number" /> <app-key-value label="شماره سریال" [value]="pos()?.serial_number" />
@@ -13,7 +13,7 @@
name="starts_at" name="starts_at"
hint="مدت زمان لایسنس‌ها ۱ ساله هستند." hint="مدت زمان لایسنس‌ها ۱ ساله هستند."
/> />
<!-- <uikit-datepicker label="تاریخ انقضای لایسنس" [control]="form.controls.expires_at" name="expires_at" /> --> <!-- <uikit-datepicker label="تاریخ انقضا لایسنس" [control]="form.controls.expires_at" name="expires_at" /> -->
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
</shared-dialog> </shared-dialog>
@@ -8,7 +8,7 @@
<app-key-value label="عنوان" [value]="businessActivity()?.name" /> <app-key-value label="عنوان" [value]="businessActivity()?.name" />
<app-key-value label="کد اقتصادی" [value]="businessActivity()?.economic_code" /> <app-key-value label="کد اقتصادی" [value]="businessActivity()?.economic_code" />
<app-key-value label="صنف" [value]="businessActivity()?.guild?.name" /> <app-key-value label="صنف" [value]="businessActivity()?.guild?.name" />
<app-key-value label="تاریخ انقضای مجوز" [value]="businessActivity()?.license_info?.expires_at" type="date" /> <app-key-value label="تاریخ انقضا مجوز" [value]="businessActivity()?.license_info?.expires_at" type="date" />
<app-key-value label="تعداد کاربران مجاز" [value]="businessActivity()?.license_info?.accounts_limit" /> <app-key-value label="تعداد کاربران مجاز" [value]="businessActivity()?.license_info?.accounts_limit" />
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<app-card-data cardTitle="اطلاعات پایانه‌ی فروش" [editable]="true" [(editMode)]="editMode"> <app-card-data cardTitle="اطلاعات پایانه‌ فروش" [editable]="true" [(editMode)]="editMode">
<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" />
@@ -1,8 +1,9 @@
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-input [control]="form.controls.first_name" name="first_name" label="نام" /> <field-first-name [control]="form.controls.first_name" />
<app-input [control]="form.controls.last_name" name="last_name" label="نام خانوادگی" /> <field-last-name [control]="form.controls.last_name" />
<app-input [control]="form.controls.national_id" name="national_id" label="کد ملی" type="nationalId" /> <field-mobile-number [control]="form.controls.mobile_number" />
<app-input [control]="form.controls.economic_code" name="economic_code" label="کد اقتصادی" maxlength="11" /> <field-national-id [control]="form.controls.national_id" />
<field-economic-code [control]="form.controls.economic_code" />
<app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" (onSubmit)="submit()" /> <app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" (onSubmit)="submit()" />
</form> </form>
@@ -1,11 +1,16 @@
import { postalCodeValidator } from '@/core/validators';
import { nationalIdValidator } from '@/core/validators/national-id.validator';
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components'; import {
EconomicCodeComponent,
FirstNameComponent,
LastNameComponent,
MobileNumberComponent,
NationalIdComponent,
} from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { fieldControl } from '@/shared/constants';
import { CustomerType } from '@/shared/localEnum/constants/customerTypes'; import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
import { Component, inject } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { IIndividualCustomer } from '../../../models/customer'; import { IIndividualCustomer } from '../../../models/customer';
import { PosLandingStore } from '../../../store/main.store'; import { PosLandingStore } from '../../../store/main.store';
@@ -13,36 +18,31 @@ import { PosLandingStore } from '../../../store/main.store';
@Component({ @Component({
selector: 'pos-customer-individual-form', selector: 'pos-customer-individual-form',
templateUrl: './form.component.html', templateUrl: './form.component.html',
imports: [ReactiveFormsModule, InputComponent, FormFooterActionsComponent], imports: [
ReactiveFormsModule,
FormFooterActionsComponent,
FirstNameComponent,
LastNameComponent,
NationalIdComponent,
EconomicCodeComponent,
MobileNumberComponent,
],
}) })
export class CustomerIndividualFormComponent extends AbstractForm< export class CustomerIndividualFormComponent extends AbstractForm<
IIndividualCustomer, IIndividualCustomer,
IIndividualCustomer IIndividualCustomer
> { > {
private readonly store = inject(PosLandingStore); private readonly store = inject(PosLandingStore);
costumerInfo = computed(() => this.store.customer().info?.customer_individual);
override showSuccessMessage = false; override showSuccessMessage = false;
form = this.fb.group({ form = this.fb.group({
first_name: [ first_name: fieldControl.first_name(this.costumerInfo()?.first_name || ''),
this.store.customer().info?.customer_individual?.first_name || '', last_name: fieldControl.last_name(this.costumerInfo()?.last_name || ''),
[Validators.required], national_id: fieldControl.national_id(this.costumerInfo()?.national_id || ''),
], postal_code: fieldControl.postal_code(this.costumerInfo()?.postal_code || ''),
last_name: [ economic_code: fieldControl.economic_code(this.costumerInfo()?.economic_code || ''),
this.store.customer().info?.customer_individual?.last_name || '', mobile_number: fieldControl.mobile_number(this.costumerInfo()?.mobile_number || ''),
[Validators.required],
],
national_id: [
this.store.customer().info?.customer_individual?.national_id || '',
[Validators.required, nationalIdValidator()],
],
economic_code: [
this.store.customer().info?.customer_individual?.economic_code || '',
[Validators.required],
],
postal_code: [
this.store.customer().info?.customer_individual?.postal_code || '',
[postalCodeValidator()],
],
}); });
override submitForm() { override submitForm() {
@@ -1,5 +1,5 @@
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-input [control]="form.controls.name" name="name" label="نام مشتری" />
<app-input [control]="form.controls.national_id" name="national_id" label="شماره ملی" type="nationalId" /> <app-input [control]="form.controls.national_id" name="national_id" label="شماره ملی" type="nationalId" />
<app-input [control]="form.controls.name" name="name" label="نام" />
<app-form-footer-actions submitLabel="ثبت بدون اطلاعات خریدار و ادامه" (onCancel)="close()" /> <app-form-footer-actions submitLabel="ثبت بدون اطلاعات خریدار و ادامه" (onCancel)="close()" />
</form> </form>
@@ -12,7 +12,7 @@
<button <button
pButton pButton
type="button" type="button"
icon="pi pi-refresh" icon="pi pi-trash"
outlined outlined
severity="danger" severity="danger"
size="small" size="small"
@@ -75,6 +75,15 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
this.updateCalculateAmount(value as any); this.updateCalculateAmount(value as any);
}); });
form.controls.payload.controls.profit_amount.valueChanges.subscribe((value) => {
// if ((form.controls.discount_amount.value || 0) > (value || 0)) {
form.controls.discount_amount.setValue(0);
// }
});
form.controls.payload.controls.profit_percentage.valueChanges.subscribe((value) => {
form.controls.discount_amount.setValue(0);
});
form.setValue({ form.setValue({
unit_price: 200_000_000, unit_price: 200_000_000,
quantity: 2, quantity: 2,
@@ -3,6 +3,7 @@ export interface IIndividualCustomer {
last_name: string; last_name: string;
national_id: string; national_id: string;
postal_code: string; postal_code: string;
mobile_number: string;
economic_code: string; economic_code: string;
is_favorite?: boolean; is_favorite?: boolean;
} }
@@ -5,10 +5,11 @@
<p-button <p-button
icon="pi pi-filter" icon="pi pi-filter"
type="button" type="button"
size="small"
[outlined]="!activeFilters().length" [outlined]="!activeFilters().length"
(click)="toggleFilterVisible()" (click)="toggleFilterVisible()"
></p-button> ></p-button>
<button pButton icon="pi pi-refresh" type="button" outlined (click)="refresh()"></button> <button pButton icon="pi pi-refresh" type="button" size="small" outlined (click)="refresh()"></button>
</div> </div>
</ng-template> </ng-template>
</app-inner-pages-header> </app-inner-pages-header>
@@ -14,19 +14,43 @@
</div> </div>
<hr /> <hr />
<div class="flex items-center justify-center gap-4"> <div class="flex items-center justify-center gap-4">
@if (["success", "failure"].includes(saleInvoice.status.value.toLowerCase())) {
<p-button
label="ابطال"
type="button"
severity="danger"
(click)="revokeInvoice()"
[loading]="revokingInvoiceLoading()"
[disabled]="onAction()"
></p-button>
}
@if (saleInvoice.status.value.toLowerCase() === "not_send") { @if (saleInvoice.status.value.toLowerCase() === "not_send") {
<p-button label="ارسال فاکتور" type="button" (click)="sendInvoice()" [loading]="sendingLoading()"></p-button> <p-button
label="ارسال فاکتور"
type="button"
(click)="sendInvoice()"
[loading]="sendingLoading()"
[disabled]="onAction()"
></p-button>
} }
@if (saleInvoice.status.value.toLowerCase() === "queued") { @if (saleInvoice.status.value.toLowerCase() === "queued") {
<p-button <p-button
label="استعلام وضعیت ارسال" label="استعلام وضعیت ارسال"
type="button" type="button"
severity="info"
(click)="getStatus()" (click)="getStatus()"
[loading]="gettingStatusLoading()" [loading]="gettingStatusLoading()"
[disabled]="onAction()"
></p-button> ></p-button>
} }
@if (saleInvoice.status.value.toLowerCase() === "failure") { @if (saleInvoice.status.value.toLowerCase() === "failure") {
<p-button label="ارسال فاکتور" type="button" (click)="resendInvoice()" [loading]="resendingLoading()"></p-button> <p-button
label="ارسال مجدد"
type="button"
(click)="resendInvoice()"
[loading]="resendingLoading()"
[disabled]="onAction()"
></p-button>
} }
<a [routerLink]="singlePageRoute()" pButton type="button" (click)="viewDetails()" outlined>مشاهده جزییات</a> <a [routerLink]="singlePageRoute()" pButton type="button" (click)="viewDetails()" outlined>مشاهده جزییات</a>
</div> </div>
@@ -5,7 +5,6 @@ import { Component, computed, EventEmitter, inject, Input, Output, signal } from
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { Button, ButtonDirective } from 'primeng/button'; import { Button, ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import { Tag } from 'primeng/tag';
import { finalize } from 'rxjs'; import { finalize } from 'rxjs';
import { posSaleInvoicesNamedRoutes } from '../constants'; import { posSaleInvoicesNamedRoutes } from '../constants';
import { IPosSaleInvoicesSummaryResponse } from '../models'; import { IPosSaleInvoicesSummaryResponse } from '../models';
@@ -17,7 +16,6 @@ import { PosSaleInvoicesService } from '../services/main.service';
imports: [ imports: [
Button, Button,
KeyValueComponent, KeyValueComponent,
Tag,
Card, Card,
RouterLink, RouterLink,
ButtonDirective, ButtonDirective,
@@ -34,6 +32,15 @@ export class SaleInvoiceCardComponent {
sendingLoading = signal(false); sendingLoading = signal(false);
gettingStatusLoading = signal(false); gettingStatusLoading = signal(false);
resendingLoading = signal(false); resendingLoading = signal(false);
revokingInvoiceLoading = signal(false);
onAction = computed(
() =>
this.sendingLoading() ||
this.gettingStatusLoading() ||
this.resendingLoading() ||
this.revokingInvoiceLoading(),
);
singlePageRoute = computed(() => singlePageRoute = computed(() =>
posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.saleInvoice?.id), posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.saleInvoice?.id),
@@ -74,9 +81,6 @@ export class SaleInvoiceCardComponent {
.getInquiry(this.saleInvoice.id) .getInquiry(this.saleInvoice.id)
.pipe(finalize(() => this.gettingStatusLoading.set(false))) .pipe(finalize(() => this.gettingStatusLoading.set(false)))
.subscribe((res) => { .subscribe((res) => {
this.toastService.info({
text: `وضعیت: ${res.status || 'نامشخص'}`,
});
this.refreshRequested.emit(); this.refreshRequested.emit();
}); });
} }
@@ -91,5 +95,16 @@ export class SaleInvoiceCardComponent {
this.refreshRequested.emit(); this.refreshRequested.emit();
}); });
} }
revokeInvoice() {
this.revokingInvoiceLoading.set(true);
this.service
.revoke(this.saleInvoice.id)
.pipe(finalize(() => this.revokingInvoiceLoading.set(false)))
.subscribe(() => {
this.toastService.success({ text: 'ابطال فاکتور با موفقیت انجام شد.' });
this.refreshRequested.emit();
});
}
viewDetails() {} viewDetails() {}
} }
@@ -8,6 +8,7 @@ export const POS_SALE_INVOICES_API_ROUTES = {
retry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/retry`, retry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/retry`,
status: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status`, status: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status`,
getInquiry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/inquiry`, getInquiry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/inquiry`,
revoke: (invoiceId: string) => `${baseUrl()}/${invoiceId}/revoke`,
attempts: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/attempts`, attempts: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/attempts`,
}, },
}; };
@@ -1,4 +1,5 @@
import ISummary from '@/core/models/summary'; import ISummary from '@/core/models/summary';
import { TspProviderResponseStatus } from '@/shared/catalog';
import { IEnumTranslate } from '@/shared/models/enum_translate.type'; import { IEnumTranslate } from '@/shared/models/enum_translate.type';
export interface IPosSaleInvoicesRawResponse { export interface IPosSaleInvoicesRawResponse {
@@ -59,6 +59,13 @@ export class PosSaleInvoicesService {
); );
} }
revoke(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.post<IPosSaleInvoiceFiscalStatusResponse>(
this.apiRoutes.fiscal.revoke(invoiceId),
{},
);
}
getFiscalAttempts(invoiceId: string): Observable<IPosSaleInvoiceFiscalAttemptsResponse> { getFiscalAttempts(invoiceId: string): Observable<IPosSaleInvoiceFiscalAttemptsResponse> {
return this.http.get<IPosSaleInvoiceFiscalAttemptsResponse>( return this.http.get<IPosSaleInvoiceFiscalAttemptsResponse>(
this.apiRoutes.fiscal.attempts(invoiceId), this.apiRoutes.fiscal.attempts(invoiceId),
@@ -1 +1,3 @@
<consumer-saleInvoice-shared [loading]="loading()" [invoice]="invoice()" variant="pos" /> <div class="p-4">
<consumer-saleInvoice-shared [loading]="loading()" [invoice]="invoice()" [backRoute]="backRoute()" variant="pos" />
</div>
@@ -2,6 +2,7 @@ import { ConsumerSaleInvoiceSharedComponent } from '@/domains/consumer/component
import pageParamsUtils from '@/utils/page-params.utils'; import pageParamsUtils from '@/utils/page-params.utils';
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { posSaleInvoicesNamedRoutes } from '../constants';
import { PosSaleInvoiceStore } from '../store/main.store'; import { PosSaleInvoiceStore } from '../store/main.store';
@Component({ @Component({
@@ -19,6 +20,8 @@ export class PosSaleInvoiceComponent {
readonly invoice = computed(() => this.store.entity()); readonly invoice = computed(() => this.store.entity());
readonly loading = computed(() => this.store.loading()); readonly loading = computed(() => this.store.loading());
readonly backRoute = computed(() => posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!());
ngOnInit() { ngOnInit() {
this.store.getData(this.invoiceId()); this.store.getData(this.invoiceId());
} }
@@ -37,7 +37,7 @@ export const SUPER_ADMIN_MENU_ITEMS = [
routerLink: ['/super_admin/guilds'], routerLink: ['/super_admin/guilds'],
}, },
{ {
label: 'شناسه‌ی کالاها', label: 'شناسه کالاها',
icon: 'pi pi-fw pi-good', icon: 'pi pi-fw pi-good',
routerLink: [stockKeepingUnitsNamedRoutes.stockKeepingUnits.meta.pagePath!()], routerLink: [stockKeepingUnitsNamedRoutes.stockKeepingUnits.meta.pagePath!()],
}, },
@@ -25,7 +25,7 @@
name="license.starts_at" name="license.starts_at"
hint="مدت زمان لایسنس‌ها ۱ ساله هستند." hint="مدت زمان لایسنس‌ها ۱ ساله هستند."
/> --> /> -->
<!-- <uikit-datepicker label="تاریخ انقضای لایسنس" [control]="form.controls.license.controls.expires_at" name="license.expires_at" /> --> <!-- <uikit-datepicker label="تاریخ انقضا لایسنس" [control]="form.controls.license.controls.expires_at" name="license.expires_at" /> -->
<!-- <app-partner-select label="شریک تجاری" [control]="form.controls.license.controls.partner_id" /> --> <!-- <app-partner-select label="شریک تجاری" [control]="form.controls.license.controls.partner_id" /> -->
} }
@@ -13,7 +13,7 @@
name="starts_at" name="starts_at"
hint="مدت زمان لایسنس‌ها ۱ ساله هستند." hint="مدت زمان لایسنس‌ها ۱ ساله هستند."
/> />
<!-- <uikit-datepicker label="تاریخ انقضای لایسنس" [control]="form.controls.expires_at" name="expires_at" /> --> <!-- <uikit-datepicker label="تاریخ انقضا لایسنس" [control]="form.controls.expires_at" name="expires_at" /> -->
<app-partner-select label="شریک تجاری" [control]="form.controls.partner_id" [showClear]="true" /> <app-partner-select label="شریک تجاری" [control]="form.controls.partner_id" [showClear]="true" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
@@ -37,7 +37,7 @@ export class ConsumersComponent extends AbstractList<IAdminConsumerResponse> {
}, },
// { // {
// field: 'license_expires_at', // field: 'license_expires_at',
// header: 'تاریخ انقضای لایسنس', // header: 'تاریخ انقضا لایسنس',
// customDataModel(item) { // customDataModel(item) {
// if (item.license_info) { // if (item.license_info) {
// return formatJalali(item.license_info.expires_at); // return formatJalali(item.license_info.expires_at);
@@ -1,5 +1,5 @@
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<app-card-data cardTitle="اطلاعات پایانه‌ی فروش" [editable]="true" [(editMode)]="editMode"> <app-card-data cardTitle="اطلاعات پایانه‌ فروش" [editable]="true" [(editMode)]="editMode">
<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" />
@@ -15,7 +15,7 @@
/> />
} }
<app-input label="عنوان" [control]="form.controls.name" name="name" /> <app-input label="عنوان" [control]="form.controls.name" name="name" />
<field-sku label="شناسه‌ی کالا" [control]="form.controls.sku_id" name="sku_id" /> <field-sku label="شناسه کالا" [control]="form.controls.sku_id" name="sku_id" />
<admin-guild-categories-select <admin-guild-categories-select
[guildId]="guildId" [guildId]="guildId"
label="دسته‌بندی" label="دسته‌بندی"
@@ -20,7 +20,7 @@ export class GuildGoodsListComponent extends AbstractList<IGuildGoodsResponse> {
@Input() header: IColumn[] = [ @Input() header: IColumn[] = [
{ field: 'image_url', header: 'تصویر', type: 'thumbnail' }, { field: 'image_url', header: 'تصویر', type: 'thumbnail' },
{ field: 'name', header: 'عنوان' }, { field: 'name', header: 'عنوان' },
{ field: 'sku', header: 'شناسه‌ی کالا', type: 'nested', nestedOption: { path: 'sku.name' } }, { field: 'sku', header: 'شناسه کالا', type: 'nested', nestedOption: { path: 'sku.name' } },
{ {
field: 'category', field: 'category',
header: 'دسته‌بندی', header: 'دسته‌بندی',
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="گزارش شارژ‌های ثبت شده‌ی کاربر" pageTitle="لیست شارژ‌ کاربر"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="تا به حال شارژ کاربری انجام نشده‌ است" emptyPlaceholderTitle="تا به حال شارژ کاربری انجام نشده‌ است"
[items]="items()" [items]="items()"
@@ -17,15 +17,16 @@ export class AdminPartnerChargeAccountListComponent extends AbstractList<IAdminP
override setColumns(): void { override setColumns(): void {
this.columns = [ this.columns = [
{ field: 'tracking_code', header: 'کد پیگیری' }, { field: 'tracking_code', header: 'کد پیگیری' },
{ field: 'charged_license_count', header: 'تعداد شارژ کاربر' }, { field: 'charged_license_count', header: 'تعداد شارژ کاربر', type: 'number' },
{ field: 'remained_license_count', header: 'شارژ باقی مانده' }, { field: 'remained_license_count', header: 'شارژ باقی مانده', type: 'number' },
{ {
field: 'activation_count', field: 'activation_count',
header: 'مقدار مصرف شده', header: 'مقدار مصرف شده',
type: 'number',
}, },
{ {
field: 'activation_expires_at', field: 'activation_expires_at',
header: 'تاریخ انقضای فروش', header: 'تاریخ انقضا فروش',
type: 'date', type: 'date',
dateOption: { dateOption: {
expiredMode: true, expiredMode: true,
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="گزارش شارژ‌های ثبت شده‌ی لایسنس‌ها" pageTitle="لیست شارژ‌ لایسنس‌"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="تا به حال شارژ لایسنسی انجام نشده‌ است" emptyPlaceholderTitle="تا به حال شارژ لایسنسی انجام نشده‌ است"
[items]="items()" [items]="items()"
@@ -18,15 +18,16 @@ export class AdminPartnerChargeLicenseTransactionListComponent extends AbstractL
override setColumns(): void { override setColumns(): void {
this.columns = [ this.columns = [
{ field: 'tracking_code', header: 'کد پیگیری' }, { field: 'tracking_code', header: 'کد پیگیری' },
{ field: 'charged_license_count', header: 'لایسنس‌های خریداری شده' }, { field: 'charged_license_count', header: 'لایسنس خریداری شده', type: 'number' },
{ field: 'remained_license_count', header: 'لایسنس‌های باقی‌مانده' }, { field: 'remained_license_count', header: 'لایسنس باقی‌مانده', type: 'number' },
{ {
field: 'activation_count', field: 'activation_count',
header: 'لایسنس‌های فروخته شده', header: 'لایسنس فروخته شده',
type: 'number',
}, },
{ {
field: 'activation_expires_at', field: 'activation_expires_at',
header: 'تاریخ انقضای فروش', header: 'تاریخ انقضا فروش',
type: 'date', type: 'date',
dateOption: { dateOption: {
expiredMode: true, expiredMode: true,
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="لیست لایسنس‌های فروخته شده" pageTitle="لیست لایسنس فروخته شده"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="تا به حال لایسنسی فروخته نشده‌ است" emptyPlaceholderTitle="تا به حال لایسنسی فروخته نشده‌ است"
[items]="items()" [items]="items()"
@@ -27,11 +27,11 @@ export class AdminPartnerLicensesComponent extends AbstractList<IPartnerActivate
}, },
{ {
field: 'license', field: 'license',
header: 'تعداد کاربر قابل تعریف', header: 'تعداد کاربر مجاز',
type: 'nested', type: 'nested',
nestedOption: { path: 'license.accounts_limit' }, nestedOption: { path: 'license.accounts_limit', type: 'number' },
}, },
{ field: 'expires_at', header: 'تاریخ انقضای لایسنس', type: 'date' }, { field: 'expires_at', header: 'تاریخ انقضا لایسنس', type: 'date' },
]; ];
} }
@@ -35,7 +35,7 @@ export class PartnersComponent extends AbstractList<IPartnerResponse> {
}, },
{ {
field: 'license_renew', field: 'license_renew',
header: 'تعداد لایسنس‌های تمدیدی', header: 'تعداد لایسنس تمدیدی',
customDataModel: this.licensesRenewStatus, customDataModel: this.licensesRenewStatus,
}, },
{ field: 'status', header: 'وضعیت' }, { field: 'status', header: 'وضعیت' },
@@ -8,12 +8,52 @@
<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]="partner()?.name" /> <app-key-value label="عنوان شریک تجاری" [value]="partner()?.name" />
<app-key-value label="کد شریک تجاری" [value]="partner()?.code" /> <app-key-value label="کد شریک تجاری" [value]="partner()?.code" />
<div class="col-span-3 grid grid-cols-3 gap-4 mt-6">
<partner-license-info-template
title="لایسنس‌ها"
[total]="partner()?.licenses_status?.total || 0"
[expired]="partner()?.licenses_status?.expired || 0"
[activated]="partner()?.licenses_status?.used || 0"
[remained]="
(partner()?.licenses_status?.total || 0) -
(partner()?.licenses_status?.used || 0) -
(partner()?.licenses_status?.expired || 0)
"
/>
<partner-license-info-template
title="شارژ کاربران"
[total]="partner()?.account_quota_status?.total || 0"
[expired]="partner()?.account_quota_status?.expired || 0"
[activated]="partner()?.account_quota_status?.used || 0"
[remained]="
(partner()?.account_quota_status?.total || 0) -
(partner()?.account_quota_status?.used || 0) -
(partner()?.account_quota_status?.expired || 0)
"
/>
<partner-license-info-template
title="تمدید لایسنس‌ها"
[total]="partner()?.license_renew_status?.total || 0"
[expired]="partner()?.license_renew_status?.expired || 0"
[activated]="partner()?.license_renew_status?.used || 0"
[remained]="
(partner()?.license_renew_status?.total || 0) -
(partner()?.license_renew_status?.used || 0) -
(partner()?.license_renew_status?.expired || 0)
"
/>
</div>
<!--
<p-divider align="center" class="col-span-3">اطلاعات لایسنس‌ها</p-divider> <p-divider align="center" class="col-span-3">اطلاعات لایسنس‌ها</p-divider>
<app-key-value label="مجموع لایسنس‌های خریداری شده" [value]="partner()?.licenses_status?.total" /> <app-key-value type="number" label="مجموع لایسنس خریداری شده" [value]="partner()?.licenses_status?.total" />
<app-key-value label="مجموع لایسنس‌های منقضی شده" [value]="partner()?.licenses_status?.expired" /> <app-key-value type="number" label="مجموع لایسنس منقضی شده" [value]="partner()?.licenses_status?.expired" />
<app-key-value label="مجموع لایسنس‌های فروخته شده" [value]="partner()?.licenses_status?.used" /> <app-key-value type="number" label="مجموع لایسنس فروخته شده" [value]="partner()?.licenses_status?.used" />
<app-key-value <app-key-value
label="مجموع لایسنس‌های باقی‌مانده" type="number"
label="مجموع لایسنس باقی‌مانده"
[value]=" [value]="
(partner()?.licenses_status?.total || 0) - (partner()?.licenses_status?.total || 0) -
(partner()?.licenses_status?.used || 0) - (partner()?.licenses_status?.used || 0) -
@@ -21,10 +61,19 @@
" "
/> />
<p-divider align="center" class="col-span-3">اطلاعات شارژ کاربران</p-divider> <p-divider align="center" class="col-span-3">اطلاعات شارژ کاربران</p-divider>
<app-key-value label="مجموع کاربران خریداری شده" [value]="partner()?.account_quota_status?.total" />
<app-key-value label="مجموع کاربران منقضی شده" [value]="partner()?.account_quota_status?.expired" />
<app-key-value label="مجموع کاربران فروخته شده" [value]="partner()?.account_quota_status?.used" />
<app-key-value <app-key-value
type="number"
label="مجموع کاربران خریداری شده"
[value]="partner()?.account_quota_status?.total"
/>
<app-key-value
type="number"
label="مجموع کاربران منقضی شده"
[value]="partner()?.account_quota_status?.expired"
/>
<app-key-value type="number" label="مجموع کاربران فروخته شده" [value]="partner()?.account_quota_status?.used" />
<app-key-value
type="number"
label="مجموع کاربران باقی‌مانده" label="مجموع کاربران باقی‌مانده"
[value]=" [value]="
(partner()?.account_quota_status?.total || 0) - (partner()?.account_quota_status?.total || 0) -
@@ -33,17 +82,18 @@
" "
/> />
<p-divider align="center" class="col-span-3">اطلاعات تمدید لایسنس‌ها</p-divider> <p-divider align="center" class="col-span-3">اطلاعات تمدید لایسنس‌ها</p-divider>
<app-key-value label="مجموع تمدید خریداری شده" [value]="partner()?.license_renew_status?.total" /> <app-key-value type="number" label="مجموع تمدید خریداری شده" [value]="partner()?.license_renew_status?.total" />
<app-key-value label="مجموع تمدید منقضی شده" [value]="partner()?.license_renew_status?.expired" /> <app-key-value type="number" label="مجموع تمدید منقضی شده" [value]="partner()?.license_renew_status?.expired" />
<app-key-value label="مجموع تمدید فروخته شده" [value]="partner()?.license_renew_status?.used" /> <app-key-value type="number" label="مجموع تمدید فروخته شده" [value]="partner()?.license_renew_status?.used" />
<app-key-value <app-key-value
type="number"
label="مجموع تمدید باقی‌مانده" label="مجموع تمدید باقی‌مانده"
[value]=" [value]="
(partner()?.license_renew_status?.total || 0) - (partner()?.license_renew_status?.total || 0) -
(partner()?.license_renew_status?.used || 0) - (partner()?.license_renew_status?.used || 0) -
(partner()?.license_renew_status?.expired || 0) (partner()?.license_renew_status?.expired || 0)
" "
/> />-->
</div> </div>
</div> </div>
</app-card-data> </app-card-data>
@@ -1,9 +1,9 @@
import { BreadcrumbService } from '@/core/services'; import { BreadcrumbService } from '@/core/services';
import { PartnerLicenseInfoTemplateComponent } from '@/domains/partner/modules/dashboard/components/licenseInfo/license-info-template.component';
import { AppCardComponent, KeyValueComponent } from '@/shared/components'; import { AppCardComponent, KeyValueComponent } from '@/shared/components';
import { Component, computed, effect, inject, signal } from '@angular/core'; import { Component, computed, effect, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Button } from 'primeng/button'; import { Button } from 'primeng/button';
import { Divider } from 'primeng/divider';
import { ConsumerAccountListComponent } from '../components/accounts/list.component'; import { ConsumerAccountListComponent } from '../components/accounts/list.component';
import { PartnerChargeLicenseFormDialogComponent } from '../components/charge-license-form-dialog.component'; import { PartnerChargeLicenseFormDialogComponent } from '../components/charge-license-form-dialog.component';
import { AdminPartnerChargeAccountFormDialogComponent } from '../components/chargeAccount/charge-account-form-dialog.component'; import { AdminPartnerChargeAccountFormDialogComponent } from '../components/chargeAccount/charge-account-form-dialog.component';
@@ -28,7 +28,7 @@ import { PartnerStore } from '../store/partner.store';
PartnerChargeLicenseFormDialogComponent, PartnerChargeLicenseFormDialogComponent,
AdminPartnerLicensesComponent, AdminPartnerLicensesComponent,
AdminPartnerChargeLicenseTransactionListComponent, AdminPartnerChargeLicenseTransactionListComponent,
Divider, PartnerLicenseInfoTemplateComponent,
], ],
}) })
export class PartnerComponent { export class PartnerComponent {
@@ -1,10 +1,10 @@
<app-page-data-list <app-page-data-list
[pageTitle]="'مدیریت شناسه‌ی کالاها'" [pageTitle]="'مدیریت شناسه کالاها'"
[addNewCtaLabel]="'افزودن شناسه‌ی کالا'" [addNewCtaLabel]="'افزودن شناسه کالا'"
[columns]="columns" [columns]="columns"
[showAdd]="true" [showAdd]="true"
emptyPlaceholderTitle="شناسه‌ی کالایی یافت نشد" emptyPlaceholderTitle="شناسه کالایی یافت نشد"
emptyPlaceholderDescription="برای افزودن شناسه‌ی کالا، روی دکمهٔ بالا کلیک کنید." emptyPlaceholderDescription="برای افزودن شناسه کالا، روی دکمهٔ بالا کلیک کنید."
[items]="items()" [items]="items()"
[loading]="loading()" [loading]="loading()"
[showDetails]="false" [showDetails]="false"
@@ -18,7 +18,7 @@
<i class="pi pi-shopping-cart"></i> <i class="pi pi-shopping-cart"></i>
<span class="text-base font-bold">سفارش‌ها</span> <span class="text-base font-bold">سفارش‌ها</span>
</div> </div>
<button pButton type="button" icon="pi pi-refresh" outlined size="small" (click)="clearOrderList()"> <button pButton type="button" icon="pi pi-trash" outlined size="small" (click)="clearOrderList()">
پاک کردن سفارش‌ها پاک کردن سفارش‌ها
</button> </button>
</div> </div>
@@ -4,7 +4,7 @@
[options]="items()" [options]="items()"
optionLabel="name" optionLabel="name"
[optionValue]="selectOptionValue" [optionValue]="selectOptionValue"
placeholder="انتخاب شناسه‌ی کالا" placeholder="انتخاب شناسه کالا"
[formControl]="control" [formControl]="control"
[showClear]="showClear" [showClear]="showClear"
[name]="name || 'sku_id'" [name]="name || 'sku_id'"
@@ -1,20 +1,20 @@
<uikit-field <uikit-field
[label]="preparedLabel()" [label]="label"
[name]="name" [name]="name"
[control]="selectedType.value === 'amount' ? amountControl : percentageControl" [control]="selectedType.value === 'amount' ? amountControl : percentageControl"
[showLabel]="!!label" [showLabel]="!!label"
[showErrors]="showErrors" [showErrors]="showErrors"
> >
@if (labelSuffix) {
<ng-template #labelView> <ng-template #labelView>
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center justify-between">
<uikit-label [name]="name"> <uikit-label [name]="name">
{{ preparedLabel() }} {{ preparedLabel() }}
</uikit-label> </uikit-label>
@if (labelSuffix) {
<ng-container [ngTemplateOutlet]="labelSuffix"></ng-container> <ng-container [ngTemplateOutlet]="labelSuffix"></ng-container>
}
</div> </div>
</ng-template> </ng-template>
}
<p-inputgroup> <p-inputgroup>
@if (selectedType.value === "amount") { @if (selectedType.value === "amount") {
@@ -24,9 +24,7 @@
[attr.name]="name" [attr.name]="name"
[attr.placeholder]="placeholder" [attr.placeholder]="placeholder"
[attr.autocomplete]="autocomplete" [attr.autocomplete]="autocomplete"
[class]=" [class]="` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`
"
dir="ltr" dir="ltr"
[required]="required" [required]="required"
[invalid]="amountControl.invalid && (amountControl.touched || amountControl.dirty)" [invalid]="amountControl.invalid && (amountControl.touched || amountControl.dirty)"
@@ -128,12 +128,12 @@ export class AmountPercentageInputComponent {
if (notValid) { if (notValid) {
if (minValidator) { if (minValidator) {
this.toastService.warn({ this.toastService.warn({
text: `مقدار فیلد باید بیشتر از ${min} باشد.`, text: `مقدار ${this.label} باید بیشتر از ${formatWithCurrency(min)} باشد.`,
}); });
} }
if (maxValidator) { if (maxValidator) {
this.toastService.warn({ this.toastService.warn({
text: `مقدار فیلد باید کمتر از ${max} باشد.`, text: `مقدار ${this.label} باید کمتر از ${formatWithCurrency(max)} باشد.`,
}); });
} }
@@ -146,12 +146,12 @@ export class AmountPercentageInputComponent {
if (isPercentageType) { if (isPercentageType) {
const amountValue = (this.baseAmount * newValue) / 100; const amountValue = (this.baseAmount * newValue) / 100;
newValueToSet = newValue + ''; newValueToSet = newValue + '';
this.amountControl.setValue(amountValue); this.amountControl.setValue(isNaN(amountValue) ? 0 : amountValue);
if (notValid) { if (notValid) {
this.percentageControl.setValue(newValue); this.percentageControl.setValue(newValue);
} }
} else { } else {
const percentageValue = (newValue / this.baseAmount) * 100; const percentageValue = ((newValue / this.baseAmount) * 100).toFixed(2);
newValueToSet = newValue + ''; newValueToSet = newValue + '';
this.percentageControl.setValue(percentageValue); this.percentageControl.setValue(percentageValue);
this.preparedLabel.set(`${this.label} (${percentageValue} درصد)`); this.preparedLabel.set(`${this.label} (${percentageValue} درصد)`);
@@ -1,6 +1,6 @@
<p-breadcrumb [model]="items()"></p-breadcrumb> <p-breadcrumb [model]="items()"></p-breadcrumb>
<div class="absolute left-0 inset-y-0"> <div class="absolute left-0 inset-y-0">
<div class="me-4 flex items-center justify-center h-full"> <div class="me-4 flex items-center justify-center h-full">
<i class="pi pi-question-circle" pTooltip="محتوای آموزشی" tooltipPosition="bottom"></i> <i class="pi pi-question-circle" pTooltip="راهنما" tooltipPosition="bottom"></i>
</div> </div>
</div> </div>
@@ -2,18 +2,24 @@
<ng-template #title> <ng-template #title>
<ng-container [ngTemplateOutlet]="header"> <ng-container [ngTemplateOutlet]="header">
<div class="flex items-center gap-10 justify-between"> <div class="flex items-center gap-10 justify-between">
<div class="flex items-center gap-2">
@if (backRoute) {
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" size="small" outlined [routerLink]="backRoute" />
}
<h5 class="font-bold py-1.5">{{ cardTitle }}</h5> <h5 class="font-bold py-1.5">{{ cardTitle }}</h5>
</div>
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2 shrink-0">
<ng-container [ngTemplateOutlet]="moreActions"></ng-container> <ng-container [ngTemplateOutlet]="moreActions"></ng-container>
@if (showRefresh) { @if (showRefresh) {
<p-button icon="pi pi-refresh" outlined (click)="refresh()"></p-button> <p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
} }
@if (editable) { @if (editable) {
<button <button
pButton pButton
type="button" type="button"
[icon]="`pi ${editMode ? 'pi-times' : 'pi-pencil'}`" [icon]="`pi ${editMode ? 'pi-times' : 'pi-pencil'}`"
class="p-button-text p-button-plain" outlined
size="small"
(click)="onEditClick()" (click)="onEditClick()"
></button> ></button>
} }
@@ -9,6 +9,7 @@ import {
signal, signal,
TemplateRef, TemplateRef,
} from '@angular/core'; } from '@angular/core';
import { RouterLink, UrlTree } from '@angular/router';
import { Button, ButtonDirective } from 'primeng/button'; import { Button, ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
@@ -16,12 +17,13 @@ import { Card } from 'primeng/card';
selector: 'app-card-data', selector: 'app-card-data',
standalone: true, standalone: true,
templateUrl: './card-data.component.html', templateUrl: './card-data.component.html',
imports: [Card, CommonModule, ButtonDirective, Button], imports: [Card, CommonModule, ButtonDirective, Button, RouterLink],
}) })
export class AppCardComponent { export class AppCardComponent {
@Input() cardTitle!: string; @Input() cardTitle!: string;
@Input() editable: boolean = false; @Input() editable: boolean = false;
@Input() showRefresh: boolean = false; @Input() showRefresh: boolean = false;
@Input() backRoute?: UrlTree | string | any[];
@Input() @Input()
set editMode(v: boolean) { set editMode(v: boolean) {
@@ -4,7 +4,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({ @Component({
selector: 'field-sku', selector: 'field-sku',
template: `<catalog-sku-select label="شناسه‌ی کالا" [control]="control" [name]="name" />`, template: `<catalog-sku-select label="شناسه کالا" [control]="control" [name]="name" />`,
imports: [ReactiveFormsModule, CatalogSkuSelectComponent], imports: [ReactiveFormsModule, CatalogSkuSelectComponent],
}) })
export class SkuComponent { export class SkuComponent {
@@ -2,7 +2,7 @@
<div class="grow"> <div class="grow">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if (backRoute) { @if (backRoute) {
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="backRoute" /> <p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined size="small" [routerLink]="backRoute" />
} }
<span class="text-xl font-bold">{{ pageTitle }}</span> <span class="text-xl font-bold">{{ pageTitle }}</span>
</div> </div>
@@ -2,7 +2,7 @@
<div class="inline-flex gap-2 items-center w-full"> <div class="inline-flex gap-2 items-center w-full">
<span class="text-muted-color text-base font-normal flex-auto grow-0">{{ label }}:</span> <span class="text-muted-color text-base font-normal flex-auto grow-0">{{ label }}:</span>
<div <div
class="shrink-0 grow" class="shrink-0 grow max-sm:text-left"
[ngClass]="{ [ngClass]="{
'text-right': alignment === 'start', 'text-right': alignment === 'start',
'text-center': alignment === 'center', 'text-center': alignment === 'center',
@@ -30,6 +30,7 @@ export class KeyValueComponent {
| 'thumbnail' | 'thumbnail'
| 'nested' | 'nested'
| 'index' | 'index'
| 'number'
| 'simple' = 'simple'; | 'simple' = 'simple';
@Input() trueValueToShow?: string; @Input() trueValueToShow?: string;
@@ -89,6 +90,9 @@ export class KeyValueComponent {
case 'has': case 'has':
value = 'ندارد'; value = 'ندارد';
break; break;
case 'number':
value = '0';
break;
} }
} }
} }
@@ -0,0 +1,78 @@
<div class="h-full overflow-auto">
<div
[ngClass]="{
'px-4': captionBox,
'pb-4': true,
}"
>
@if (captionBox) {
<div class="pt-4">
<ng-container [ngTemplateOutlet]="captionBox"></ng-container>
</div>
<hr />
}
<div class="grid grid-cols-1 gap-3" [ngClass]="{ 'pt-4': captionBox }">
@for (item of items; track `gridView_${$index}`) {
<div class="card border border-surface-border bg-surface-50! mb-0! rounded-2xl p-4!">
<div class="listKeyValue">
@for (col of columns; track `gridView_${col.field.toString()}_${$index}`) {
@if (col.type !== "index") {
<app-key-value [label]="col.header">
@if (col.type === "thumbnail") {
<div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
(click)="openThumbnailModal(item)"
>
@if (item && col?.field && item[col!.field!]) {
<img [src]="item[col.field]" class="w-full h-full object-cover" />
}
</div>
} @else if (getTemplate(col)) {
<ng-container
[ngTemplateOutlet]="getTemplate(col)"
[ngTemplateOutletContext]="{ $implicit: item }"
></ng-container>
} @else if (col.canCopy) {
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
} @else {
<span [class]="getPreviewClasses(item, col)">
{{ getCell(item, col) }}
</span>
}
</app-key-value>
}
}
</div>
@if (showEdit || showDelete || showDetails) {
<hr />
<div class="flex justify-center gap-2 mt-2">
@if (showEdit) {
<p-button icon="pi pi-pencil" label="ویرایش" size="small" (click)="edit(item)"> </p-button>
}
@if (showDelete) {
<p-button icon="pi pi-trash" label="حذف" size="small" (click)="remove(item)"> </p-button>
}
@if (showDetails) {
<p-button icon="pi pi-eye" label="مشاهده" size="small" (click)="details(item)"> </p-button>
}
</div>
}
</div>
}
@if (loading) {
@for (i of [1, 2, 3, 4]; track `grid_view_loading${$index}`) {
<div class="w-full h-40">
<p-skeleton height="100%"></p-skeleton>
</div>
}
}
@if (showPaginator) {
<ng-container [ngTemplateOutlet]="paginator"> </ng-container>
}
@if (items.length === 0 && !loading) {
<ng-container [ngTemplateOutlet]="emptyMessageCard"></ng-container>
}
</div>
</div>
</div>
@@ -0,0 +1,252 @@
import { Maybe } from '@/core';
import { UikitCopyComponent } from '@/uikit';
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
import { CommonModule } from '@angular/common';
import {
Component,
computed,
ContentChild,
EventEmitter,
Input,
Output,
signal,
TemplateRef,
} from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { DrawerModule } from 'primeng/drawer';
import { PaginatorModule } from 'primeng/paginator';
import { SkeletonModule } from 'primeng/skeleton';
import { TableModule } from 'primeng/table';
import { KeyValueComponent } from '../key-value.component/key-value.component';
type TDataType =
| 'simple'
| 'price'
| 'boolean'
| 'date'
| 'nested'
| 'index'
| 'id'
| 'thumbnail'
| 'number';
export interface IColumn<T = any> {
field: T extends object ? keyof T | string : string;
header: string;
width?: string;
minWidth?: string;
canCopy?: boolean;
type?: TDataType;
nestedOption?: {
path: string;
type?: TDataType;
};
thumbnailOptions?: {
editable: boolean;
deletable: boolean;
showPreview: boolean;
};
dateOption?: {
expiredMode?: boolean;
dateTime?: boolean;
onlyTime?: boolean;
};
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
}
@Component({
selector: 'app-page-data-list-grid-view',
templateUrl: './page-data-list-grid-view.component.html',
host: {
class: 'block w-full h-full overflow-hidden',
},
imports: [
CommonModule,
TableModule,
ButtonModule,
SkeletonModule,
PaginatorModule,
DrawerModule,
UikitCopyComponent,
KeyValueComponent,
],
})
export class AppPageDataListGridView<I = any> {
@Input({ required: true }) pageTitle!: string;
@Input({ required: true }) columns!: IColumn[];
@Input({ required: true }) items!: any[];
@Input({ required: true }) loading!: boolean;
@Input({ required: true }) emptyPlaceholderTitle!: string;
@Input() addNewCtaLabel?: string;
@Input() emptyPlaceholderDescription?: string = '';
@Input() emptyPlaceholderCtaLabel?: string;
@Input() currentPage?: number = 1;
@Input() showEdit: boolean = false;
@Input() showDelete: boolean = false;
@Input() showDetails: boolean = false;
@Input() showAdd: boolean = false;
@Input() showRefresh: boolean = true;
@Input() isFiltered: boolean = false;
@Input() fullHeight?: boolean = false;
@Input() height: string = '';
@Input() expandable?: boolean = false;
@Input() expandableItemKey?: string = '';
@Input() expandColumns?: Maybe<IColumn[]> = null;
@Input() dataKey?: string = 'items';
@Input() showPaginator?: boolean;
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
@ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null;
@ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef<any> | null;
@ContentChild('paginator', { static: true }) paginator!: TemplateRef<any> | null;
@ContentChild('captionBox', { static: true }) captionBox!: TemplateRef<any> | null;
@ContentChild('emptyMessageCard', { static: true }) emptyMessageCard!: TemplateRef<any> | null;
@Output() onEdit = new EventEmitter<any>();
@Output() onDelete = new EventEmitter<any>();
@Output() onDetails = new EventEmitter<any>();
@Output() onAdd = new EventEmitter<void>();
@Output() onChangePage = new EventEmitter<any>();
@Output() onThumbnailClick = new EventEmitter<any>();
@Output() onRefresh = new EventEmitter();
filterDrawerVisible = signal<boolean>(false);
expandedRows: { [key: string]: boolean } = {};
edit = (item: I) => {
this.onEdit.emit(item);
};
remove = (item: I) => {
this.onDelete.emit(item);
};
details = (item: I) => {
this.onDetails.emit(item);
};
openAddForm = () => {
this.onAdd.emit();
};
openFilter = () => {
this.filterDrawerVisible.set(true);
};
closeFilter = () => {
this.filterDrawerVisible.set(false);
};
onPage = ($event: any) => {
this.onChangePage.emit($event);
};
openThumbnailModal = (item: I) => {
// this.
};
getPreviewClasses(item: Record<string, any>, column: IColumn): any {
if (!item || !column || column.customDataModel) return '';
try {
const { field } = column;
const data = item[String(field)];
switch (column.type) {
case 'date':
if (column.dateOption?.expiredMode) {
if (jalaliDiff(new Date(), data) > 0) {
return 'text-error';
}
}
return '';
default:
return;
}
} catch (e) {
return '';
}
}
getCell(item: Record<string, any>, column: IColumn): any {
if (!item || !column) return '';
try {
let { field, type } = column;
if (column.customDataModel) {
return this.renderCustom(column, item);
}
let data = item[String(field)];
if (column.type === 'nested') {
const path = column.nestedOption?.path;
if (!path) return '-';
data = path
.split('.')
.reduce((obj, key) => (obj && obj[key] !== undefined ? obj[key] : null), item);
type = column.nestedOption?.type || 'simple';
}
if (type) {
switch (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);
case 'boolean':
return data ? 'بله' : 'خیر';
case 'price':
return formatWithCurrency(data, false, 'ریال');
case 'number':
return data || 0;
default:
break;
}
}
if (data === undefined || data === null) {
return '-';
}
if (typeof data === 'object') {
return data.title || '-';
}
return data || '-';
} catch (e) {
return '-';
}
}
getTemplate(col: IColumn): TemplateRef<any> | null {
const v = col.customDataModel;
return v instanceof TemplateRef ? (v as TemplateRef<any>) : null;
}
renderCustom(column: IColumn, item: any): any {
const v = column.customDataModel;
if (!v) {
return null;
}
if (typeof v === 'function') {
try {
return (v as (item: any) => any)(item);
} catch {
return '-';
}
}
if (typeof v === 'string') return v;
return null;
}
actionsCount = computed(() => {
let totalCount = 0;
if (this.showEdit) totalCount += 1;
if (this.showDelete) totalCount += 1;
if (this.showDetails) totalCount += 1;
return totalCount;
});
refresh() {
this.onRefresh.emit();
}
}
@@ -0,0 +1,226 @@
<div class="h-full flex flex-col gap-4">
<p-table
[columns]="columns"
[scrollable]="true"
[value]="items"
[loading]="loading"
columnResizeMode="fit"
stripedRows
[showFirstLastIcon]="false"
[expandedRowKeys]="expandedRows"
[dataKey]="dataKey"
class="max-sm:hidden! grow flex! flex-col overflow-hidden"
[tableStyleClass]="!items.length && !loading ? 'h-full' : ''"
>
@if (captionBox) {
<ng-template pTemplate="caption">
<ng-container [ngTemplateOutlet]="captionBox"> </ng-container>
</ng-template>
}
<ng-template #header let-columns>
<tr>
@for (col of columns; track col.header) {
<th
[style]="
col.type === 'index' ? { width: '3rem', minWidth: '3rem' } : { width: col.width, minWidth: col.minWidth }
"
>
{{ col.header }}
</th>
}
@if (actionsCount()) {
<th type="th" [style]="{ width: `${actionsCount() * 2 + (actionsCount() - 1) * 0.25}rem` }"></th>
}
@if (expandable) {
<th style="width: 3rem"></th>
}
</tr>
</ng-template>
<ng-template #body let-item let-i="rowIndex" let-expanded="expanded">
<tr>
@for (col of columns; track col.field) {
<td>
@if (col.type === "index") {
{{ i * (currentPage || 1) + 1 }}
} @else if (col.type === "thumbnail") {
<div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
(click)="openThumbnailModal(item)"
>
@if (item[col.field]) {
<img [src]="item[col.field]" class="w-full h-full object-cover" />
}
</div>
} @else if (getTemplate(col)) {
<ng-container
[ngTemplateOutlet]="getTemplate(col)"
[ngTemplateOutletContext]="{ $implicit: item }"
></ng-container>
} @else if (col.canCopy) {
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
} @else {
<span [class]="getPreviewClasses(item, col)">
{{ getCell(item, col) }}
</span>
}
</td>
}
@if (actionsCount()) {
<td
table-action-row
type="td"
[showDetails]="showDetails"
[showDelete]="showDelete"
[showEdit]="showEdit"
(edit)="edit(item)"
(delete)="remove(item)"
(details)="details(item)"
></td>
}
@if (expandable) {
<td>
<p-button
type="button"
pRipple
[pRowToggler]="item"
[text]="true"
[rounded]="true"
[plain]="true"
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"
/>
</td>
}
</tr>
</ng-template>
@if (expandable && expandColumns && expandableItemKey) {
<ng-template #expandedrow let-item>
<tr>
<td [attr.colspan]="columns.length + 1">
<div class="px-4">
@if (item[expandableItemKey]?.length) {
<p-table
[columns]="expandColumns || []"
[scrollable]="true"
[value]="item[expandableItemKey]"
[loading]="loading"
columnResizeMode="fit"
stripedRows
[showFirstLastIcon]="false"
[expandedRowKeys]="expandedRows"
class="grow flex! flex-col overflow-hidden"
[tableStyleClass]="!item.items?.length && !loading ? 'h-full' : ''"
>
<ng-template #header let-columns>
<tr>
@for (col of expandColumns; track col.header) {
<th
[style]="
col.type === 'index'
? { width: '3rem', minWidth: '3rem' }
: { width: col.width, minWidth: col.minWidth }
"
>
{{ col.header }}
</th>
}
</tr>
</ng-template>
<ng-template #body let-item let-i="rowIndex">
<tr>
@for (col of expandColumns; track col.field) {
<td>
{{ item.name }}
@if (col.type === "index") {
{{ i * (currentPage || 1) + 1 }}
} @else if (col.type === "thumbnail") {
<div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
(click)="openThumbnailModal(item)"
>
@if (item[col.field]) {
<img [src]="item[col.field]" class="w-full h-full object-cover" />
}
</div>
} @else if (getTemplate(col)) {
<ng-container
[ngTemplateOutlet]="getTemplate(col)"
[ngTemplateOutletContext]="{ $implicit: item }"
></ng-container>
} @else if (col.canCopy) {
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
} @else {
<span [class]="getPreviewClasses(item, col)">
{{ getCell(item, col) }}
</span>
}
</td>
}
</tr>
</ng-template>
<ng-template #emptymessage>
<tr class="">
<td [colSpan]="(expandColumns.length + 1).toString()" class="w-full">
<uikit-empty-state
[title]="emptyPlaceholderTitle"
[description]="emptyPlaceholderDescription"
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[showCTA]="showAdd"
(ctaClick)="openAddForm()"
></uikit-empty-state>
</td>
</tr>
</ng-template>
</p-table>
} @else {
<div class="grid grid-cols-3 gap-4 py-2">
@for (col of expandColumns; track $index) {
<app-key-value
[label]="col.header"
[value]="item[expandableItemKey][col.field]"
[type]="col.type || 'simple'"
/>
}
</div>
}
</div>
</td>
</tr>
</ng-template>
}
<ng-template #emptymessage>
<tr class="">
<td [colSpan]="(columns.length + 1).toString()" class="w-full">
<ng-container [ngTemplateOutlet]="emptyMessageCard"></ng-container>
</td>
</tr>
</ng-template>
<ng-template #loadingbody let-columns>
@for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) {
<tr style="height: 46px">
@for (col of columns; track col) {
<td>
<p-skeleton></p-skeleton>
</td>
}
@if (actionsCount()) {
<td>
<p-skeleton></p-skeleton>
</td>
}
</tr>
}
</ng-template>
</p-table>
@if (showPaginator) {
<ng-container [ngTemplateOutlet]="paginator"> </ng-container>
}
</div>
@@ -0,0 +1,255 @@
import { Maybe } from '@/core';
import { UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit';
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
import { CommonModule } from '@angular/common';
import {
Component,
computed,
ContentChild,
EventEmitter,
Input,
Output,
signal,
TemplateRef,
} from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { DrawerModule } from 'primeng/drawer';
import { PaginatorModule } from 'primeng/paginator';
import { SkeletonModule } from 'primeng/skeleton';
import { TableModule } from 'primeng/table';
import { KeyValueComponent } from '../key-value.component/key-value.component';
import { TableActionRowComponent } from '../table-action-row.component';
type TDataType =
| 'simple'
| 'price'
| 'boolean'
| 'date'
| 'nested'
| 'index'
| 'id'
| 'thumbnail'
| 'number';
export interface IColumn<T = any> {
field: T extends object ? keyof T | string : string;
header: string;
width?: string;
minWidth?: string;
canCopy?: boolean;
type?: TDataType;
nestedOption?: {
path: string;
type?: TDataType;
};
thumbnailOptions?: {
editable: boolean;
deletable: boolean;
showPreview: boolean;
};
dateOption?: {
expiredMode?: boolean;
dateTime?: boolean;
onlyTime?: boolean;
};
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
}
@Component({
selector: 'app-page-data-list-table-view',
templateUrl: './page-data-list-table-view.component.html',
host: {
class: 'block w-full h-full overflow-hidden',
},
imports: [
CommonModule,
TableActionRowComponent,
UikitEmptyStateComponent,
TableModule,
ButtonModule,
SkeletonModule,
PaginatorModule,
DrawerModule,
UikitCopyComponent,
KeyValueComponent,
],
})
export class AppPageDataListTableView<I = any> {
@Input({ required: true }) pageTitle!: string;
@Input({ required: true }) columns!: IColumn[];
@Input({ required: true }) items!: any[];
@Input({ required: true }) loading!: boolean;
@Input({ required: true }) emptyPlaceholderTitle!: string;
@Input() addNewCtaLabel?: string;
@Input() emptyPlaceholderDescription?: string = '';
@Input() emptyPlaceholderCtaLabel?: string;
@Input() currentPage?: number = 1;
@Input() showEdit: boolean = false;
@Input() showDelete: boolean = false;
@Input() showDetails: boolean = false;
@Input() showAdd: boolean = false;
@Input() showRefresh: boolean = true;
@Input() isFiltered: boolean = false;
@Input() fullHeight?: boolean = false;
@Input() height: string = '';
@Input() expandable?: boolean = false;
@Input() expandableItemKey?: string = '';
@Input() expandColumns?: Maybe<IColumn[]> = null;
@Input() dataKey?: string = 'items';
@Input() showPaginator?: boolean;
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
@ContentChild('paginator', { static: true }) paginator!: TemplateRef<any> | null;
@ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null;
@ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef<any> | null;
@ContentChild('captionBox', { static: true }) captionBox!: TemplateRef<any> | null;
@ContentChild('emptyMessageCard', { static: true }) emptyMessageCard!: TemplateRef<any> | null;
@Output() onEdit = new EventEmitter<any>();
@Output() onDelete = new EventEmitter<any>();
@Output() onDetails = new EventEmitter<any>();
@Output() onAdd = new EventEmitter<void>();
@Output() onChangePage = new EventEmitter<any>();
@Output() onThumbnailClick = new EventEmitter<any>();
@Output() onRefresh = new EventEmitter();
filterDrawerVisible = signal<boolean>(false);
expandedRows: { [key: string]: boolean } = {};
edit = (item: I) => {
this.onEdit.emit(item);
};
remove = (item: I) => {
this.onDelete.emit(item);
};
details = (item: I) => {
this.onDetails.emit(item);
};
openAddForm = () => {
this.onAdd.emit();
};
openFilter = () => {
this.filterDrawerVisible.set(true);
};
closeFilter = () => {
this.filterDrawerVisible.set(false);
};
onPage = ($event: any) => {
this.onChangePage.emit($event);
};
openThumbnailModal = (item: I) => {
// this.
};
getPreviewClasses(item: Record<string, any>, column: IColumn): any {
if (!item || !column || column.customDataModel) return '';
try {
const { field } = column;
const data = item[String(field)];
switch (column.type) {
case 'date':
if (column.dateOption?.expiredMode) {
if (jalaliDiff(new Date(), data) > 0) {
return 'text-error';
}
}
return '';
default:
return;
}
} catch (e) {
return '';
}
}
getCell(item: Record<string, any>, column: IColumn): any {
if (!item || !column) return '';
try {
let { field, type } = column;
if (column.customDataModel) {
return this.renderCustom(column, item);
}
let data = item[String(field)];
if (column.type === 'nested') {
const path = column.nestedOption?.path;
if (!path) return '-';
data = path
.split('.')
.reduce((obj, key) => (obj && obj[key] !== undefined ? obj[key] : null), item);
type = column.nestedOption?.type || 'simple';
}
if (type) {
switch (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);
case 'boolean':
return data ? 'بله' : 'خیر';
case 'price':
return formatWithCurrency(data, false, 'ریال');
case 'number':
return data || 0;
default:
break;
}
}
if (data === undefined || data === null) {
return '-';
}
if (typeof data === 'object') {
return data.title || '-';
}
return data || '-';
} catch (e) {
return '-';
}
}
getTemplate(col: IColumn): TemplateRef<any> | null {
const v = col.customDataModel;
return v instanceof TemplateRef ? (v as TemplateRef<any>) : null;
}
renderCustom(column: IColumn, item: any): any {
const v = column.customDataModel;
if (!v) {
return null;
}
if (typeof v === 'function') {
try {
return (v as (item: any) => any)(item);
} catch {
return '-';
}
}
if (typeof v === 'string') return v;
return null;
}
actionsCount = computed(() => {
let totalCount = 0;
if (this.showEdit) totalCount += 1;
if (this.showDelete) totalCount += 1;
if (this.showDetails) totalCount += 1;
return totalCount;
});
refresh() {
this.onRefresh.emit();
}
}
@@ -1,20 +1,24 @@
<div class="h-full bg-surface-overlay rounded-(--p-card-border-radius) overflow-hidden"> <div class="h-full bg-surface-overlay rounded-(--p-card-border-radius) overflow-hidden">
<div class="h-full flex flex-col gap-4"> @if (!isMobile) {
<p-table <app-page-data-list-table-view
[pageTitle]="pageTitle"
[columns]="columns" [columns]="columns"
[scrollable]="true" [items]="items"
[value]="items"
[loading]="loading" [loading]="loading"
columnResizeMode="fit" [emptyPlaceholderTitle]="emptyPlaceholderTitle"
stripedRows [emptyPlaceholderDescription]="emptyPlaceholderDescription"
[showFirstLastIcon]="false" [emptyPlaceholderCtaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[expandedRowKeys]="expandedRows" [addNewCtaLabel]="addNewCtaLabel"
[dataKey]="dataKey" [currentPage]="currentPage"
class="grow flex! flex-col overflow-hidden" [showEdit]="showEdit"
[tableStyleClass]="!items.length && !loading ? 'h-full' : ''" [showDelete]="showDelete"
[showDetails]="showDetails"
(onEdit)="edit($event)"
(onDelete)="remove($event)"
(onDetails)="details($event)"
> >
@if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) { @if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) {
<ng-template pTemplate="caption"> <ng-template #captionBox>
<ng-container [ngTemplateOutlet]="caption"> <ng-container [ngTemplateOutlet]="caption">
<div class="flex justify-between items-center gap-4"> <div class="flex justify-between items-center gap-4">
<h5 class="font-bold">{{ pageTitle }}</h5> <h5 class="font-bold">{{ pageTitle }}</h5>
@@ -27,15 +31,21 @@
icon="pi pi-filter" icon="pi pi-filter"
variant="outlined" variant="outlined"
badgeSeverity="info" badgeSeverity="info"
size="small"
[badge]="isFiltered ? '1' : undefined" [badge]="isFiltered ? '1' : undefined"
(click)="openFilter()" (click)="openFilter()"
></p-button> ></p-button>
} }
@if (showRefresh) { @if (showRefresh) {
<p-button icon="pi pi-refresh" outlined (click)="refresh()"></p-button> <p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
} }
@if (showAdd) { @if (showAdd) {
<p-button label="{{ addNewCtaLabel }}" icon="pi pi-plus" (click)="openAddForm()"></p-button> <p-button
label="{{ addNewCtaLabel }}"
icon="pi pi-plus"
size="small"
(click)="openAddForm()"
></p-button>
} }
</div> </div>
} }
@@ -43,157 +53,7 @@
</ng-container> </ng-container>
</ng-template> </ng-template>
} }
<ng-template #emptyMessageCard>
<ng-template #header let-columns>
<tr>
@for (col of columns; track col.header) {
<th
[style]="
col.type === 'index'
? { width: '3rem', minWidth: '3rem' }
: { width: col.width, minWidth: col.minWidth }
"
>
{{ col.header }}
</th>
}
@if (actionsCount()) {
<th type="th" [style]="{ width: `${actionsCount() * 2 + (actionsCount() - 1) * 0.25}rem` }"></th>
}
@if (expandable) {
<th style="width: 3rem"></th>
}
</tr>
</ng-template>
<ng-template #body let-item let-i="rowIndex" let-expanded="expanded">
<tr>
@for (col of columns; track col.field) {
<td>
@if (col.type === "index") {
{{ i * (currentPage || 1) + 1 }}
} @else if (col.type === "thumbnail") {
<div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
(click)="openThumbnailModal(item)"
>
@if (item[col.field]) {
<img [src]="item[col.field]" class="w-full h-full object-cover" />
}
</div>
} @else if (getTemplate(col)) {
<ng-container
[ngTemplateOutlet]="getTemplate(col)"
[ngTemplateOutletContext]="{ $implicit: item }"
></ng-container>
} @else if (col.canCopy) {
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
} @else {
<span [class]="getPreviewClasses(item, col)">
{{ getCell(item, col) }}
</span>
}
</td>
}
@if (actionsCount()) {
<td
table-action-row
type="td"
[showDetails]="showDetails"
[showDelete]="showDelete"
[showEdit]="showEdit"
(edit)="edit(item)"
(delete)="remove(item)"
(details)="details(item)"
></td>
}
@if (expandable) {
<td>
<p-button
type="button"
pRipple
[pRowToggler]="item"
[text]="true"
[rounded]="true"
[plain]="true"
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"
/>
</td>
}
</tr>
</ng-template>
@if (expandable && expandColumns && expandableItemKey) {
<ng-template #expandedrow let-item>
<tr>
<td [attr.colspan]="columns.length + 1">
<div class="px-4">
@if (item[expandableItemKey]?.length) {
<p-table
[columns]="expandColumns || []"
[scrollable]="true"
[value]="item[expandableItemKey]"
[loading]="loading"
columnResizeMode="fit"
stripedRows
[showFirstLastIcon]="false"
[expandedRowKeys]="expandedRows"
class="grow flex! flex-col overflow-hidden"
[tableStyleClass]="!item.items?.length && !loading ? 'h-full' : ''"
>
<ng-template #header let-columns>
<tr>
@for (col of expandColumns; track col.header) {
<th
[style]="
col.type === 'index'
? { width: '3rem', minWidth: '3rem' }
: { width: col.width, minWidth: col.minWidth }
"
>
{{ col.header }}
</th>
}
</tr>
</ng-template>
<ng-template #body let-item let-i="rowIndex">
<tr>
@for (col of expandColumns; track col.field) {
<td>
{{ item.name }}
@if (col.type === "index") {
{{ i * (currentPage || 1) + 1 }}
} @else if (col.type === "thumbnail") {
<div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
(click)="openThumbnailModal(item)"
>
@if (item[col.field]) {
<img [src]="item[col.field]" class="w-full h-full object-cover" />
}
</div>
} @else if (getTemplate(col)) {
<ng-container
[ngTemplateOutlet]="getTemplate(col)"
[ngTemplateOutletContext]="{ $implicit: item }"
></ng-container>
} @else if (col.canCopy) {
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
} @else {
<span [class]="getPreviewClasses(item, col)">
{{ getCell(item, col) }}
</span>
}
</td>
}
</tr>
</ng-template>
<ng-template #emptymessage>
<tr class="">
<td [colSpan]="(expandColumns.length + 1).toString()" class="w-full">
<uikit-empty-state <uikit-empty-state
[title]="emptyPlaceholderTitle" [title]="emptyPlaceholderTitle"
[description]="emptyPlaceholderDescription" [description]="emptyPlaceholderDescription"
@@ -201,59 +61,8 @@
[showCTA]="showAdd" [showCTA]="showAdd"
(ctaClick)="openAddForm()" (ctaClick)="openAddForm()"
></uikit-empty-state> ></uikit-empty-state>
</td>
</tr>
</ng-template> </ng-template>
</p-table> <ng-template #paginator>
} @else {
<div class="grid grid-cols-3 gap-4 py-2">
@for (col of expandColumns; track $index) {
<app-key-value
[label]="col.header"
[value]="item[expandableItemKey][col.field]"
[type]="col.type || 'simple'"
/>
}
</div>
}
</div>
</td>
</tr>
</ng-template>
}
<ng-template #emptymessage>
<tr class="">
<td [colSpan]="(columns.length + 1).toString()" class="w-full">
<uikit-empty-state
[title]="emptyPlaceholderTitle"
[description]="emptyPlaceholderDescription"
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[showCTA]="showAdd"
(ctaClick)="openAddForm()"
></uikit-empty-state>
</td>
</tr>
</ng-template>
<ng-template #loadingbody let-columns>
@for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) {
<tr style="height: 46px">
@for (col of columns; track col) {
<td>
<p-skeleton></p-skeleton>
</td>
}
@if (actionsCount()) {
<td>
<p-skeleton></p-skeleton>
</td>
}
</tr>
}
</ng-template>
</p-table>
@if (showPaginator) {
<app-paginator <app-paginator
[currentPage]="currentPage || 1" [currentPage]="currentPage || 1"
[totalRecords]="totalRecords" [totalRecords]="totalRecords"
@@ -261,8 +70,83 @@
[loading]="loading" [loading]="loading"
(onChange)="onPage($event)" (onChange)="onPage($event)"
/> />
</ng-template>
</app-page-data-list-table-view>
} @else {
<app-page-data-list-grid-view
[pageTitle]="pageTitle"
[columns]="columns"
[items]="items"
[loading]="loading"
[emptyPlaceholderTitle]="emptyPlaceholderTitle"
[emptyPlaceholderDescription]="emptyPlaceholderDescription"
[emptyPlaceholderCtaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[addNewCtaLabel]="addNewCtaLabel"
[currentPage]="currentPage"
[showEdit]="showEdit"
[showDelete]="showDelete"
[showDetails]="showDetails"
(onEdit)="edit($event)"
(onDelete)="remove($event)"
(onDetails)="details($event)"
>
@if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) {
<ng-template #captionBox>
<ng-container [ngTemplateOutlet]="caption">
<div class="flex justify-between items-center gap-4">
<h5 class="font-bold">{{ pageTitle }}</h5>
@if (showAdd || filter || showRefresh || moreActions) {
<div class="flex items-center gap-2">
<ng-container [ngTemplateOutlet]="moreActions" />
@if (filter) {
<p-button
label="فیلتر"
icon="pi pi-filter"
variant="outlined"
badgeSeverity="info"
size="small"
[badge]="isFiltered ? '1' : undefined"
(click)="openFilter()"
></p-button>
}
@if (showRefresh) {
<p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
}
@if (showAdd) {
<p-button
label="{{ addNewCtaLabel }}"
icon="pi pi-plus"
size="small"
(click)="openAddForm()"
></p-button>
} }
</div> </div>
}
</div>
</ng-container>
</ng-template>
}
<ng-template #emptyMessageCard>
<uikit-empty-state
[title]="emptyPlaceholderTitle"
[description]="emptyPlaceholderDescription"
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[showCTA]="showAdd"
(ctaClick)="openAddForm()"
></uikit-empty-state>
</ng-template>
<ng-template #paginator>
<app-paginator
[currentPage]="currentPage || 1"
[totalRecords]="totalRecords"
[perPage]="perPage || 10"
[loading]="loading"
(onChange)="onPage($event)"
/>
</ng-template>
</app-page-data-list-grid-view>
}
@if (filter) { @if (filter) {
<p-drawer <p-drawer
[visible]="filterDrawerVisible()" [visible]="filterDrawerVisible()"
@@ -1,13 +1,13 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { PaginatorComponent, UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit'; import { PaginatorComponent, UikitEmptyStateComponent } from '@/uikit';
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils'; import { jalaliDiff } from '@/utils';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
Component, Component,
computed,
ContentChild, ContentChild,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostListener,
Input, Input,
Output, Output,
Renderer2, Renderer2,
@@ -19,10 +19,19 @@ import { DrawerModule } from 'primeng/drawer';
import { PaginatorModule } from 'primeng/paginator'; import { PaginatorModule } from 'primeng/paginator';
import { SkeletonModule } from 'primeng/skeleton'; import { SkeletonModule } from 'primeng/skeleton';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { KeyValueComponent } from '../key-value.component/key-value.component'; import { AppPageDataListGridView } from './page-data-list-grid-view.component';
import { TableActionRowComponent } from '../table-action-row.component'; import { AppPageDataListTableView } from './page-data-list-table-view.component';
type TDataType = 'simple' | 'price' | 'boolean' | 'date' | 'nested' | 'index' | 'id' | 'thumbnail'; type TDataType =
| 'simple'
| 'price'
| 'boolean'
| 'date'
| 'nested'
| 'index'
| 'id'
| 'thumbnail'
| 'number';
export interface IColumn<T = any> { export interface IColumn<T = any> {
field: T extends object ? keyof T | string : string; field: T extends object ? keyof T | string : string;
header: string; header: string;
@@ -56,19 +65,18 @@ export interface IColumn<T = any> {
}, },
imports: [ imports: [
CommonModule, CommonModule,
TableActionRowComponent,
UikitEmptyStateComponent,
TableModule, TableModule,
ButtonModule, ButtonModule,
SkeletonModule, SkeletonModule,
PaginatorModule, PaginatorModule,
DrawerModule, DrawerModule,
PaginatorComponent, PaginatorComponent,
UikitCopyComponent, AppPageDataListTableView,
KeyValueComponent, AppPageDataListGridView,
UikitEmptyStateComponent,
], ],
}) })
export class PageDataListComponent<I> { export class PageDataListComponent<I = any> {
constructor( constructor(
private host: ElementRef, private host: ElementRef,
private renderer: Renderer2, private renderer: Renderer2,
@@ -77,7 +85,7 @@ export class PageDataListComponent<I> {
@Input({ required: true }) pageTitle!: string; @Input({ required: true }) pageTitle!: string;
@Input() addNewCtaLabel?: string; @Input() addNewCtaLabel?: string;
@Input({ required: true }) columns!: IColumn[]; @Input({ required: true }) columns!: IColumn[];
@Input({ required: true }) items!: I[]; @Input({ required: true }) items!: any[];
@Input({ required: true }) loading!: boolean; @Input({ required: true }) loading!: boolean;
@Input() emptyPlaceholderTitle: string = 'موردی برای نمایش وجود ندارد'; @Input() emptyPlaceholderTitle: string = 'موردی برای نمایش وجود ندارد';
@Input() emptyPlaceholderDescription?: string = ''; @Input() emptyPlaceholderDescription?: string = '';
@@ -123,17 +131,19 @@ export class PageDataListComponent<I> {
); );
// if (this.fullHeight) { // if (this.fullHeight) {
// } // }
this.updateViewportMode();
} }
edit = (item: I) => { edit = (item: any) => {
this.onEdit.emit(item); this.onEdit.emit(item);
}; };
remove = (item: I) => { remove = (item: any) => {
this.onDelete.emit(item); this.onDelete.emit(item);
}; };
details = (item: I) => { details = (item: any) => {
this.onDetails.emit(item); this.onDetails.emit(item);
}; };
@@ -183,84 +193,18 @@ export class PageDataListComponent<I> {
} }
} }
getCell(item: Record<string, any>, column: IColumn): any {
if (!item || !column) return '';
try {
let { field, type } = column;
if (column.customDataModel) {
return this.renderCustom(column, item);
}
let data = item[String(field)];
if (column.type === 'nested') {
const path = column.nestedOption?.path;
if (!path) return '-';
data = path
.split('.')
.reduce((obj, key) => (obj && obj[key] !== undefined ? obj[key] : null), item);
type = column.nestedOption?.type || 'simple';
}
if (type) {
switch (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);
case 'boolean':
return data ? 'بله' : 'خیر';
case 'price':
return formatWithCurrency(data, false, 'ریال');
default:
break;
}
}
if (data === undefined || data === null) {
return '-';
}
if (typeof data === 'object') {
return data.title || '-';
}
return data || '-';
} catch (e) {
return '-';
}
}
getTemplate(col: IColumn): TemplateRef<any> | null {
const v = col.customDataModel;
return v instanceof TemplateRef ? (v as TemplateRef<any>) : null;
}
renderCustom(column: IColumn, item: any): any {
const v = column.customDataModel;
if (!v) {
return null;
}
if (typeof v === 'function') {
try {
return (v as (item: any) => any)(item);
} catch {
return '-';
}
}
if (typeof v === 'string') return v;
return null;
}
actionsCount = computed(() => {
let totalCount = 0;
if (this.showEdit) totalCount += 1;
if (this.showDelete) totalCount += 1;
if (this.showDetails) totalCount += 1;
return totalCount;
});
refresh() { refresh() {
this.onRefresh.emit(); this.onRefresh.emit();
} }
isMobile = false;
@HostListener('window:resize')
onResize() {
this.updateViewportMode();
}
private updateViewportMode() {
this.isMobile = typeof window !== 'undefined' && window.innerWidth <= 768;
}
} }
+4
View File
@@ -1,3 +1,7 @@
:root { :root {
--p-drawer-header-padding: 0.5rem 0.875rem !important; --p-drawer-header-padding: 0.5rem 0.875rem !important;
} }
.listKeyValue {
@apply grid md:grid-cols-3 sm:grid-cols-2 sm:gap-4 gap-3 items-center;
}
+3 -2
View File
@@ -1,9 +1,10 @@
// Development environment configuration // Development environment configuration
export const environment = { export const environment = {
production: false, production: false,
apiBaseUrl: 'http://194.59.214.243:5002', // apiBaseUrl: 'http://194.59.214.243:5002',
apiBaseUrl: 'http://192.168.128.73:5002',
host: 'localhost', host: 'localhost',
port: 5000, port: 5001,
enableLogging: false, enableLogging: false,
enableDebug: false, enableDebug: false,
enableNativeBridge: false, enableNativeBridge: false,
+8 -10
View File
@@ -1,15 +1,13 @@
// Staging environment configuration // Development environment configuration
export const environment = { export const environment = {
production: false, production: false,
apiBaseUrl: 'https://staging-api.yourdomain.com', // apiBaseUrl: 'http://194.59.214.243:5002',
host: 'staging.yourdomain.com', apiBaseUrl: 'http://192.168.128.73:5002',
port: 443, // apiBaseUrl: 'http://localhost:5002',
enableNativeBridge: false, // host: 'http://194.59.214.243',
apiEndpoints: { host: '0.0.0.0',
auth: '/api/auth', port: 5005,
users: '/api/users',
// Add more endpoints as needed
},
enableLogging: true, enableLogging: true,
enableDebug: true, enableDebug: true,
enableNativeBridge: false,
}; };
+2 -1
View File
@@ -1,7 +1,8 @@
// TIS tenant environment configuration // TIS tenant environment configuration
export const environment = { export const environment = {
production: true, production: true,
apiBaseUrl: 'http://194.59.214.243:5002', // apiBaseUrl: 'http://194.59.214.243:5002',
apiBaseUrl: 'http://192.168.128.73:5002',
host: 'localhost', host: 'localhost',
port: 5000, port: 5000,
enableLogging: false, enableLogging: false,
+5 -4
View File
@@ -32,6 +32,7 @@ const MyPreset = definePreset(Aura, {
}, },
semantic: { semantic: {
primary: { primary: {
0: '{surface.0}',
50: '{surface.50}', 50: '{surface.50}',
100: '{surface.100}', 100: '{surface.100}',
200: '{surface.200}', 200: '{surface.200}',
@@ -49,15 +50,15 @@ const MyPreset = definePreset(Aura, {
light: { light: {
primary: { primary: {
color: '{primary.950}', color: '{primary.950}',
contrastColor: '#ffffff', contrastColor: '#f0f0f0',
hoverColor: '{primary.800}', hoverColor: '{primary.800}',
activeColor: '{primary.700}', activeColor: '{primary.700}',
}, },
highlight: { highlight: {
background: '{primary.950}', background: '{primary.950}',
focusBackground: '{primary.700}', focusBackground: '{primary.700}',
color: '#ffffff', color: '#f0f0f0',
focusColor: '#ffffff', focusColor: '#f0f0f0',
}, },
}, },
dark: { dark: {
@@ -78,7 +79,7 @@ const MyPreset = definePreset(Aura, {
}, },
primitive: { primitive: {
...updateSurfacePalette({ ...updateSurfacePalette({
0: '#ffffff', 0: '#f0f0f0',
50: '#f8fafc', 50: '#f8fafc',
100: '#f1f5f9', 100: '#f1f5f9',
200: '#e2e8f0', 200: '#e2e8f0',
+6 -2
View File
@@ -1,11 +1,15 @@
import { POS_ROUTES } from '@/domains/pos/routes'; import { POS_ROUTES } from '@/domains/pos/routes';
import { AppLayout } from '@/layout/default/app.layout.component';
import { AuthComponent } from '@/modules/auth/pages/auth.component'; import { AuthComponent } from '@/modules/auth/pages/auth.component';
import { Notfound } from '@/pages/notfound/notfound.component'; import { Notfound } from '@/pages/notfound/notfound.component';
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
export const appRoutes: Routes = [ export const appRoutes: Routes = [
{ path: '', redirectTo: 'pos', pathMatch: 'full' }, {
POS_ROUTES, path: '',
component: AppLayout,
children: [POS_ROUTES],
},
{ {
path: 'auth', path: 'auth',
component: AuthComponent, component: AuthComponent,