diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..ab1ff6d --- /dev/null +++ b/.env.production @@ -0,0 +1,2 @@ +TENANT=default +DIST_DIR=default diff --git a/.env.tis b/.env.tis new file mode 100644 index 0000000..9abf20b --- /dev/null +++ b/.env.tis @@ -0,0 +1,2 @@ +TENANT=tis +DIST_DIR=tis diff --git a/Dockerfile b/Dockerfile index dc9cdb1..5e1eb6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,15 @@ -# Build stage -FROM node:20 AS builder +# ---------- Build stage ---------- +FROM node:20-alpine AS builder ARG TENANT=tis ARG DIST_DIR=tis WORKDIR /app -# RUN npm config set registry "https://hub.megan.ir/npm/" - RUN npm install -g pnpm + COPY package.json pnpm-lock.yaml ./ -RUN pnpm install +RUN pnpm install --frozen-lockfile COPY . . @@ -20,14 +19,23 @@ RUN if [ "$TENANT" = "default" ]; then \ pnpm run build:$TENANT; \ fi + +# ---------- Runtime stage ---------- 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 -COPY nginx.conf /etc/nginx/nginx.conf - 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;"] diff --git a/angular.json b/angular.json index 5bf3c3e..554bd8f 100644 --- a/angular.json +++ b/angular.json @@ -10,6 +10,43 @@ "build": { "builder": "@angular/build:application", "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": { "extractLicenses": false, "optimization": false, @@ -32,10 +69,18 @@ { "replace": "src/environments/environment.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", - "outputPath": "dist/pos.client", + "outputPath": "dist/production", "serviceWorker": "ngsw-config.json" }, "staging": { diff --git a/docker-compose.yml b/docker-compose.yml index 8b20d76..836e833 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,72 +1,51 @@ version: "3.8" services: - # Production service - app: - platform: ${DOCKER_PLATFORM:-linux/amd64} + app_tis: build: context: . dockerfile: Dockerfile args: - TENANT: ${TENANT:-tis} - DIST_DIR: ${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 + TENANT: tis + DIST_DIR: tis - # # Development service with hot reload - # dev: - # build: - # context: . - # dockerfile: Dockerfile.dev - # container_name: psp_panel-dev - # working_dir: /app - # volumes: - # - .:/app - # - /app/node_modules - # ports: - # - "4200:4200" - # environment: - # - NODE_ENV=development - # restart: unless-stopped - # profiles: - # - dev - # networks: - # - psp_panel_network + ports: + - "8090:8090" + restart: unless-stopped + + app_default: + build: + context: . + dockerfile: Dockerfile + args: + TENANT: default + DIST_DIR: default + + ports: + - "8090:8090" + restart: unless-stopped + + # labels: + # - "traefik.enable=true" + + # # 🌐 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: - psp_panel_network: + web: driver: bridge diff --git a/nginx.conf b/nginx.conf index 1ca0967..a3d925d 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,80 +1,28 @@ -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; +server { + listen 8090; + server_name _; -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; + root /usr/share/nginx/html; + index index.html; + # 🔥 Gzip gzip on; - gzip_vary on; - gzip_proxied any; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 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; + # 🔥 Brotli (better than gzip) + # brotli on; + # brotli_comp_level 6; + # brotli_types text/plain text/css application/javascript application/json image/svg+xml; - server { - listen 5000; - server_name localhost; - charset utf-8; + # 🔥 Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } - root /usr/share/nginx/html; - index index.html index.htm; - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - - # Cache control for static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Service worker files should be revalidated on each request - 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 / { - 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"; - } + # 🔥 Angular routing (SPA fallback) + location / { + try_files $uri $uri/ /index.html; } } diff --git a/ngsw-config.json b/ngsw-config.json index bbbb970..3a7b4a6 100644 --- a/ngsw-config.json +++ b/ngsw-config.json @@ -2,7 +2,7 @@ "$schema": "./node_modules/@angular/service-worker/config/schema.json", "appData": { "appVersion": "0.0.0", - "buildDate": "2026-04-27T06:02:22.249Z" + "buildDate": "2026-05-03T16:17:50.761Z" }, "assetGroups": [ { diff --git a/src/app/components/pos-display/README.md b/src/app/components/pos-display/README.md index e6a4e78..60f0207 100644 --- a/src/app/components/pos-display/README.md +++ b/src/app/components/pos-display/README.md @@ -78,7 +78,7 @@ Displays: name, pos_type, serial_number, device, model, provider (6 fields - sam | `variant` | 'consumer' \| 'partner' \| 'full' | 'full' | Display variant mode | | `pos` | Signal | undefined | POS entity data | | `editMode` | Signal | false | Edit mode state | -| `cardTitle` | string | 'اطلاعات پایانه‌ی فروش' | Card header title | +| `cardTitle` | string | 'اطلاعات پایانه‌ فروش' | Card header title | | `showMoreActions` | boolean | true | Show more actions button | ## Outputs diff --git a/src/app/components/pos-display/pos-display.component.ts b/src/app/components/pos-display/pos-display.component.ts index a371bc4..30c0c58 100644 --- a/src/app/components/pos-display/pos-display.component.ts +++ b/src/app/components/pos-display/pos-display.component.ts @@ -14,7 +14,7 @@ export class PosDisplayComponent { @Input() variant: PosVariant = 'full'; @Input() pos = signal(undefined); @Input() editMode = signal(false); - @Input() cardTitle = 'اطلاعات پایانه‌ی فروش'; + @Input() cardTitle = 'اطلاعات پایانه‌ فروش'; @Input() showMoreActions = true; @Output() editModeChange = new EventEmitter(); diff --git a/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.html b/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.html index 6f3ecf1..1b54768 100644 --- a/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.html +++ b/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.html @@ -4,24 +4,32 @@ } @else {
- + - +
-
+
-
+
اطلاعات پرداخت -
+
@for (payment of invoice.payments; track $index) { اطلاعات صادر کننده -
+
@@ -43,7 +51,7 @@ @if (variant !== "customer") { اطلاعات مشتری @if (invoice.customer) { -
+
@if (invoice.customer.type === "INDIVIDUAL") { @@ -60,7 +68,7 @@ }
} @else if (invoice.unknown_customer) { -
+
diff --git a/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.ts b/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.ts index 43e0e7d..dbac37d 100644 --- a/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.ts +++ b/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.ts @@ -19,6 +19,7 @@ import { TemplateRef, ViewChild, } from '@angular/core'; +import { UrlTree } from '@angular/router'; import { ButtonDirective } from 'primeng/button'; import { Divider } from 'primeng/divider'; import { TableModule } from 'primeng/table'; @@ -48,6 +49,7 @@ export class ConsumerSaleInvoiceSharedComponent { @Input({ required: true }) loading!: boolean; @Input() invoice?: Maybe; @Input() variant: TConsumerSaleInvoice = 'full'; + @Input() backRoute?: UrlTree | string | any[]; @Output() onRefresh = new EventEmitter(); diff --git a/src/app/domains/consumer/modules/businessActivities/components/goods/list.component.ts b/src/app/domains/consumer/modules/businessActivities/components/goods/list.component.ts index 2d7965c..e1da83c 100644 --- a/src/app/domains/consumer/modules/businessActivities/components/goods/list.component.ts +++ b/src/app/domains/consumer/modules/businessActivities/components/goods/list.component.ts @@ -21,7 +21,7 @@ export class ConsumerBusinessActivityGoodsListComponent extends AbstractList - + - ورود به پایانه + ورود به پایانه
-
+
diff --git a/src/app/domains/consumer/modules/dashboard/components/statistics/latestInvoices/latest-Invoices.component.html b/src/app/domains/consumer/modules/dashboard/components/statistics/latestInvoices/latest-Invoices.component.html index 71339d7..4350a44 100644 --- a/src/app/domains/consumer/modules/dashboard/components/statistics/latestInvoices/latest-Invoices.component.html +++ b/src/app/domains/consumer/modules/dashboard/components/statistics/latestInvoices/latest-Invoices.component.html @@ -1,5 +1,5 @@ - + - ورود به پایانه + ورود به پایانه
-
+
diff --git a/src/app/domains/partner/modules/consumers/components/licenses/form.component.html b/src/app/domains/partner/modules/consumers/components/licenses/form.component.html index a8c1c44..42feaaf 100644 --- a/src/app/domains/partner/modules/consumers/components/licenses/form.component.html +++ b/src/app/domains/partner/modules/consumers/components/licenses/form.component.html @@ -13,7 +13,7 @@ name="starts_at" hint="مدت زمان لایسنس‌ها ۱ ساله هستند." /> - + diff --git a/src/app/domains/partner/modules/consumers/views/businessActivities/single.component.html b/src/app/domains/partner/modules/consumers/views/businessActivities/single.component.html index b01cda9..fad7bc2 100644 --- a/src/app/domains/partner/modules/consumers/views/businessActivities/single.component.html +++ b/src/app/domains/partner/modules/consumers/views/businessActivities/single.component.html @@ -8,7 +8,7 @@ - +
diff --git a/src/app/domains/partner/modules/consumers/views/poses/single.component.html b/src/app/domains/partner/modules/consumers/views/poses/single.component.html index fe9d277..432ee50 100644 --- a/src/app/domains/partner/modules/consumers/views/poses/single.component.html +++ b/src/app/domains/partner/modules/consumers/views/poses/single.component.html @@ -1,5 +1,5 @@
- +
diff --git a/src/app/domains/pos/modules/landing/components/customers/individual/form.component.html b/src/app/domains/pos/modules/landing/components/customers/individual/form.component.html index 0d58db4..c05e775 100644 --- a/src/app/domains/pos/modules/landing/components/customers/individual/form.component.html +++ b/src/app/domains/pos/modules/landing/components/customers/individual/form.component.html @@ -1,8 +1,9 @@
- - - - + + + + + diff --git a/src/app/domains/pos/modules/landing/components/customers/individual/form.component.ts b/src/app/domains/pos/modules/landing/components/customers/individual/form.component.ts index d64b39c..e932965 100644 --- a/src/app/domains/pos/modules/landing/components/customers/individual/form.component.ts +++ b/src/app/domains/pos/modules/landing/components/customers/individual/form.component.ts @@ -1,11 +1,16 @@ -import { postalCodeValidator } from '@/core/validators'; -import { nationalIdValidator } from '@/core/validators/national-id.validator'; 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 { fieldControl } from '@/shared/constants'; import { CustomerType } from '@/shared/localEnum/constants/customerTypes'; -import { Component, inject } from '@angular/core'; -import { ReactiveFormsModule, Validators } from '@angular/forms'; +import { Component, computed, inject } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; import { Observable } from 'rxjs'; import { IIndividualCustomer } from '../../../models/customer'; import { PosLandingStore } from '../../../store/main.store'; @@ -13,36 +18,31 @@ import { PosLandingStore } from '../../../store/main.store'; @Component({ selector: 'pos-customer-individual-form', templateUrl: './form.component.html', - imports: [ReactiveFormsModule, InputComponent, FormFooterActionsComponent], + imports: [ + ReactiveFormsModule, + FormFooterActionsComponent, + FirstNameComponent, + LastNameComponent, + NationalIdComponent, + EconomicCodeComponent, + MobileNumberComponent, + ], }) export class CustomerIndividualFormComponent extends AbstractForm< IIndividualCustomer, IIndividualCustomer > { private readonly store = inject(PosLandingStore); + costumerInfo = computed(() => this.store.customer().info?.customer_individual); override showSuccessMessage = false; form = this.fb.group({ - first_name: [ - this.store.customer().info?.customer_individual?.first_name || '', - [Validators.required], - ], - last_name: [ - this.store.customer().info?.customer_individual?.last_name || '', - [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()], - ], + first_name: fieldControl.first_name(this.costumerInfo()?.first_name || ''), + last_name: fieldControl.last_name(this.costumerInfo()?.last_name || ''), + national_id: fieldControl.national_id(this.costumerInfo()?.national_id || ''), + postal_code: fieldControl.postal_code(this.costumerInfo()?.postal_code || ''), + economic_code: fieldControl.economic_code(this.costumerInfo()?.economic_code || ''), + mobile_number: fieldControl.mobile_number(this.costumerInfo()?.mobile_number || ''), }); override submitForm() { diff --git a/src/app/domains/pos/modules/landing/components/customers/unknown/form.component.html b/src/app/domains/pos/modules/landing/components/customers/unknown/form.component.html index a781de5..be9cf3a 100644 --- a/src/app/domains/pos/modules/landing/components/customers/unknown/form.component.html +++ b/src/app/domains/pos/modules/landing/components/customers/unknown/form.component.html @@ -1,5 +1,5 @@
+ - diff --git a/src/app/domains/pos/modules/landing/components/order/order-section.component.html b/src/app/domains/pos/modules/landing/components/order/order-section.component.html index 2ba033e..ac229d2 100644 --- a/src/app/domains/pos/modules/landing/components/order/order-section.component.html +++ b/src/app/domains/pos/modules/landing/components/order/order-section.component.html @@ -12,7 +12,7 @@ +
diff --git a/src/app/domains/pos/modules/saleInvoices/components/sale-invoice-card.component.html b/src/app/domains/pos/modules/saleInvoices/components/sale-invoice-card.component.html index cf50bea..a445c77 100644 --- a/src/app/domains/pos/modules/saleInvoices/components/sale-invoice-card.component.html +++ b/src/app/domains/pos/modules/saleInvoices/components/sale-invoice-card.component.html @@ -14,19 +14,43 @@

+ @if (["success", "failure"].includes(saleInvoice.status.value.toLowerCase())) { + + } @if (saleInvoice.status.value.toLowerCase() === "not_send") { - + } @if (saleInvoice.status.value.toLowerCase() === "queued") { } @if (saleInvoice.status.value.toLowerCase() === "failure") { - + } مشاهده جزییات
diff --git a/src/app/domains/pos/modules/saleInvoices/components/sale-invoice-card.component.ts b/src/app/domains/pos/modules/saleInvoices/components/sale-invoice-card.component.ts index 9a75f73..f66c1df 100644 --- a/src/app/domains/pos/modules/saleInvoices/components/sale-invoice-card.component.ts +++ b/src/app/domains/pos/modules/saleInvoices/components/sale-invoice-card.component.ts @@ -5,7 +5,6 @@ import { Component, computed, EventEmitter, inject, Input, Output, signal } from import { RouterLink } from '@angular/router'; import { Button, ButtonDirective } from 'primeng/button'; import { Card } from 'primeng/card'; -import { Tag } from 'primeng/tag'; import { finalize } from 'rxjs'; import { posSaleInvoicesNamedRoutes } from '../constants'; import { IPosSaleInvoicesSummaryResponse } from '../models'; @@ -17,7 +16,6 @@ import { PosSaleInvoicesService } from '../services/main.service'; imports: [ Button, KeyValueComponent, - Tag, Card, RouterLink, ButtonDirective, @@ -34,6 +32,15 @@ export class SaleInvoiceCardComponent { sendingLoading = signal(false); gettingStatusLoading = signal(false); resendingLoading = signal(false); + revokingInvoiceLoading = signal(false); + + onAction = computed( + () => + this.sendingLoading() || + this.gettingStatusLoading() || + this.resendingLoading() || + this.revokingInvoiceLoading(), + ); singlePageRoute = computed(() => posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.saleInvoice?.id), @@ -74,9 +81,6 @@ export class SaleInvoiceCardComponent { .getInquiry(this.saleInvoice.id) .pipe(finalize(() => this.gettingStatusLoading.set(false))) .subscribe((res) => { - this.toastService.info({ - text: `وضعیت: ${res.status || 'نامشخص'}`, - }); this.refreshRequested.emit(); }); } @@ -91,5 +95,16 @@ export class SaleInvoiceCardComponent { 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() {} } diff --git a/src/app/domains/pos/modules/saleInvoices/constants/apiRoutes/index.ts b/src/app/domains/pos/modules/saleInvoices/constants/apiRoutes/index.ts index 8113473..7ea93f8 100644 --- a/src/app/domains/pos/modules/saleInvoices/constants/apiRoutes/index.ts +++ b/src/app/domains/pos/modules/saleInvoices/constants/apiRoutes/index.ts @@ -8,6 +8,7 @@ export const POS_SALE_INVOICES_API_ROUTES = { retry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/retry`, status: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status`, getInquiry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/inquiry`, + revoke: (invoiceId: string) => `${baseUrl()}/${invoiceId}/revoke`, attempts: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/attempts`, }, }; diff --git a/src/app/domains/pos/modules/saleInvoices/models/io.d.ts b/src/app/domains/pos/modules/saleInvoices/models/io.d.ts index 4dd32c4..e7007d1 100644 --- a/src/app/domains/pos/modules/saleInvoices/models/io.d.ts +++ b/src/app/domains/pos/modules/saleInvoices/models/io.d.ts @@ -1,4 +1,5 @@ import ISummary from '@/core/models/summary'; +import { TspProviderResponseStatus } from '@/shared/catalog'; import { IEnumTranslate } from '@/shared/models/enum_translate.type'; export interface IPosSaleInvoicesRawResponse { diff --git a/src/app/domains/pos/modules/saleInvoices/services/main.service.ts b/src/app/domains/pos/modules/saleInvoices/services/main.service.ts index ce8aa80..f5722ed 100644 --- a/src/app/domains/pos/modules/saleInvoices/services/main.service.ts +++ b/src/app/domains/pos/modules/saleInvoices/services/main.service.ts @@ -59,6 +59,13 @@ export class PosSaleInvoicesService { ); } + revoke(invoiceId: string): Observable { + return this.http.post( + this.apiRoutes.fiscal.revoke(invoiceId), + {}, + ); + } + getFiscalAttempts(invoiceId: string): Observable { return this.http.get( this.apiRoutes.fiscal.attempts(invoiceId), diff --git a/src/app/domains/pos/modules/saleInvoices/views/single.component.html b/src/app/domains/pos/modules/saleInvoices/views/single.component.html index 6c04430..02959e0 100644 --- a/src/app/domains/pos/modules/saleInvoices/views/single.component.html +++ b/src/app/domains/pos/modules/saleInvoices/views/single.component.html @@ -1 +1,3 @@ - +
+ +
diff --git a/src/app/domains/pos/modules/saleInvoices/views/single.component.ts b/src/app/domains/pos/modules/saleInvoices/views/single.component.ts index ab94438..5d767f6 100644 --- a/src/app/domains/pos/modules/saleInvoices/views/single.component.ts +++ b/src/app/domains/pos/modules/saleInvoices/views/single.component.ts @@ -2,6 +2,7 @@ import { ConsumerSaleInvoiceSharedComponent } from '@/domains/consumer/component import pageParamsUtils from '@/utils/page-params.utils'; import { Component, computed, inject, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { posSaleInvoicesNamedRoutes } from '../constants'; import { PosSaleInvoiceStore } from '../store/main.store'; @Component({ @@ -19,6 +20,8 @@ export class PosSaleInvoiceComponent { readonly invoice = computed(() => this.store.entity()); readonly loading = computed(() => this.store.loading()); + readonly backRoute = computed(() => posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!()); + ngOnInit() { this.store.getData(this.invoiceId()); } diff --git a/src/app/domains/superAdmin/constants/menuItems.const.ts b/src/app/domains/superAdmin/constants/menuItems.const.ts index 3888498..e5fdf9f 100644 --- a/src/app/domains/superAdmin/constants/menuItems.const.ts +++ b/src/app/domains/superAdmin/constants/menuItems.const.ts @@ -37,7 +37,7 @@ export const SUPER_ADMIN_MENU_ITEMS = [ routerLink: ['/super_admin/guilds'], }, { - label: 'شناسه‌ی کالاها', + label: 'شناسه کالاها', icon: 'pi pi-fw pi-good', routerLink: [stockKeepingUnitsNamedRoutes.stockKeepingUnits.meta.pagePath!()], }, diff --git a/src/app/domains/superAdmin/modules/consumers/components/form.component.html b/src/app/domains/superAdmin/modules/consumers/components/form.component.html index 5ee5fee..bd52b97 100644 --- a/src/app/domains/superAdmin/modules/consumers/components/form.component.html +++ b/src/app/domains/superAdmin/modules/consumers/components/form.component.html @@ -25,7 +25,7 @@ name="license.starts_at" hint="مدت زمان لایسنس‌ها ۱ ساله هستند." /> --> - + } diff --git a/src/app/domains/superAdmin/modules/consumers/components/licenses/form.component.html b/src/app/domains/superAdmin/modules/consumers/components/licenses/form.component.html index 0416858..ab58e29 100644 --- a/src/app/domains/superAdmin/modules/consumers/components/licenses/form.component.html +++ b/src/app/domains/superAdmin/modules/consumers/components/licenses/form.component.html @@ -13,7 +13,7 @@ name="starts_at" hint="مدت زمان لایسنس‌ها ۱ ساله هستند." /> - + diff --git a/src/app/domains/superAdmin/modules/consumers/views/list.component.ts b/src/app/domains/superAdmin/modules/consumers/views/list.component.ts index 9313683..94ddf80 100644 --- a/src/app/domains/superAdmin/modules/consumers/views/list.component.ts +++ b/src/app/domains/superAdmin/modules/consumers/views/list.component.ts @@ -37,7 +37,7 @@ export class ConsumersComponent extends AbstractList { }, // { // field: 'license_expires_at', - // header: 'تاریخ انقضای لایسنس', + // header: 'تاریخ انقضا لایسنس', // customDataModel(item) { // if (item.license_info) { // return formatJalali(item.license_info.expires_at); diff --git a/src/app/domains/superAdmin/modules/consumers/views/poses/single.component.html b/src/app/domains/superAdmin/modules/consumers/views/poses/single.component.html index 3a919b6..106a214 100644 --- a/src/app/domains/superAdmin/modules/consumers/views/poses/single.component.html +++ b/src/app/domains/superAdmin/modules/consumers/views/poses/single.component.html @@ -1,5 +1,5 @@
- +
diff --git a/src/app/domains/superAdmin/modules/guilds/components/goods/form.component.html b/src/app/domains/superAdmin/modules/guilds/components/goods/form.component.html index 08522a2..774a539 100644 --- a/src/app/domains/superAdmin/modules/guilds/components/goods/form.component.html +++ b/src/app/domains/superAdmin/modules/guilds/components/goods/form.component.html @@ -15,7 +15,7 @@ /> } - + { @Input() header: IColumn[] = [ { field: 'image_url', header: 'تصویر', type: 'thumbnail' }, { field: 'name', header: 'عنوان' }, - { field: 'sku', header: 'شناسه‌ی کالا', type: 'nested', nestedOption: { path: 'sku.name' } }, + { field: 'sku', header: 'شناسه کالا', type: 'nested', nestedOption: { path: 'sku.name' } }, { field: 'category', header: 'دسته‌بندی', diff --git a/src/app/domains/superAdmin/modules/partners/components/chargeAccount/list.component.html b/src/app/domains/superAdmin/modules/partners/components/chargeAccount/list.component.html index 25aaab5..4cff7e7 100644 --- a/src/app/domains/superAdmin/modules/partners/components/chargeAccount/list.component.html +++ b/src/app/domains/superAdmin/modules/partners/components/chargeAccount/list.component.html @@ -1,5 +1,5 @@ { }, { field: 'license_renew', - header: 'تعداد لایسنس‌های تمدیدی', + header: 'تعداد لایسنس تمدیدی', customDataModel: this.licensesRenewStatus, }, { field: 'status', header: 'وضعیت' }, diff --git a/src/app/domains/superAdmin/modules/partners/views/single.component.html b/src/app/domains/superAdmin/modules/partners/views/single.component.html index ee57c94..711b988 100644 --- a/src/app/domains/superAdmin/modules/partners/views/single.component.html +++ b/src/app/domains/superAdmin/modules/partners/views/single.component.html @@ -8,12 +8,52 @@
+ +
+ + + + + +
+
diff --git a/src/app/domains/superAdmin/modules/partners/views/single.component.ts b/src/app/domains/superAdmin/modules/partners/views/single.component.ts index dddce19..1937f6b 100644 --- a/src/app/domains/superAdmin/modules/partners/views/single.component.ts +++ b/src/app/domains/superAdmin/modules/partners/views/single.component.ts @@ -1,9 +1,9 @@ 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 { Component, computed, effect, inject, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Button } from 'primeng/button'; -import { Divider } from 'primeng/divider'; import { ConsumerAccountListComponent } from '../components/accounts/list.component'; import { PartnerChargeLicenseFormDialogComponent } from '../components/charge-license-form-dialog.component'; import { AdminPartnerChargeAccountFormDialogComponent } from '../components/chargeAccount/charge-account-form-dialog.component'; @@ -28,7 +28,7 @@ import { PartnerStore } from '../store/partner.store'; PartnerChargeLicenseFormDialogComponent, AdminPartnerLicensesComponent, AdminPartnerChargeLicenseTransactionListComponent, - Divider, + PartnerLicenseInfoTemplateComponent, ], }) export class PartnerComponent { diff --git a/src/app/domains/superAdmin/modules/stockKeepingUnits/views/list.component.html b/src/app/domains/superAdmin/modules/stockKeepingUnits/views/list.component.html index 06f1efd..b15e17a 100644 --- a/src/app/domains/superAdmin/modules/stockKeepingUnits/views/list.component.html +++ b/src/app/domains/superAdmin/modules/stockKeepingUnits/views/list.component.html @@ -1,10 +1,10 @@ سفارش‌ها
-
diff --git a/src/app/shared/catalog/sku/components/select.component.html b/src/app/shared/catalog/sku/components/select.component.html index ec48311..ae52bd1 100644 --- a/src/app/shared/catalog/sku/components/select.component.html +++ b/src/app/shared/catalog/sku/components/select.component.html @@ -4,7 +4,7 @@ [options]="items()" optionLabel="name" [optionValue]="selectOptionValue" - placeholder="انتخاب شناسه‌ی کالا" + placeholder="انتخاب شناسه کالا" [formControl]="control" [showClear]="showClear" [name]="name || 'sku_id'" diff --git a/src/app/shared/components/amountPercentageInput/amount-percentage-input.component.html b/src/app/shared/components/amountPercentageInput/amount-percentage-input.component.html index 2119f0a..88e88b4 100644 --- a/src/app/shared/components/amountPercentageInput/amount-percentage-input.component.html +++ b/src/app/shared/components/amountPercentageInput/amount-percentage-input.component.html @@ -1,20 +1,20 @@ - @if (labelSuffix) { - -
- - {{ preparedLabel() }} - + +
+ + {{ preparedLabel() }} + + @if (labelSuffix) { -
-
- } + } +
+
@if (selectedType.value === "amount") { @@ -24,9 +24,7 @@ [attr.name]="name" [attr.placeholder]="placeholder" [attr.autocomplete]="autocomplete" - [class]=" - ` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}` - " + [class]="` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`" dir="ltr" [required]="required" [invalid]="amountControl.invalid && (amountControl.touched || amountControl.dirty)" diff --git a/src/app/shared/components/amountPercentageInput/amount-percentage-input.component.ts b/src/app/shared/components/amountPercentageInput/amount-percentage-input.component.ts index b0f0add..2c16db4 100644 --- a/src/app/shared/components/amountPercentageInput/amount-percentage-input.component.ts +++ b/src/app/shared/components/amountPercentageInput/amount-percentage-input.component.ts @@ -128,12 +128,12 @@ export class AmountPercentageInputComponent { if (notValid) { if (minValidator) { this.toastService.warn({ - text: `مقدار فیلد باید بیشتر از ${min} باشد.`, + text: `مقدار ${this.label} باید بیشتر از ${formatWithCurrency(min)} باشد.`, }); } if (maxValidator) { this.toastService.warn({ - text: `مقدار فیلد باید کمتر از ${max} باشد.`, + text: `مقدار ${this.label} باید کمتر از ${formatWithCurrency(max)} باشد.`, }); } @@ -146,12 +146,12 @@ export class AmountPercentageInputComponent { if (isPercentageType) { const amountValue = (this.baseAmount * newValue) / 100; newValueToSet = newValue + ''; - this.amountControl.setValue(amountValue); + this.amountControl.setValue(isNaN(amountValue) ? 0 : amountValue); if (notValid) { this.percentageControl.setValue(newValue); } } else { - const percentageValue = (newValue / this.baseAmount) * 100; + const percentageValue = ((newValue / this.baseAmount) * 100).toFixed(2); newValueToSet = newValue + ''; this.percentageControl.setValue(percentageValue); this.preparedLabel.set(`${this.label} (${percentageValue} درصد)`); diff --git a/src/app/shared/components/breadcrumb.component.html b/src/app/shared/components/breadcrumb.component.html index 90f88a6..e6b1a3c 100644 --- a/src/app/shared/components/breadcrumb.component.html +++ b/src/app/shared/components/breadcrumb.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/app/shared/components/card-data.component.html b/src/app/shared/components/card-data.component.html index 44c6b59..8a5a699 100644 --- a/src/app/shared/components/card-data.component.html +++ b/src/app/shared/components/card-data.component.html @@ -2,18 +2,24 @@
-
{{ cardTitle }}
+
+ @if (backRoute) { + + } +
{{ cardTitle }}
+
@if (showRefresh) { - + } @if (editable) { } diff --git a/src/app/shared/components/card-data.component.ts b/src/app/shared/components/card-data.component.ts index fc5c080..a876281 100644 --- a/src/app/shared/components/card-data.component.ts +++ b/src/app/shared/components/card-data.component.ts @@ -9,6 +9,7 @@ import { signal, TemplateRef, } from '@angular/core'; +import { RouterLink, UrlTree } from '@angular/router'; import { Button, ButtonDirective } from 'primeng/button'; import { Card } from 'primeng/card'; @@ -16,12 +17,13 @@ import { Card } from 'primeng/card'; selector: 'app-card-data', standalone: true, templateUrl: './card-data.component.html', - imports: [Card, CommonModule, ButtonDirective, Button], + imports: [Card, CommonModule, ButtonDirective, Button, RouterLink], }) export class AppCardComponent { @Input() cardTitle!: string; @Input() editable: boolean = false; @Input() showRefresh: boolean = false; + @Input() backRoute?: UrlTree | string | any[]; @Input() set editMode(v: boolean) { diff --git a/src/app/shared/components/fields/sku.component.ts b/src/app/shared/components/fields/sku.component.ts index 75793ae..1cc7f47 100644 --- a/src/app/shared/components/fields/sku.component.ts +++ b/src/app/shared/components/fields/sku.component.ts @@ -4,7 +4,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'field-sku', - template: ``, + template: ``, imports: [ReactiveFormsModule, CatalogSkuSelectComponent], }) export class SkuComponent { diff --git a/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html index 0708f92..71bbd27 100644 --- a/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html +++ b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html @@ -2,7 +2,7 @@
@if (backRoute) { - + } {{ pageTitle }}
diff --git a/src/app/shared/components/key-value.component/key-value.component.html b/src/app/shared/components/key-value.component/key-value.component.html index d949726..d1377de 100644 --- a/src/app/shared/components/key-value.component/key-value.component.html +++ b/src/app/shared/components/key-value.component/key-value.component.html @@ -2,7 +2,7 @@
{{ label }}:
+
+ @if (captionBox) { +
+ +
+
+ } +
+ @for (item of items; track `gridView_${$index}`) { +
+
+ @for (col of columns; track `gridView_${col.field.toString()}_${$index}`) { + @if (col.type !== "index") { + + @if (col.type === "thumbnail") { +
+ @if (item && col?.field && item[col!.field!]) { + + } +
+ } @else if (getTemplate(col)) { + + } @else if (col.canCopy) { + + } @else { + + {{ getCell(item, col) }} + + } +
+ } + } +
+ @if (showEdit || showDelete || showDetails) { +
+
+ @if (showEdit) { + + } + @if (showDelete) { + + } + @if (showDetails) { + + } +
+ } +
+ } + @if (loading) { + @for (i of [1, 2, 3, 4]; track `grid_view_loading${$index}`) { +
+ +
+ } + } + + @if (showPaginator) { + + } + @if (items.length === 0 && !loading) { + + } +
+
+
diff --git a/src/app/shared/components/pageDataList/page-data-list-grid-view.component.ts b/src/app/shared/components/pageDataList/page-data-list-grid-view.component.ts new file mode 100644 index 0000000..fea395a --- /dev/null +++ b/src/app/shared/components/pageDataList/page-data-list-grid-view.component.ts @@ -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 { + 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 | ((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 { + @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 = null; + @Input() dataKey?: string = 'items'; + @Input() showPaginator?: boolean; + + @ContentChild('filter', { static: true }) filter!: TemplateRef | null; + @ContentChild('moreActions', { static: true }) moreActions!: TemplateRef | null; + @ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef | null; + @ContentChild('paginator', { static: true }) paginator!: TemplateRef | null; + @ContentChild('captionBox', { static: true }) captionBox!: TemplateRef | null; + @ContentChild('emptyMessageCard', { static: true }) emptyMessageCard!: TemplateRef | null; + + @Output() onEdit = new EventEmitter(); + @Output() onDelete = new EventEmitter(); + @Output() onDetails = new EventEmitter(); + @Output() onAdd = new EventEmitter(); + @Output() onChangePage = new EventEmitter(); + @Output() onThumbnailClick = new EventEmitter(); + @Output() onRefresh = new EventEmitter(); + + filterDrawerVisible = signal(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, 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, 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 | null { + const v = col.customDataModel; + return v instanceof TemplateRef ? (v as TemplateRef) : 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(); + } +} diff --git a/src/app/shared/components/pageDataList/page-data-list-table-view.component.html b/src/app/shared/components/pageDataList/page-data-list-table-view.component.html new file mode 100644 index 0000000..eb4e656 --- /dev/null +++ b/src/app/shared/components/pageDataList/page-data-list-table-view.component.html @@ -0,0 +1,226 @@ +
+ + @if (captionBox) { + + + + } + + + + @for (col of columns; track col.header) { + + {{ col.header }} + + } + @if (actionsCount()) { + + } + @if (expandable) { + + } + + + + + + @for (col of columns; track col.field) { + + @if (col.type === "index") { + {{ i * (currentPage || 1) + 1 }} + } @else if (col.type === "thumbnail") { +
+ @if (item[col.field]) { + + } +
+ } @else if (getTemplate(col)) { + + } @else if (col.canCopy) { + + } @else { + + {{ getCell(item, col) }} + + } + + } + @if (actionsCount()) { + + } + + @if (expandable) { + + + + } + +
+ + @if (expandable && expandColumns && expandableItemKey) { + + + +
+ @if (item[expandableItemKey]?.length) { + + + + @for (col of expandColumns; track col.header) { + + {{ col.header }} + + } + + + + + + @for (col of expandColumns; track col.field) { + + {{ item.name }} + @if (col.type === "index") { + {{ i * (currentPage || 1) + 1 }} + } @else if (col.type === "thumbnail") { +
+ @if (item[col.field]) { + + } +
+ } @else if (getTemplate(col)) { + + } @else if (col.canCopy) { + + } @else { + + {{ getCell(item, col) }} + + } + + } + +
+ + + + + + + + +
+ } @else { +
+ @for (col of expandColumns; track $index) { + + } +
+ } +
+ + +
+ } + + + + + + + + + + + @for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) { + + @for (col of columns; track col) { + + + + } + @if (actionsCount()) { + + + + } + + } + +
+ + @if (showPaginator) { + + } +
diff --git a/src/app/shared/components/pageDataList/page-data-list-table-view.component.ts b/src/app/shared/components/pageDataList/page-data-list-table-view.component.ts new file mode 100644 index 0000000..284cf08 --- /dev/null +++ b/src/app/shared/components/pageDataList/page-data-list-table-view.component.ts @@ -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 { + 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 | ((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 { + @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 = null; + @Input() dataKey?: string = 'items'; + @Input() showPaginator?: boolean; + + @ContentChild('filter', { static: true }) filter!: TemplateRef | null; + @ContentChild('paginator', { static: true }) paginator!: TemplateRef | null; + @ContentChild('moreActions', { static: true }) moreActions!: TemplateRef | null; + @ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef | null; + @ContentChild('captionBox', { static: true }) captionBox!: TemplateRef | null; + @ContentChild('emptyMessageCard', { static: true }) emptyMessageCard!: TemplateRef | null; + + @Output() onEdit = new EventEmitter(); + @Output() onDelete = new EventEmitter(); + @Output() onDetails = new EventEmitter(); + @Output() onAdd = new EventEmitter(); + @Output() onChangePage = new EventEmitter(); + @Output() onThumbnailClick = new EventEmitter(); + @Output() onRefresh = new EventEmitter(); + + filterDrawerVisible = signal(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, 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, 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 | null { + const v = col.customDataModel; + return v instanceof TemplateRef ? (v as TemplateRef) : 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(); + } +} diff --git a/src/app/shared/components/pageDataList/page-data-list.component.html b/src/app/shared/components/pageDataList/page-data-list.component.html index 0772f11..5c9ee17 100644 --- a/src/app/shared/components/pageDataList/page-data-list.component.html +++ b/src/app/shared/components/pageDataList/page-data-list.component.html @@ -1,20 +1,24 @@
-
- @if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) { - +
{{ pageTitle }}
@@ -27,15 +31,21 @@ icon="pi pi-filter" variant="outlined" badgeSeverity="info" + size="small" [badge]="isFiltered ? '1' : undefined" (click)="openFilter()" > } @if (showRefresh) { - + } @if (showAdd) { - + }
} @@ -43,226 +53,100 @@
} - - - - @for (col of columns; track col.header) { - - {{ col.header }} - - } - @if (actionsCount()) { - - } - @if (expandable) { - - } - + + - - - - @for (col of columns; track col.field) { - - @if (col.type === "index") { - {{ i * (currentPage || 1) + 1 }} - } @else if (col.type === "thumbnail") { -
- @if (item[col.field]) { - + + + + + } @else { + + @if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) { + + +
+
{{ pageTitle }}
+ @if (showAdd || filter || showRefresh || moreActions) { +
+ + @if (filter) { + + } + @if (showRefresh) { + + } + @if (showAdd) { + }
- } @else if (getTemplate(col)) { - - } @else if (col.canCopy) { - - } @else { - - {{ getCell(item, col) }} - } - - } - @if (actionsCount()) { - - } - - @if (expandable) { - - - - } - - - - @if (expandable && expandColumns && expandableItemKey) { - - - -
- @if (item[expandableItemKey]?.length) { - - - - @for (col of expandColumns; track col.header) { - - {{ col.header }} - - } - - - - - - @for (col of expandColumns; track col.field) { - - {{ item.name }} - @if (col.type === "index") { - {{ i * (currentPage || 1) + 1 }} - } @else if (col.type === "thumbnail") { -
- @if (item[col.field]) { - - } -
- } @else if (getTemplate(col)) { - - } @else if (col.canCopy) { - - } @else { - - {{ getCell(item, col) }} - - } - - } - -
- - - - - - - - -
- } @else { -
- @for (col of expandColumns; track $index) { - - } -
- } -
- - +
+
} - - - - - - - + + - - - @for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) { - - @for (col of columns; track col) { - - - - } - @if (actionsCount()) { - - - - } - - } + + - - @if (showPaginator) { - - } -
+ + } + @if (filter) { { field: T extends object ? keyof T | string : string; header: string; @@ -56,19 +65,18 @@ export interface IColumn { }, imports: [ CommonModule, - TableActionRowComponent, - UikitEmptyStateComponent, TableModule, ButtonModule, SkeletonModule, PaginatorModule, DrawerModule, PaginatorComponent, - UikitCopyComponent, - KeyValueComponent, + AppPageDataListTableView, + AppPageDataListGridView, + UikitEmptyStateComponent, ], }) -export class PageDataListComponent { +export class PageDataListComponent { constructor( private host: ElementRef, private renderer: Renderer2, @@ -77,7 +85,7 @@ export class PageDataListComponent { @Input({ required: true }) pageTitle!: string; @Input() addNewCtaLabel?: string; @Input({ required: true }) columns!: IColumn[]; - @Input({ required: true }) items!: I[]; + @Input({ required: true }) items!: any[]; @Input({ required: true }) loading!: boolean; @Input() emptyPlaceholderTitle: string = 'موردی برای نمایش وجود ندارد'; @Input() emptyPlaceholderDescription?: string = ''; @@ -123,17 +131,19 @@ export class PageDataListComponent { ); // if (this.fullHeight) { // } + + this.updateViewportMode(); } - edit = (item: I) => { + edit = (item: any) => { this.onEdit.emit(item); }; - remove = (item: I) => { + remove = (item: any) => { this.onDelete.emit(item); }; - details = (item: I) => { + details = (item: any) => { this.onDetails.emit(item); }; @@ -183,84 +193,18 @@ export class PageDataListComponent { } } - getCell(item: Record, 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 | null { - const v = col.customDataModel; - return v instanceof TemplateRef ? (v as TemplateRef) : 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(); } + + isMobile = false; + + @HostListener('window:resize') + onResize() { + this.updateViewportMode(); + } + + private updateViewportMode() { + this.isMobile = typeof window !== 'undefined' && window.innerWidth <= 768; + } } diff --git a/src/assets/customize.scss b/src/assets/customize.scss index f8b932b..2a321be 100644 --- a/src/assets/customize.scss +++ b/src/assets/customize.scss @@ -1,3 +1,7 @@ :root { --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; +} diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index f801336..f142156 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,9 +1,10 @@ // Development environment configuration export const environment = { 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', - port: 5000, + port: 5001, enableLogging: false, enableDebug: false, enableNativeBridge: false, diff --git a/src/environments/environment.staging.ts b/src/environments/environment.staging.ts index 21d8a7b..0308cd5 100644 --- a/src/environments/environment.staging.ts +++ b/src/environments/environment.staging.ts @@ -1,15 +1,13 @@ -// Staging environment configuration +// Development environment configuration export const environment = { production: false, - apiBaseUrl: 'https://staging-api.yourdomain.com', - host: 'staging.yourdomain.com', - port: 443, - enableNativeBridge: false, - apiEndpoints: { - auth: '/api/auth', - users: '/api/users', - // Add more endpoints as needed - }, + // apiBaseUrl: 'http://194.59.214.243:5002', + apiBaseUrl: 'http://192.168.128.73:5002', + // apiBaseUrl: 'http://localhost:5002', + // host: 'http://194.59.214.243', + host: '0.0.0.0', + port: 5005, enableLogging: true, enableDebug: true, + enableNativeBridge: false, }; diff --git a/src/environments/environment.tis.ts b/src/environments/environment.tis.ts index ffb4abc..8718ece 100644 --- a/src/environments/environment.tis.ts +++ b/src/environments/environment.tis.ts @@ -1,7 +1,8 @@ // TIS tenant environment configuration export const environment = { 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', port: 5000, enableLogging: false, diff --git a/src/presets.ts b/src/presets.ts index 71c0c52..5a15cb7 100644 --- a/src/presets.ts +++ b/src/presets.ts @@ -32,6 +32,7 @@ const MyPreset = definePreset(Aura, { }, semantic: { primary: { + 0: '{surface.0}', 50: '{surface.50}', 100: '{surface.100}', 200: '{surface.200}', @@ -49,15 +50,15 @@ const MyPreset = definePreset(Aura, { light: { primary: { color: '{primary.950}', - contrastColor: '#ffffff', + contrastColor: '#f0f0f0', hoverColor: '{primary.800}', activeColor: '{primary.700}', }, highlight: { background: '{primary.950}', focusBackground: '{primary.700}', - color: '#ffffff', - focusColor: '#ffffff', + color: '#f0f0f0', + focusColor: '#f0f0f0', }, }, dark: { @@ -78,7 +79,7 @@ const MyPreset = definePreset(Aura, { }, primitive: { ...updateSurfacePalette({ - 0: '#ffffff', + 0: '#f0f0f0', 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', diff --git a/src/tenants/tis/app.routes.ts b/src/tenants/tis/app.routes.ts index abb2745..c625355 100644 --- a/src/tenants/tis/app.routes.ts +++ b/src/tenants/tis/app.routes.ts @@ -1,11 +1,15 @@ import { POS_ROUTES } from '@/domains/pos/routes'; +import { AppLayout } from '@/layout/default/app.layout.component'; import { AuthComponent } from '@/modules/auth/pages/auth.component'; import { Notfound } from '@/pages/notfound/notfound.component'; import { Routes } from '@angular/router'; export const appRoutes: Routes = [ - { path: '', redirectTo: 'pos', pathMatch: 'full' }, - POS_ROUTES, + { + path: '', + component: AppLayout, + children: [POS_ROUTES], + }, { path: 'auth', component: AuthComponent,