Compare commits
11
Commits
ee5a430b17
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eaf9b959f | ||
|
|
b184a0909c | ||
|
|
f278eec99a | ||
|
|
b03efef84e | ||
|
|
c0f96e401f | ||
|
|
b0d72d16f3 | ||
|
|
170a8a3f97 | ||
|
|
ffcf9f4d41 | ||
|
|
f4e596f641 | ||
|
|
8db857dbfe | ||
|
|
c6e9ec049e |
+302
@@ -0,0 +1,302 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: auto-deploy
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- dev
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DOCKER BUILD & PUSH
|
||||||
|
#
|
||||||
|
# Angular build (npm ci + ng build) Dockerfile ke andar
|
||||||
|
# multi-stage me hota hai, isliye alag build step nahi chahiye.
|
||||||
|
# ============================================================
|
||||||
|
- name: docker-build-push
|
||||||
|
image: docker
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Repository name comes from Drone
|
||||||
|
# --------------------------------------------------------
|
||||||
|
REPO="$${DRONE_REPO_NAME}"
|
||||||
|
REPO="$$(echo "$$REPO" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
||||||
|
IMAGE="harbor.lincpay:8082/lincpay/$$REPO:$${DRONE_BUILD_NUMBER}"
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "DOCKER BUILD & PUSH"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Repository : $$REPO"
|
||||||
|
echo "Build : $${DRONE_BUILD_NUMBER}"
|
||||||
|
echo "Image : $$IMAGE"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Harbor login
|
||||||
|
# --------------------------------------------------------
|
||||||
|
echo 'QVgW5QbqlKKtBU2dYApTY28Dd6J36GK3' | \
|
||||||
|
docker login harbor.lincpay:8082 \
|
||||||
|
-u 'robot$lincpay+drone' \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Build
|
||||||
|
# --------------------------------------------------------
|
||||||
|
echo "Building Docker image..."
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
-t "$$IMAGE" \
|
||||||
|
.
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Push
|
||||||
|
# --------------------------------------------------------
|
||||||
|
echo "Pushing Docker image..."
|
||||||
|
|
||||||
|
docker push "$$IMAGE"
|
||||||
|
|
||||||
|
echo "Docker image pushed successfully."
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DEPLOY
|
||||||
|
# ============================================================
|
||||||
|
- name: deploy
|
||||||
|
image: docker
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
|
||||||
|
- name: envfiles
|
||||||
|
path: /platform-env
|
||||||
|
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# REPOSITORY / SERVICE / IMAGE
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
REPO="$${DRONE_REPO_NAME}"
|
||||||
|
REPO="$$(echo "$$REPO" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
||||||
|
SERVICE_NAME="$${REPO}_app"
|
||||||
|
|
||||||
|
IMAGE="harbor.lincpay:8082/lincpay/$$REPO:$${DRONE_BUILD_NUMBER}"
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "DEPLOYMENT"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Repository : $$REPO"
|
||||||
|
echo "Service : $$SERVICE_NAME"
|
||||||
|
echo "Image : $$IMAGE"
|
||||||
|
echo "Placement : node.labels.environment == dev2"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# VALIDATION
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
if [ -z "$$REPO" ]; then
|
||||||
|
echo "ERROR: DRONE_REPO_NAME is empty."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$$SERVICE_NAME" ]; then
|
||||||
|
echo "ERROR: Service name is empty."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$$IMAGE" ]; then
|
||||||
|
echo "ERROR: Docker image is empty."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Repository verified : $$REPO"
|
||||||
|
echo "Service verified : $$SERVICE_NAME"
|
||||||
|
echo "Image verified : $$IMAGE"
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# HARBOR LOGIN
|
||||||
|
#
|
||||||
|
# Zaroori hai taaki --with-registry-auth worker node (dev2) ko
|
||||||
|
# valid Harbor credentials propagate kare — warna cross-node pull
|
||||||
|
# "No such image" / "no basic auth credentials" se fail hota hai.
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
echo 'QVgW5QbqlKKtBU2dYApTY28Dd6J36GK3' | \
|
||||||
|
docker login harbor.lincpay:8082 \
|
||||||
|
-u 'robot$lincpay+drone' \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# EXISTING SERVICE
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
if docker service ls --format '{{.Name}}' | grep -Fxq "$$SERVICE_NAME"; then
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "EXISTING SERVICE"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Service: $$SERVICE_NAME"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Check whether dev constraint already exists
|
||||||
|
# ------------------------------------------------------
|
||||||
|
|
||||||
|
CONSTRAINT_EXISTS=$$(docker service inspect "$$SERVICE_NAME" \
|
||||||
|
--format '{{range .Spec.TaskTemplate.Placement.Constraints}}{{println .}}{{end}}' \
|
||||||
|
| grep -Fx 'node.labels.environment == dev2' || true)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Add constraint if missing
|
||||||
|
# ------------------------------------------------------
|
||||||
|
|
||||||
|
if [ -z "$$CONSTRAINT_EXISTS" ]; then
|
||||||
|
|
||||||
|
echo "Constraint missing."
|
||||||
|
echo "Adding: node.labels.environment == dev2"
|
||||||
|
|
||||||
|
docker service update \
|
||||||
|
--constraint-add 'node.labels.environment == dev2' \
|
||||||
|
"$$SERVICE_NAME"
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
echo "Constraint already exists."
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Update image
|
||||||
|
# ------------------------------------------------------
|
||||||
|
|
||||||
|
echo "Updating service image..."
|
||||||
|
|
||||||
|
docker service update \
|
||||||
|
--image "$$IMAGE" \
|
||||||
|
--replicas 1 \
|
||||||
|
--with-registry-auth \
|
||||||
|
"$$SERVICE_NAME"
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# NEW SERVICE
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "NEW SERVICE"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Creating: $$SERVICE_NAME"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Traefik rule
|
||||||
|
#
|
||||||
|
# Construct it without allowing Drone to consume
|
||||||
|
# the shell backticks.
|
||||||
|
# ------------------------------------------------------
|
||||||
|
|
||||||
|
TRAEFIK_RULE='Host(`'$$REPO'.lincpay`)'
|
||||||
|
|
||||||
|
echo "Traefik rule: $$TRAEFIK_RULE"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Create service
|
||||||
|
# ------------------------------------------------------
|
||||||
|
|
||||||
|
docker service create \
|
||||||
|
--name "$$SERVICE_NAME" \
|
||||||
|
--network edge-network \
|
||||||
|
--replicas 1 \
|
||||||
|
--constraint 'node.labels.environment == dev2' \
|
||||||
|
--with-registry-auth \
|
||||||
|
--env-file /platform-env/global.env \
|
||||||
|
--env-file /platform-env/dev.env \
|
||||||
|
--label traefik.enable=true \
|
||||||
|
--label "traefik.docker.network=edge-network" \
|
||||||
|
--label "traefik.http.routers.$$REPO.rule=$$TRAEFIK_RULE" \
|
||||||
|
--label "traefik.http.services.$$REPO.loadbalancer.server.port=80" \
|
||||||
|
"$$IMAGE"
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# FINAL STATUS
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "FINAL SERVICE STATUS"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
docker service ls \
|
||||||
|
--filter "name=$$SERVICE_NAME"
|
||||||
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "PLACEMENT CONSTRAINT"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
docker service inspect "$$SERVICE_NAME" \
|
||||||
|
--format '{{json .Spec.TaskTemplate.Placement}}'
|
||||||
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "SERVICE TASKS"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
docker service ps "$$SERVICE_NAME" --no-trunc
|
||||||
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "DEPLOYMENT COMPLETE"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DOCKER SOCKET
|
||||||
|
# ============================================================
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ENVIRONMENT FILES
|
||||||
|
# ============================================================
|
||||||
|
- name: envfiles
|
||||||
|
host:
|
||||||
|
path: /opt/platform/env
|
||||||
|
|
||||||
+27
-7
@@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
###############
|
###############
|
||||||
### STAGE 1: Build app
|
### STAGE 1: Build app
|
||||||
###############
|
###############
|
||||||
@@ -9,10 +10,13 @@ ARG NPM_REGISTRY_URL=https://registry.npmjs.org/
|
|||||||
ARG BUILD_ENVIRONMENT_OPTIONS="--configuration production"
|
ARG BUILD_ENVIRONMENT_OPTIONS="--configuration production"
|
||||||
ARG PUPPETEER_DOWNLOAD_HOST_ARG=https://storage.googleapis.com
|
ARG PUPPETEER_DOWNLOAD_HOST_ARG=https://storage.googleapis.com
|
||||||
ARG PUPPETEER_CHROMIUM_REVISION_ARG=1011831
|
ARG PUPPETEER_CHROMIUM_REVISION_ARG=1011831
|
||||||
ARG PUPPETEER_SKIP_DOWNLOAD_ARG
|
# Build ko Chromium nahi chahiye — skip (warna npm ci Chromium download par atak jata hai).
|
||||||
|
ARG PUPPETEER_SKIP_DOWNLOAD_ARG=true
|
||||||
|
|
||||||
# Set the environment variable to increase Node.js memory limit
|
# Set the environment variable to increase Node.js memory limit
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
# (Mifos web-app ka production build memory-heavy hai; upstream build:prod
|
||||||
|
# 16384 use karta hai. 4096 par 'ng build' OOM ho jata hai.)
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=8192"
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
@@ -25,14 +29,22 @@ ENV PUPPETEER_DOWNLOAD_HOST=$PUPPETEER_DOWNLOAD_HOST_ARG
|
|||||||
ENV PUPPETEER_CHROMIUM_REVISION=$PUPPETEER_CHROMIUM_REVISION_ARG
|
ENV PUPPETEER_CHROMIUM_REVISION=$PUPPETEER_CHROMIUM_REVISION_ARG
|
||||||
ENV PUPPETEER_SKIP_DOWNLOAD=$PUPPETEER_SKIP_DOWNLOAD_ARG
|
ENV PUPPETEER_SKIP_DOWNLOAD=$PUPPETEER_SKIP_DOWNLOAD_ARG
|
||||||
|
|
||||||
|
# Test/e2e tooling ke bhaari binary downloads skip karo — build inhe use nahi
|
||||||
|
# karta, aur inke bina npm ci turant complete ho jata hai.
|
||||||
|
# Cypress -> ~250MB binary (CI me sabse zyada atakta hai)
|
||||||
|
# Playwright-> browser binaries
|
||||||
|
ENV CYPRESS_INSTALL_BINARY=0
|
||||||
|
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||||
|
|
||||||
COPY ./ /usr/src/app/
|
COPY ./ /usr/src/app/
|
||||||
|
|
||||||
RUN npm cache clear --force
|
RUN npm config set fetch-retry-maxtimeout 120000 \
|
||||||
|
&& npm config set registry $NPM_REGISTRY_URL --location=global
|
||||||
|
|
||||||
RUN npm config set fetch-retry-maxtimeout 120000
|
# BuildKit cache mount: npm ka download cache builds ke beech persist hota hai,
|
||||||
RUN npm config set registry $NPM_REGISTRY_URL --location=global
|
# isliye har build par saare packages dobara download nahi hote (bahut fast).
|
||||||
|
# 'npm cache clear --force' hata diya — wo har build par cache wipe kar deta tha.
|
||||||
RUN npm ci
|
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund
|
||||||
|
|
||||||
RUN sh -c "ng build --output-path=/dist $BUILD_ENVIRONMENT_OPTIONS"
|
RUN sh -c "ng build --output-path=/dist $BUILD_ENVIRONMENT_OPTIONS"
|
||||||
|
|
||||||
@@ -41,6 +53,14 @@ RUN sh -c "ng build --output-path=/dist $BUILD_ENVIRONMENT_OPTIONS"
|
|||||||
###############
|
###############
|
||||||
FROM $NGINX_IMAGE
|
FROM $NGINX_IMAGE
|
||||||
|
|
||||||
|
# envsubst (gettext) alpine-slim me nahi hota — runtime env.js generation ke
|
||||||
|
# liye zaroori hai, isliye explicitly install kar rahe hain.
|
||||||
|
RUN apk add --no-cache gettext
|
||||||
|
|
||||||
|
# Static-only nginx config (SPA fallback, no API proxy). Stock default.conf
|
||||||
|
# ko replace karta hai taaki deep-link refresh par 404 na aaye.
|
||||||
|
COPY nginx.spa.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
COPY --from=builder /dist/browser /usr/share/nginx/html
|
COPY --from=builder /dist/browser /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Static-only nginx config for the CBSNexora Angular UI.
|
||||||
|
# NOTE: No API reverse-proxy here by design — the browser calls the Fineract
|
||||||
|
# backend directly via FINERACT_API_URL (env.js). nginx sirf static files
|
||||||
|
# serve karta hai + Angular SPA routing handle karta hai.
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# ---- Compression ----
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json
|
||||||
|
application/xml image/svg+xml font/woff font/woff2;
|
||||||
|
|
||||||
|
# ---- Runtime env: env.js aur index.html kabhi cache na ho ----
|
||||||
|
# (env.js har container start par envsubst se regenerate hoti hai;
|
||||||
|
# index.html hashed asset filenames point karti hai)
|
||||||
|
location = /assets/env.js {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Hashed static assets: lambe time cache ----
|
||||||
|
location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Angular SPA fallback: har route index.html par ----
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,10 +25,68 @@ import { Form15Kind, Form15ghRecord } from './form-15gh/form-15gh.component';
|
|||||||
import { HypothecationRecord } from './hypothecation/hypothecation.component';
|
import { HypothecationRecord } from './hypothecation/hypothecation.component';
|
||||||
import { NocRecord } from './noc/noc.component';
|
import { NocRecord } from './noc/noc.component';
|
||||||
|
|
||||||
const BANK_NAME = 'Nexora Co-operative Bank Ltd.';
|
/**
|
||||||
const BANK_ADDRESS = 'Head Office, 4th Floor, Nexora Tower, Mumbai 400051';
|
* The issuing bank's own identity, as set on the Institution Profile (Admin > Organization) — this
|
||||||
const BANK_REG = 'RBI Lic. No. NX-2018/MH/482';
|
* file is shared by every certificate type (loan interest, share, balance, Form 60/15G/15H,
|
||||||
const BANK_CONTACT = 'info@nexorabank.in · 022 6789 0000';
|
* hypothecation, NOC) and every tenant this app runs for, so nothing here can be a fixed name.
|
||||||
|
* These defaults only cover the moment before whichever screen opened this module has fetched the
|
||||||
|
* real profile and called {@link setBankIdentity}; they are never meant to reach a printed PDF.
|
||||||
|
*/
|
||||||
|
const bankIdentity = {
|
||||||
|
name: 'Bank',
|
||||||
|
address: '',
|
||||||
|
regNo: '',
|
||||||
|
contact: '',
|
||||||
|
/** City/state/country line used by the Share Certificate's own letterhead style. */
|
||||||
|
location: '',
|
||||||
|
/** Short label for the circular seal graphic — two lines, e.g. bank's short name + type. */
|
||||||
|
sealPrimary: 'BANK',
|
||||||
|
sealSecondary: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Called once the caller has fetched the real Institution Profile, so every certificate built
|
||||||
|
* after this point — of any type — carries the actual bank's own details. */
|
||||||
|
export function setBankIdentity(profile: {
|
||||||
|
bankName?: string;
|
||||||
|
statementHeaderName?: string;
|
||||||
|
shortName?: string;
|
||||||
|
bankType?: string;
|
||||||
|
addressLine1?: string;
|
||||||
|
addressLine2?: string;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
country?: string;
|
||||||
|
licenseNumber?: string;
|
||||||
|
contactEmail?: string;
|
||||||
|
contactPhone?: string;
|
||||||
|
}): void {
|
||||||
|
bankIdentity.name = profile.statementHeaderName || profile.bankName || bankIdentity.name;
|
||||||
|
bankIdentity.address = [
|
||||||
|
profile.addressLine1,
|
||||||
|
profile.addressLine2,
|
||||||
|
profile.city
|
||||||
|
]
|
||||||
|
.filter((p) => !!p && String(p).trim())
|
||||||
|
.join(', ');
|
||||||
|
bankIdentity.regNo = profile.licenseNumber ? `RBI Lic. No. ${profile.licenseNumber}` : '';
|
||||||
|
bankIdentity.contact = [
|
||||||
|
profile.contactEmail,
|
||||||
|
profile.contactPhone
|
||||||
|
]
|
||||||
|
.filter((p) => !!p && String(p).trim())
|
||||||
|
.join(' · ');
|
||||||
|
bankIdentity.location = [
|
||||||
|
profile.city,
|
||||||
|
profile.state,
|
||||||
|
profile.country
|
||||||
|
]
|
||||||
|
.filter((p) => !!p && String(p).trim())
|
||||||
|
.join(', ');
|
||||||
|
bankIdentity.sealPrimary = (profile.shortName || profile.bankName || bankIdentity.name).toUpperCase();
|
||||||
|
bankIdentity.sealSecondary = profile.bankType
|
||||||
|
? profile.bankType.replace(/_/g, ' ').toUpperCase() + (/BANK$/i.test(profile.bankType) ? '' : ' BANK')
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
const C_TEXT: [number, number, number] = [
|
const C_TEXT: [number, number, number] = [
|
||||||
15,
|
15,
|
||||||
@@ -141,14 +199,24 @@ function paintLetterhead(
|
|||||||
c.doc.setFont('times', 'bold');
|
c.doc.setFont('times', 'bold');
|
||||||
c.doc.setFontSize(19);
|
c.doc.setFontSize(19);
|
||||||
setText(c, c.accent);
|
setText(c, c.accent);
|
||||||
c.doc.text(BANK_NAME, c.pageW / 2, y, { align: 'center' });
|
c.doc.text(bankIdentity.name, c.pageW / 2, y, { align: 'center' });
|
||||||
y += 5.5;
|
y += 5.5;
|
||||||
c.doc.setFont('helvetica', 'normal');
|
c.doc.setFont('helvetica', 'normal');
|
||||||
c.doc.setFontSize(9);
|
c.doc.setFontSize(9);
|
||||||
setText(c, C_MUTED);
|
setText(c, C_MUTED);
|
||||||
c.doc.text(BANK_ADDRESS, c.pageW / 2, y, { align: 'center' });
|
if (bankIdentity.address) {
|
||||||
|
c.doc.text(bankIdentity.address, c.pageW / 2, y, { align: 'center' });
|
||||||
y += 4;
|
y += 4;
|
||||||
c.doc.text(`${BANK_REG} · ${BANK_CONTACT}`, c.pageW / 2, y, { align: 'center' });
|
}
|
||||||
|
const regContactLine = [
|
||||||
|
bankIdentity.regNo,
|
||||||
|
bankIdentity.contact
|
||||||
|
]
|
||||||
|
.filter((p) => !!p)
|
||||||
|
.join(' · ');
|
||||||
|
if (regContactLine) {
|
||||||
|
c.doc.text(regContactLine, c.pageW / 2, y, { align: 'center' });
|
||||||
|
}
|
||||||
y += 5;
|
y += 5;
|
||||||
setDraw(c, C_BORDER);
|
setDraw(c, C_BORDER);
|
||||||
c.doc.setLineWidth(0.3);
|
c.doc.setLineWidth(0.3);
|
||||||
@@ -350,11 +418,10 @@ function paintSignoff(c: PdfCtx, y: number, branch: string): number {
|
|||||||
c.doc.setFont('helvetica', 'bold');
|
c.doc.setFont('helvetica', 'bold');
|
||||||
c.doc.setFontSize(7);
|
c.doc.setFontSize(7);
|
||||||
setText(c, c.accent);
|
setText(c, c.accent);
|
||||||
c.doc.text('NEXORA', cx, cy - 1, { align: 'center' });
|
c.doc.text(bankIdentity.sealPrimary, cx, cy - 1, { align: 'center' });
|
||||||
c.doc.setFontSize(5);
|
c.doc.setFontSize(5);
|
||||||
setText(c, C_MUTED);
|
setText(c, C_MUTED);
|
||||||
c.doc.text('CO-OPERATIVE', cx, cy + 3, { align: 'center' });
|
c.doc.text(bankIdentity.sealSecondary, cx, cy + 4, { align: 'center' });
|
||||||
c.doc.text('BANK', cx, cy + 5.5, { align: 'center' });
|
|
||||||
|
|
||||||
return y + 28;
|
return y + 28;
|
||||||
}
|
}
|
||||||
@@ -376,7 +443,7 @@ function paintFooter(c: PdfCtx, y: number): void {
|
|||||||
c.doc.setFontSize(8);
|
c.doc.setFontSize(8);
|
||||||
setText(c, C_MUTED);
|
setText(c, C_MUTED);
|
||||||
c.doc.text(
|
c.doc.text(
|
||||||
`Issued by ${BANK_NAME} on ${formatLongDate(today())}. Computer generated — does not require physical signature for record purposes.`,
|
`Issued by ${bankIdentity.name} on ${formatLongDate(today())}. Computer generated — does not require physical signature for record purposes.`,
|
||||||
c.pageW / 2,
|
c.pageW / 2,
|
||||||
y,
|
y,
|
||||||
{ align: 'center' }
|
{ align: 'center' }
|
||||||
@@ -609,13 +676,15 @@ export function buildShareCertificatePdf(r: ShareCertificateRecord): jsPDF {
|
|||||||
doc.setFont('times', 'bold');
|
doc.setFont('times', 'bold');
|
||||||
doc.setFontSize(22);
|
doc.setFontSize(22);
|
||||||
doc.setTextColor(darkBlue[0], darkBlue[1], darkBlue[2]);
|
doc.setTextColor(darkBlue[0], darkBlue[1], darkBlue[2]);
|
||||||
doc.text('NEXORA CO-OPERATIVE BANK LTD.', pageW / 2, y, { align: 'center' });
|
doc.text(bankIdentity.name.toUpperCase(), pageW / 2, y, { align: 'center' });
|
||||||
|
|
||||||
|
if (bankIdentity.location) {
|
||||||
y += 5;
|
y += 5;
|
||||||
doc.setFont('times', 'italic');
|
doc.setFont('times', 'italic');
|
||||||
doc.setFontSize(10.5);
|
doc.setFontSize(10.5);
|
||||||
doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]);
|
doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]);
|
||||||
doc.text('Mumbai, Maharashtra, India', pageW / 2, y, { align: 'center' });
|
doc.text(bankIdentity.location, pageW / 2, y, { align: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
y += 4;
|
y += 4;
|
||||||
doc.setDrawColor(242, 237, 213); // light gold separator
|
doc.setDrawColor(242, 237, 213); // light gold separator
|
||||||
@@ -685,7 +754,7 @@ export function buildShareCertificatePdf(r: ShareCertificateRecord): jsPDF {
|
|||||||
doc.setFontSize(11);
|
doc.setFontSize(11);
|
||||||
doc.setTextColor(textDark[0], textDark[1], textDark[2]);
|
doc.setTextColor(textDark[0], textDark[1], textDark[2]);
|
||||||
|
|
||||||
const bodyText = `This is to certify that ${r.memberName} is the registered holder of ${r.shareCount.toLocaleString('en-IN')} (${classLabel}) shares of Rs. ${r.faceValue.toLocaleString('en-IN')} each, fully paid up, in Nexora Co-operative Bank Ltd., subject to the memorandum, articles of association, and bye-laws of the bank.`;
|
const bodyText = `This is to certify that ${r.memberName} is the registered holder of ${r.shareCount.toLocaleString('en-IN')} (${classLabel}) shares of Rs. ${r.faceValue.toLocaleString('en-IN')} each, fully paid up, in ${bankIdentity.name}, subject to the memorandum, articles of association, and bye-laws of the bank.`;
|
||||||
const bodyLines = doc.splitTextToSize(bodyText, pageW - 50);
|
const bodyLines = doc.splitTextToSize(bodyText, pageW - 50);
|
||||||
for (const line of bodyLines) {
|
for (const line of bodyLines) {
|
||||||
doc.text(line, pageW / 2, y, { align: 'center' });
|
doc.text(line, pageW / 2, y, { align: 'center' });
|
||||||
@@ -763,7 +832,7 @@ export function buildShareCertificatePdf(r: ShareCertificateRecord): jsPDF {
|
|||||||
|
|
||||||
// Row 1
|
// Row 1
|
||||||
drawTableCell(25, y, tableColW, tableRowH, 'Branch', true);
|
drawTableCell(25, y, tableColW, tableRowH, 'Branch', true);
|
||||||
drawTableCell(25 + tableColW, y, tableColW, tableRowH, r.branch || 'Mumbai Main Branch', false);
|
drawTableCell(25 + tableColW, y, tableColW, tableRowH, r.branch || 'Head Office', false);
|
||||||
drawTableCell(25 + tableColW * 2, y, tableColW, tableRowH, 'Member Since', true);
|
drawTableCell(25 + tableColW * 2, y, tableColW, tableRowH, 'Member Since', true);
|
||||||
drawTableCell(25 + tableColW * 3, y, tableColW, tableRowH, formatLongDate(r.joiningDate), false);
|
drawTableCell(25 + tableColW * 3, y, tableColW, tableRowH, formatLongDate(r.joiningDate), false);
|
||||||
y += tableRowH;
|
y += tableRowH;
|
||||||
@@ -1168,7 +1237,7 @@ export function buildHypothecationPdf(r: HypothecationRecord): jsPDF {
|
|||||||
y = paintPara(
|
y = paintPara(
|
||||||
c,
|
c,
|
||||||
y,
|
y,
|
||||||
`This is to certify that the asset described below stands hypothecated to ${BANK_NAME} as security for the captioned loan facility extended to ${r.customerName}, and the bank holds the first and exclusive charge over the said asset until the loan is fully discharged.`
|
`This is to certify that the asset described below stands hypothecated to ${bankIdentity.name} as security for the captioned loan facility extended to ${r.customerName}, and the bank holds the first and exclusive charge over the said asset until the loan is fully discharged.`
|
||||||
);
|
);
|
||||||
|
|
||||||
const rows: [string, string, string, string][] = [
|
const rows: [string, string, string, string][] = [
|
||||||
|
|||||||
+2
-2
@@ -684,9 +684,9 @@
|
|||||||
<div class="cpd-seal">
|
<div class="cpd-seal">
|
||||||
<div class="cpd-seal-ring">
|
<div class="cpd-seal-ring">
|
||||||
<div class="cpd-seal-inner">
|
<div class="cpd-seal-inner">
|
||||||
<div class="cpd-seal-bank">NEXORA</div>
|
<div class="cpd-seal-bank">{{ bankSealPrimary }}</div>
|
||||||
<div class="cpd-seal-line"></div>
|
<div class="cpd-seal-line"></div>
|
||||||
<div class="cpd-seal-sub">CO-OPERATIVE BANK</div>
|
<div class="cpd-seal-sub">{{ bankSealSecondary }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+32
-2
@@ -575,8 +575,19 @@
|
|||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ───────── Print — only the page, no toolbar / dialog chrome ───────── */
|
/* ───────── Print — only the page, no toolbar / dialog chrome ─────────
|
||||||
|
This dialog is opened with `maxHeight: '92vh'`, which Angular Material carries onto the
|
||||||
|
overlay pane / .mat-mdc-dialog-container as an inline max-height + overflow:auto clip.
|
||||||
|
`position: absolute` still lives inside that clip and gets cut off — only the on-screen
|
||||||
|
scroll position prints. `position: fixed` escapes it: a fixed element's containing block is
|
||||||
|
the viewport itself, not the clipped ancestor (the classic "fixed child ignores an
|
||||||
|
overflow:hidden parent" behaviour), as long as nothing between here and the viewport sets a
|
||||||
|
transform/filter — the CDK overlay chrome doesn't. */
|
||||||
@media print {
|
@media print {
|
||||||
|
body > :not(.cdk-overlay-container) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@@ -585,11 +596,29 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cdk-overlay-container,
|
||||||
|
.cdk-global-overlay-wrapper,
|
||||||
|
.cdk-overlay-pane,
|
||||||
|
.mat-mdc-dialog-container,
|
||||||
|
.mdc-dialog__surface {
|
||||||
|
position: static !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
max-height: none !important;
|
||||||
|
height: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.cpd-root {
|
.cpd-root {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpd-page {
|
.cpd-page {
|
||||||
@@ -599,6 +628,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-top: 4px solid var(--cpd-blue-d);
|
border-top: 4px solid var(--cpd-blue-d);
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
&.cpd-page--share {
|
&.cpd-page--share {
|
||||||
border-top-color: var(--cpd-amber);
|
border-top-color: var(--cpd-amber);
|
||||||
|
|||||||
+47
-7
@@ -6,12 +6,13 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatTooltip } from '@angular/material/tooltip';
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
import { OrganizationService } from 'app/organization/organization.service';
|
||||||
|
|
||||||
import { FyInterestBreakup, LoanInterestRecord } from '../loan-interest/loan-interest.component';
|
import { FyInterestBreakup, LoanInterestRecord } from '../loan-interest/loan-interest.component';
|
||||||
import { ShareCertificateRecord } from '../share-certificate/share-certificate.component';
|
import { ShareCertificateRecord } from '../share-certificate/share-certificate.component';
|
||||||
@@ -27,7 +28,8 @@ import {
|
|||||||
downloadHypothecationPdf,
|
downloadHypothecationPdf,
|
||||||
downloadLoanInterestPdf,
|
downloadLoanInterestPdf,
|
||||||
downloadNocPdf,
|
downloadNocPdf,
|
||||||
downloadShareCertificatePdf
|
downloadShareCertificatePdf,
|
||||||
|
setBankIdentity
|
||||||
} from '../certificate-pdf';
|
} from '../certificate-pdf';
|
||||||
|
|
||||||
export interface InterestPreviewPayload {
|
export interface InterestPreviewPayload {
|
||||||
@@ -70,14 +72,52 @@ export interface CertificatePreviewDialogData {
|
|||||||
MatTooltip
|
MatTooltip
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CertificatePreviewDialogComponent {
|
export class CertificatePreviewDialogComponent implements OnInit {
|
||||||
private dialogRef = inject(MatDialogRef<CertificatePreviewDialogComponent>);
|
private dialogRef = inject(MatDialogRef<CertificatePreviewDialogComponent>);
|
||||||
|
private organizationService = inject(OrganizationService);
|
||||||
data: CertificatePreviewDialogData = inject(MAT_DIALOG_DATA);
|
data: CertificatePreviewDialogData = inject(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
readonly bankName = 'Nexora Co-operative Bank Ltd.';
|
/** The issuing bank's own details, fetched from the Institution Profile — this dialog is shared
|
||||||
readonly bankAddress = 'Head Office, 4th Floor, Nexora Tower, Mumbai 400051';
|
* by every certificate type and every tenant, so nothing here is a fixed name. */
|
||||||
readonly bankRegNo = 'RBI Lic. No. NX-2018/MH/482';
|
bankName = 'Bank';
|
||||||
readonly bankContact = 'info@nexorabank.in · 022 6789 0000';
|
bankAddress = '';
|
||||||
|
bankRegNo = '';
|
||||||
|
bankContact = '';
|
||||||
|
bankSealPrimary = 'BANK';
|
||||||
|
bankSealSecondary = '';
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.organizationService.getInstitutionProfile().subscribe({
|
||||||
|
next: (profile: any) => {
|
||||||
|
this.bankName = profile?.statementHeaderName || profile?.bankName || this.bankName;
|
||||||
|
this.bankAddress = [
|
||||||
|
profile?.addressLine1,
|
||||||
|
profile?.addressLine2,
|
||||||
|
profile?.city
|
||||||
|
]
|
||||||
|
.filter((p: any) => !!p && String(p).trim())
|
||||||
|
.join(', ');
|
||||||
|
this.bankRegNo = profile?.licenseNumber ? `RBI Lic. No. ${profile.licenseNumber}` : '';
|
||||||
|
this.bankContact = [
|
||||||
|
profile?.contactEmail,
|
||||||
|
profile?.contactPhone
|
||||||
|
]
|
||||||
|
.filter((p: any) => !!p && String(p).trim())
|
||||||
|
.join(' · ');
|
||||||
|
this.bankSealPrimary = (profile?.shortName || this.bankName).toUpperCase();
|
||||||
|
this.bankSealSecondary = profile?.bankType
|
||||||
|
? profile.bankType.replace(/_/g, ' ').toUpperCase() + (/BANK$/i.test(profile.bankType) ? '' : ' BANK')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Every certificate PDF this dialog can build (Download button below, or the list page's
|
||||||
|
// own download icon opened without previewing first) reads the same shared identity, so
|
||||||
|
// set it here once the real profile is in — never leave a PDF carrying the placeholder.
|
||||||
|
setBankIdentity(profile || {});
|
||||||
|
},
|
||||||
|
// A missing/incomplete profile must not stop the certificate from rendering.
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get isInterest(): boolean {
|
get isInterest(): boolean {
|
||||||
return this.data.kind === 'interest';
|
return this.data.kind === 'interest';
|
||||||
|
|||||||
+11
-1
@@ -17,8 +17,9 @@ import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
|
|||||||
|
|
||||||
import { SharesService } from '../../../../shares/shares.service';
|
import { SharesService } from '../../../../shares/shares.service';
|
||||||
import { ClientsService } from '../../../../clients/clients.service';
|
import { ClientsService } from '../../../../clients/clients.service';
|
||||||
|
import { OrganizationService } from '../../../../organization/organization.service';
|
||||||
import { ShareCertificateDialogComponent } from '../../../../shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component';
|
import { ShareCertificateDialogComponent } from '../../../../shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component';
|
||||||
import { downloadShareCertificatePdf } from '../../certificate-pdf';
|
import { downloadShareCertificatePdf, setBankIdentity } from '../../certificate-pdf';
|
||||||
import { DscSignService } from '../../../dsc-sign-dialog/dsc-sign.service';
|
import { DscSignService } from '../../../dsc-sign-dialog/dsc-sign.service';
|
||||||
import type { ShareClass, ShareCertificateRecord } from '../share-certificate.component';
|
import type { ShareClass, ShareCertificateRecord } from '../share-certificate.component';
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ export class RealShareAccountsViewComponent implements OnInit {
|
|||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
private sharesService = inject(SharesService);
|
private sharesService = inject(SharesService);
|
||||||
private clientsService = inject(ClientsService);
|
private clientsService = inject(ClientsService);
|
||||||
|
private organizationService = inject(OrganizationService);
|
||||||
private dscSign = inject(DscSignService);
|
private dscSign = inject(DscSignService);
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -70,6 +72,14 @@ export class RealShareAccountsViewComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadRealAccounts();
|
this.loadRealAccounts();
|
||||||
|
|
||||||
|
// The Download button below builds the PDF directly (no preview dialog in between), so this
|
||||||
|
// view must set the shared bank identity itself rather than relying on some other screen
|
||||||
|
// having opened first.
|
||||||
|
this.organizationService.getInstitutionProfile().subscribe({
|
||||||
|
next: (profile: any) => setBankIdentity(profile || {}),
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadRealAccounts(): void {
|
private loadRealAccounts(): void {
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import {
|
|||||||
CertificatePreviewDialogComponent,
|
CertificatePreviewDialogComponent,
|
||||||
CertificatePreviewDialogData
|
CertificatePreviewDialogData
|
||||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||||
import { downloadShareCertificatePdf } from '../certificate-pdf';
|
import { downloadShareCertificatePdf, setBankIdentity } from '../certificate-pdf';
|
||||||
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
||||||
|
import { OrganizationService } from '../../../organization/organization.service';
|
||||||
import { RealShareAccountsViewComponent } from './real-share-accounts-view/real-share-accounts-view.component';
|
import { RealShareAccountsViewComponent } from './real-share-accounts-view/real-share-accounts-view.component';
|
||||||
|
|
||||||
export type ShareClass = 'ORDINARY' | 'PREFERENCE';
|
export type ShareClass = 'ORDINARY' | 'PREFERENCE';
|
||||||
@@ -62,6 +63,7 @@ export class ShareCertificateComponent implements OnInit {
|
|||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
private snack = inject(MatSnackBar);
|
private snack = inject(MatSnackBar);
|
||||||
private dscSign = inject(DscSignService);
|
private dscSign = inject(DscSignService);
|
||||||
|
private organizationService = inject(OrganizationService);
|
||||||
|
|
||||||
/** Toggles between the static sample table and the live m_share_account view. */
|
/** Toggles between the static sample table and the live m_share_account view. */
|
||||||
viewMode: 'demo' | 'real' = 'demo';
|
viewMode: 'demo' | 'real' = 'demo';
|
||||||
@@ -80,6 +82,13 @@ export class ShareCertificateComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.records = seedShareCertificates();
|
this.records = seedShareCertificates();
|
||||||
|
|
||||||
|
// Download here builds the PDF directly, without necessarily opening the preview dialog first,
|
||||||
|
// so this list must set the shared bank identity itself.
|
||||||
|
this.organizationService.getInstitutionProfile().subscribe({
|
||||||
|
next: (profile: any) => setBankIdentity(profile || {}),
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Derived list ──
|
// ── Derived list ──
|
||||||
|
|||||||
+7
-5
@@ -21,8 +21,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="certificate-header">
|
<div class="certificate-header">
|
||||||
<div class="bank-name">Nexora Co-operative Bank Ltd.</div>
|
<div class="bank-name">{{ bankName }}</div>
|
||||||
<div class="bank-sub">Mumbai, Maharashtra, India</div>
|
@if (bankLocation) {
|
||||||
|
<div class="bank-sub">{{ bankLocation }}</div>
|
||||||
|
}
|
||||||
<div class="cert-title">SHARE CERTIFICATE</div>
|
<div class="cert-title">SHARE CERTIFICATE</div>
|
||||||
<div class="cert-subtitle">Issued under the Common Seal of the Bank</div>
|
<div class="cert-subtitle">Issued under the Common Seal of the Bank</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,8 +53,8 @@
|
|||||||
<strong class="highlight-text">{{
|
<strong class="highlight-text">{{
|
||||||
sharesAccountData.currentMarketPrice | currency: sharesAccountData.currency.code : 'symbol-narrow'
|
sharesAccountData.currentMarketPrice | currency: sharesAccountData.currency.code : 'symbol-narrow'
|
||||||
}}</strong>
|
}}</strong>
|
||||||
each, fully paid up, in <strong class="highlight-text">Nexora Co-operative Bank Ltd.</strong>, subject to
|
each, fully paid up, in <strong class="highlight-text">{{ bankName }}</strong
|
||||||
the memorandum, articles of association, and bye-laws of the bank.
|
>, subject to the memorandum, articles of association, and bye-laws of the bank.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="issued-to-details">
|
<div class="issued-to-details">
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="lbl-col">Branch</td>
|
<td class="lbl-col">Branch</td>
|
||||||
<td class="val-col">{{ clientData?.officeName || 'Mumbai Main Branch' }}</td>
|
<td class="val-col">{{ clientData?.officeName || 'Head Office' }}</td>
|
||||||
<td class="lbl-col">Member Since</td>
|
<td class="lbl-col">Member Since</td>
|
||||||
<td class="val-col">
|
<td class="val-col">
|
||||||
{{
|
{{
|
||||||
|
|||||||
+18
@@ -140,6 +140,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.meta-value {
|
.meta-value {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #322f2b;
|
color: #322f2b;
|
||||||
@@ -226,12 +228,19 @@
|
|||||||
|
|
||||||
.lbl-col {
|
.lbl-col {
|
||||||
background-color: #f7f5ee;
|
background-color: #f7f5ee;
|
||||||
|
font-family: Georgia, 'Times New Roman', Times, serif;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #7b6f63;
|
color: #7b6f63;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every figure a member actually needs to read at a glance — share counts, amounts, dates, the
|
||||||
|
// resolution number — sits in this column. Georgia's numerals kept blurring into each other at
|
||||||
|
// this size, so values get a plain sans-serif with tabular figures instead; the label column
|
||||||
|
// keeps the certificate's own serif since it's static text, never a number to misread.
|
||||||
.val-col {
|
.val-col {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
color: #322f2b;
|
color: #322f2b;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
@@ -242,6 +251,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lbl-col-highlight {
|
.lbl-col-highlight {
|
||||||
|
font-family: Georgia, 'Times New Roman', Times, serif;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #0b3c5d;
|
color: #0b3c5d;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -251,6 +261,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.val-col-highlight {
|
.val-col-highlight {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #b8860b; /* Dark Goldenrod */
|
color: #b8860b; /* Dark Goldenrod */
|
||||||
@@ -270,6 +282,7 @@
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border: 1px solid #ede8db;
|
border: 1px solid #ede8db;
|
||||||
background-color: #f7f5ee;
|
background-color: #f7f5ee;
|
||||||
|
font-family: Georgia, 'Times New Roman', Times, serif;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #7b6f63;
|
color: #7b6f63;
|
||||||
@@ -277,9 +290,14 @@
|
|||||||
letter-spacing: 0.6px;
|
letter-spacing: 0.6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same reasoning as .val-col above: every cell here is a figure someone has to check against the
|
||||||
|
// register, so it reads in a plain sans-serif with tabular numerals rather than the certificate's
|
||||||
|
// decorative serif.
|
||||||
td {
|
td {
|
||||||
padding: 9px 12px;
|
padding: 9px 12px;
|
||||||
border: 1px solid #ede8db;
|
border: 1px solid #ede8db;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #322f2b;
|
color: #322f2b;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -16,6 +16,7 @@ import {
|
|||||||
MatDialogClose
|
MatDialogClose
|
||||||
} from '@angular/material/dialog';
|
} from '@angular/material/dialog';
|
||||||
import { ClientsService } from 'app/clients/clients.service';
|
import { ClientsService } from 'app/clients/clients.service';
|
||||||
|
import { OrganizationService } from 'app/organization/organization.service';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||||
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
|
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
|
||||||
@@ -44,6 +45,7 @@ export class ShareCertificateDialogComponent implements OnInit {
|
|||||||
dialogRef = inject<MatDialogRef<ShareCertificateDialogComponent>>(MatDialogRef);
|
dialogRef = inject<MatDialogRef<ShareCertificateDialogComponent>>(MatDialogRef);
|
||||||
data = inject(MAT_DIALOG_DATA);
|
data = inject(MAT_DIALOG_DATA);
|
||||||
clientsService = inject(ClientsService);
|
clientsService = inject(ClientsService);
|
||||||
|
private organizationService = inject(OrganizationService);
|
||||||
|
|
||||||
sharesAccountData: any;
|
sharesAccountData: any;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@@ -53,6 +55,11 @@ export class ShareCertificateDialogComponent implements OnInit {
|
|||||||
panNumber: string = 'Not Available';
|
panNumber: string = 'Not Available';
|
||||||
memberId: string = '';
|
memberId: string = '';
|
||||||
|
|
||||||
|
/** The issuing bank's own name/location, as set on the Institution Profile — never a fixed name,
|
||||||
|
* since this certificate template is shared by every tenant this app runs for. */
|
||||||
|
bankName: string = 'Bank';
|
||||||
|
bankLocation: string = '';
|
||||||
|
|
||||||
certificateNo: string = '';
|
certificateNo: string = '';
|
||||||
folioNo: string = '';
|
folioNo: string = '';
|
||||||
shareNumbersRange: string = '';
|
shareNumbersRange: string = '';
|
||||||
@@ -155,6 +162,24 @@ export class ShareCertificateDialogComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
error: () => {}
|
error: () => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch the issuing bank's own name/location from the Institution Profile, set once under
|
||||||
|
// Admin > Organization for whichever bank this instance runs for — never a fixed name baked
|
||||||
|
// into the certificate itself.
|
||||||
|
this.organizationService.getInstitutionProfile().subscribe({
|
||||||
|
next: (profile: any) => {
|
||||||
|
this.bankName = profile?.statementHeaderName || profile?.bankName || this.bankName;
|
||||||
|
this.bankLocation = [
|
||||||
|
profile?.city,
|
||||||
|
profile?.state,
|
||||||
|
profile?.country
|
||||||
|
]
|
||||||
|
.filter((p) => !!p && String(p).trim())
|
||||||
|
.join(', ');
|
||||||
|
},
|
||||||
|
// A missing/incomplete profile must not stop the certificate from rendering.
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260914',
|
'version': '260914',
|
||||||
'hash': 'ab58d7f7'
|
'hash': 'ee5a430b'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
+36
-1
@@ -978,9 +978,18 @@ mat-card {
|
|||||||
mifosx-breadcrumb,
|
mifosx-breadcrumb,
|
||||||
mifosx-footer,
|
mifosx-footer,
|
||||||
.sidebar-panel,
|
.sidebar-panel,
|
||||||
|
.sidebar-handle,
|
||||||
.mat-mdc-tab-header,
|
.mat-mdc-tab-header,
|
||||||
.mat-stepper-vertical-line,
|
.mat-stepper-vertical-line,
|
||||||
.mat-step-header {
|
.mat-step-header,
|
||||||
|
/* The real 2-row nav menu (Client Master / Approvals / Cashier … and the row below it) and the
|
||||||
|
live KPI ticker live directly in shell.component.html as .main-header / .kpi-ticker, not
|
||||||
|
inside <mifosx-toolbar> — hiding only the toolbar left this menu printing on every page. */
|
||||||
|
.main-header,
|
||||||
|
.kpi-ticker,
|
||||||
|
.cashier-nav,
|
||||||
|
.gl-backdrop,
|
||||||
|
.gl-mega-menu {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,6 +997,32 @@ mat-card {
|
|||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The whole app shell scrolls inside CDK's own clipped viewport —
|
||||||
|
.mat-drawer-container { overflow: hidden } and .mat-drawer-content { overflow: auto; height: 100% }
|
||||||
|
— so without freeing these too, print is still capped to one screen's worth of the page
|
||||||
|
regardless of the tab-body fix below. */
|
||||||
|
#mifosx-shell-container,
|
||||||
|
.mat-drawer-container,
|
||||||
|
.mat-drawer-content,
|
||||||
|
mat-sidenav-content.sidenav {
|
||||||
|
position: static !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Angular Material clips a tab's content to a JS-measured on-screen height via
|
||||||
|
`.mat-mdc-tab-body-wrapper { overflow: hidden }` (for the tab-switch height animation) and
|
||||||
|
`.mat-mdc-tab-body-content { height: 100%; overflow: auto }` — printing only what fit that
|
||||||
|
clip left every account/tab page (Shares Account General included) blank apart from a
|
||||||
|
stray border. Printing needs the full content, not the animated viewport. */
|
||||||
|
.mat-mdc-tab-body-wrapper,
|
||||||
|
.mat-mdc-tab-body,
|
||||||
|
.mat-mdc-tab-body-content {
|
||||||
|
position: static !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user