diff --git a/matomo-documentation.md b/matomo-documentation.md new file mode 100644 index 0000000..114fda8 --- /dev/null +++ b/matomo-documentation.md @@ -0,0 +1,307 @@ +# Matomo Tracking System Documentation + +## Table of Contents + +1. [Overview](#overview) +2. [How It Works](#how-it-works) +3. [Setup and Configuration](#setup-and-configuration) +4. [Adding Tracking to Components](#adding-tracking-to-components) +5. [Adding a New Website to Matomo](#adding-a-new-website-to-matomo) +6. [API Reference](#api-reference) +7. [Best Practices](#tips-we-can-use-while-adding-tracking) +8. [Troubleshooting](#troubleshooting) + +## Overview + +The Amrit tracking system is a flexible analytics solution that supports multiple tracking providers (Matomo, Google Analytics) with a unified interface. It automatically tracks page views, user interactions, and custom events throughout your Angular application. + +### Key Features + +- Multiple provider support (Matomo, Google Analytics) +- Automatic page view tracking +- User identification tracking +- Environment-based configuration +- Built-in error handling and fallbacks +- Rich event tracking capabilities + +## How It Works + +### System Architecture Diagram + +![Amrit Tracking System Architecture](src/assets/images/tracking-high-level-diagram.png) + +*High-level architecture showing the complete data flow from Angular components to external analytics platforms* + +### Architecture Overview + +The Amrit tracking system follows a layered architecture pattern: + +#### 1. Configuration Layer +- **TrackingModule**: Provides dependency injection configuration +- **Environment Configuration**: Defines platform-specific settings (Matomo/GA) +- **Injection Tokens**: Enable flexible provider configuration + +#### 2. Platform Services Layer +- **Matomo Tracking Service**: Handles Matomo-specific tracking implementation +- **Google Analytics Tracking Service**: Handles GA-specific tracking implementation +- **Provider Selection**: Dynamically selects the appropriate service based on configuration + +#### 3. Amrit Tracking Interface Layer +- **Router Integration**: Automatic page view tracking via Angular Router events +- **Session Management**: User session and ID management +- **Error Handling**: Graceful fallback mechanisms +- **Method Abstraction**: Unified tracking methods (trackEvent, trackButtonClick, etc.) + +#### 4. Application Components Layer +- **Manual/Automatic Tracking**: Components can trigger tracking events +- **User Session Layer**: Provides user ID for session tracking +- **Enabled/Disable Flag**: Conditional tracking based on environment settings + +#### 5. External Analytics Layer +- **Matomo Server**: Receives HTTP tracking data via matomo.php endpoint +- **Google Analytics**: Receives tracking data via GA tracking endpoints + +``` +┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ +│ Angular Component │───▶│ AmritTrackingService │───▶│ TrackingProvider │ +└─────────────────────┘ └──────────────────────┘ │ (Matomo/GA) │ + │ └─────────────────────┘ + ▼ │ + ┌──────────────────────┐ ▼ + │ Router Events │ ┌─────────────────────┐ + │ (Auto Page Tracking)│ │ External Analytics │ + └──────────────────────┘ │ Platform │ + └─────────────────────┘ +``` + +### Component Interaction Flow + +1. **Initialization**: `AmritTrackingService` loads the appropriate tracking provider based on environment configuration +2. **Auto-tracking**: Router events are automatically captured for page view tracking +3. **Manual tracking**: Components can call tracking methods for custom events +4. **Provider delegation**: All tracking calls are forwarded to the configured provider (Matomo/GA) + +## Setup and Configuration + +### 1. Environment Configuration + +Add tracking configuration to your environment files: + +```typescript +// src/environments/environment.ts +export const environment = { + // ... other config + tracking: { + platform: "matomo", // or 'ga' + siteId: 1, + trackerUrl: "https://matomo.piramalswasthya.org/", + trackingPlatform: "platform", + enabled: true, + }, +}; +``` + +### 2. Module Import + +Import the tracking module in your app module: + +```typescript +// app.module.ts +import { TrackingModule } from "Common-UI/src/tracking"; + +@NgModule({ + imports: [ + // ... other imports + TrackingModule.forRoot(), + ], + // ... +}) +export class AppModule {} +``` + +## Adding Tracking to Components + +### Basic Component Integration + +Here's how to add tracking to any Angular component: + +```typescript +import { Component } from "@angular/core"; +import { AmritTrackingService } from "@common-ui/tracking"; + +@Component({ + selector: "app-user-registration", + templateUrl: "./user-registration.component.html", +}) +export class UserRegistrationComponent { + constructor(private trackingService: AmritTrackingService) {} + + // Track button clicks + onSubmitRegistration() { + this.trackingService.trackButtonClick("Submit Registration"); + this.trackingService.trackFormSubmit("User Registration Form"); + } + + // Track field interactions + trackFieldInteraction(fieldName: string) { + this.trackingService.trackFieldInteraction(fieldName, "Facility Selection"); + } +} +``` + +### Template Integration + +Add tracking to template interactions: + +```html + +
+ + + + + + + +
+``` + +## Adding a New Website to Matomo + +### Step 1: Access Matomo Admin Panel + +1. Log into your Matomo instance admin panel +2. Navigate to **Administration** → **Websites** → **Manage** + +### Step 2: Add New Website + +1. Click **"Add a new website"** +2. Fill in the website details: + ``` + Website Name: Your Website Name + Website URL: https://yourwebsite.com + Time zone: Select appropriate timezone + Currency: Select currency if e-commerce tracking needed + ``` + +### Step 3: Get Tracking Information + +After creating the website, Matomo will provide: + +- **Site ID**: A unique number (e.g., 3) +- **Tracking URL**: Your Matomo instance URL + +### Step 4: Configure Your Application + +Update your environment configuration: + +```typescript +// src/environments/environment.prod.ts +export const environment = { + tracking: { + enabled: true, + platform: "matomo", + siteId: 3, // The new Site ID from Matomo + trackerUrl: "https://matomo.piramalswasthya.org/", + }, +}; +``` + +### Step 5: Verify Tracking + +1. Deploy your application with the new configuration +2. Visit your website +3. Check Matomo dashboard for incoming data +4. Navigate to **Visitors** → **Real-time** to see live visitors + + +## API Reference + +### AmritTrackingService Methods + +| Method | Parameters | Description | +| ------------------------- | ------------------------------------------------------------------ | -------------------------------- | +| `trackEvent()` | `category: string, action: string, label?: string, value?: number` | Track custom events | +| `trackButtonClick()` | `buttonName: string` | Track button interactions | +| `trackFormSubmit()` | `formName: string` | Track form submissions | +| `trackFeatureUsage()` | `featureName: string` | Track feature utilization | +| `trackFieldInteraction()` | `fieldName: string, category?: string` | Track form field interactions | +| `setUserId()` | `userId: string \| number` | Set user identifier for tracking | + +### Event Categories + +| Category | Purpose | Example Actions | +| -------------- | ---------------------------- | -------------------------------------------------- | +| `UI` | User interface interactions | `ButtonClick`, `MenuOpen`, `TabSwitch` | +| `Form` | Form-related activities | `Submit`, `Validation Error`, `Field Focus` | +| `Feature` | Feature usage tracking | `Usage`, `Enable`, `Configure` | +| `Registration` | User registration flow | `Field Interaction`, `Step Complete`, `Validation` | +| `Navigation` | Page and route changes | `Page View`, `Route Change`, `Back Button` | + +### Tips we can use while adding tracking +- Inputs: Can use (focus) to call the tracking function so that it calls only once for the input. + +- Dropdowns (mat-select): Can use (selectionChange) to call function to track events. + +- Buttons: Can use (click) to call function to track events. +Use the field label plus 'Button' (e.g., 'Advance Search Button'). + +## Troubleshooting + +### Common Issues + +1. **Events not appearing in Matomo** + + ```typescript + // Check if tracking is enabled + console.log("Tracking enabled:", environment.tracking.enabled); + + // Verify site ID and URL + console.log("Site ID:", environment.tracking.siteId); + console.log("Tracker URL:", environment.tracking.trackerUrl); + + // Check browser console for errors + ``` + +2. **Script loading failures** + + ```typescript + // Check network connectivity to Matomo instance + // Verify CORS settings on Matomo server + // Check Content Security Policy (CSP) headers + ``` + +3. **User ID not being set** + + ```typescript + // Verify session storage service is working + const userId = this.sessionStorage.getItem("userID"); + console.log("Retrieved User ID:", userId); + +### Support + +For additional support: + +1. Check Matomo documentation: https://matomo.org/docs/ +2. Review browser developer tools for errors +3. Test with Matomo's real-time visitor log +4. Verify network requests to tracking endpoint + +--- + +_Last updated: September 2025_ diff --git a/matomo-local-setup.md b/matomo-local-setup.md new file mode 100644 index 0000000..950b078 --- /dev/null +++ b/matomo-local-setup.md @@ -0,0 +1,220 @@ +# Matomo on Ubuntu: A Comprehensive Setup Guide + +This document provides a complete, step-by-step guide for installing **Matomo**, a powerful open-source web analytics platform, on an **Ubuntu** system. + +This setup utilizes a robust and modern stack: +* **nginx** and **PHP-FPM** run directly on the host for optimal performance. +* **MySQL** is containerized using **Docker** for easy management and isolation. + +--- + +## 📋 1. Prerequisites + +Before you begin, ensure your system meets the following requirements: + +* You have an **Ubuntu** system (18.04 LTS or later recommended). +* **Docker** is installed and running. +* You have a user with `sudo` privileges. +* You have a running MySQL container. For this guide, we'll assume its name is `amrit-mysql`. + +--- + +## 🐬 2. Prepare the MySQL Database + +First, we'll create a dedicated database and user for Matomo within your Dockerized MySQL container. This script is idempotent, meaning it will safely `DROP` and recreate the database and user if they already exist, ensuring a clean slate. + +1. **Verify the MySQL container is running:** + ```bash + docker ps + ``` + *You should see your MySQL container in the output list.* + +2. **Execute the database setup script:** + This command logs into your MySQL container and runs a series of SQL commands to prepare the environment. + + ```bash + # Replace 'amrit-mysql' with your container name and 'root123' with your MySQL root password. + docker exec -i amrit-mysql mysql -uroot -proot123 <<'EOF' + DROP DATABASE IF EXISTS matomo_db; + DROP USER IF EXISTS 'matomo_user'@'%'; + CREATE DATABASE matomo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + CREATE USER 'matomo_user'@'%' IDENTIFIED BY 'matomo123'; + GRANT ALL PRIVILEGES ON matomo_db.* TO 'matomo_user'@'%'; + FLUSH PRIVILEGES; + EOF + ``` + > **Note:** This creates a user named `matomo_user` with the password `matomo123`. For a production environment, always use a strong, securely generated password. + +--- + +## 📦 3. Install Host Dependencies + +Next, install nginx, PHP, and the necessary PHP extensions on your Ubuntu host. + +1. **Update package list and install packages using APT:** + ```bash + sudo apt update + sudo apt install -y nginx php-fpm php-mysql php-xml php-gd php-mbstring php-curl php-zip unzip wget + ``` + +2. **Enable and start the services:** + ```bash + sudo systemctl enable nginx php8.1-fpm + sudo systemctl start nginx php8.1-fpm + ``` + > **Note:** Replace `php8.1-fpm` with your PHP version if different. You can check with `php --version`. + +--- + +## 📂 4. Download Matomo + +Now, we will download the latest version of Matomo and place it in the web server's root directory, setting the correct permissions. + +1. **Create the web directory and set initial ownership:** + ```bash + sudo mkdir -p /var/www/matomo + sudo chown $USER:$USER /var/www/matomo + ``` + +2. **Download and extract the Matomo files:** + This sequence downloads the latest release, extracts it to a temporary folder, moves the contents to the final destination, and cleans up the temporary files. + ```bash + cd ~ + wget https://builds.matomo.org/matomo-latest.zip + unzip matomo-latest.zip -d matomo-temp + mv matomo-temp/matomo/* /var/www/matomo/ + rm -rf matomo-temp matomo-latest.zip + ``` + +3. **Set final file permissions:** + It's crucial to set the correct ownership and permissions so that the web server (`www-data` user) can read, write, and execute files as needed. + ```bash + # Set ownership to the web server user + sudo chown -R www-data:www-data /var/www/matomo + + # Set standard permissions: 755 for directories, 644 for files + sudo find /var/www/matomo -type d -exec chmod 755 {} \; + sudo find /var/www/matomo -type f -exec chmod 644 {} \; + + # Set write permissions for specific directories that Matomo needs to write to + sudo chmod -R 755 /var/www/matomo/tmp + sudo chmod -R 755 /var/www/matomo/config + ``` + +--- + +## ⚙️ 5. Configure Nginx + +We need to tell nginx how to serve the Matomo application. We'll do this by creating a dedicated virtual host configuration file. + +1. **Create the nginx virtual host file:** + The following command creates the configuration file at `/etc/nginx/sites-available/matomo`. + ```bash + sudo tee /etc/nginx/sites-available/matomo > /dev/null << 'EOF' + server { + listen 80 default_server; + server_name localhost; # Replace with your domain in production + + root /var/www/matomo; + index index.php; + + access_log /var/log/nginx/matomo.access.log; + error_log /var/log/nginx/matomo.error.log; + + # Standard file serving + location / { + try_files $uri $uri/ =404; + } + + # Pass PHP scripts to PHP-FPM + location ~ \.php$ { + try_files $uri =404; + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; + } + + # Block access to sensitive directories for security + location ~ ^/(config|tmp|core|lang) { + deny all; + return 403; + } + + # Block access to .htaccess files + location ~ /\.ht { + deny all; + } + + # Block access to sensitive files + location ~ \.(ini|log|conf)$ { + deny all; + } + } + EOF + ``` + > **Note:** Replace `php8.1-fpm.sock` with your PHP version if different. + +2. **Disable the default nginx site and enable Matomo:** + ```bash + sudo unlink /etc/nginx/sites-enabled/default + sudo ln -s /etc/nginx/sites-available/matomo /etc/nginx/sites-enabled/ + ``` + +3. **Ensure the main `nginx.conf` includes virtual hosts:** + Open `/etc/nginx/nginx.conf` and verify that the `http` block contains the following line. It's usually there by default on Ubuntu. + ```nginx + http { + # ... other directives + include /etc/nginx/sites-enabled/*; + # ... other directives + } + ``` + > **Note:** Ubuntu uses `/etc/nginx/sites-enabled/*` (without `.conf` extension) unlike some other distributions. + +4. **Test and reload nginx:** + Always test the configuration before applying it. + ```bash + sudo nginx -t && sudo systemctl reload nginx + ``` + +--- + +## 6. Run the Matomo Web Installer + +With the backend configured, you can now complete the installation through the web interface. + +1. Navigate to **`http://localhost` or `http://127.0.0.1/`** (or your server's IP/domain) in your browser. +2. Follow the on-screen instructions: + * **System Check**: Matomo will check if all dependencies are met. Click **Next**. + * **Database Setup**: Enter the credentials we created in Step 2. + * Database Server: `127.0.0.1` + * Login: `matomo_user` + * Password: `matomo123` + * Database Name: `matomo_db` + * Table Prefix: `matomo_` + * Adapter: `PDO\MYSQL` + * **Super User**: Create your primary administrator account. + * **Set up a Website**: Enter the details of the first website you want to track. + * **JavaScript Tracking Code**: Copy the provided snippet. You will add this to the pages of the website you want to track. + * **Done**: The installation is complete! Proceed to your Matomo dashboard. + +--- + +## 🔧 7. Additional Ubuntu-Specific Notes + +* **Firewall Configuration**: If you have UFW enabled, allow HTTP traffic: + ```bash + sudo ufw allow 'Nginx HTTP' + ``` + +* **PHP Version Management**: Ubuntu often has multiple PHP versions. Check your active version: + ```bash + php --version + sudo systemctl status php*-fpm + ``` + +* **Troubleshooting**: If you encounter permission issues, ensure the web server user has proper access: + ```bash + sudo usermod -a -G www-data $USER + ``` + + diff --git a/src/assets/images/tracking-high-level-diagram.png b/src/assets/images/tracking-high-level-diagram.png new file mode 100644 index 0000000..c61bf2b Binary files /dev/null and b/src/assets/images/tracking-high-level-diagram.png differ diff --git a/src/feedback/feedback-routing.module.ts b/src/feedback/feedback-routing.module.ts new file mode 100644 index 0000000..df73186 --- /dev/null +++ b/src/feedback/feedback-routing.module.ts @@ -0,0 +1,37 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { FeedbackPublicPageComponent } from "./pages/feedback-public-page/feedback-public-page-component"; + +const routes: Routes = [ + { + path: "", + component: FeedbackPublicPageComponent, // public post-logout page + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class FeedbackRoutingModule {} diff --git a/src/feedback/feedback.module.ts b/src/feedback/feedback.module.ts new file mode 100644 index 0000000..a421b53 --- /dev/null +++ b/src/feedback/feedback.module.ts @@ -0,0 +1,48 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { HttpClientModule } from "@angular/common/http"; + +import { FeedbackRoutingModule } from "./feedback-routing.module"; +import { MaterialModule } from "src/app/app-modules/core/material.module"; // your shared material bundle + +import { FeedbackPublicPageComponent } from "./pages/feedback-public-page/feedback-public-page-component"; +import { FeedbackDialogComponent } from "./shared/feedback-dialog/feedback-dialog.component"; + +import { FeedbackService } from "./services/feedback.service"; + +@NgModule({ + declarations: [FeedbackPublicPageComponent, FeedbackDialogComponent], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + HttpClientModule, + MaterialModule, + FeedbackRoutingModule, + ], + exports: [FeedbackDialogComponent], + providers: [FeedbackService], +}) +export class FeedbackModule {} diff --git a/src/feedback/pages/feedback-public-page/feedback-public-page-component.ts b/src/feedback/pages/feedback-public-page/feedback-public-page-component.ts new file mode 100644 index 0000000..1927be0 --- /dev/null +++ b/src/feedback/pages/feedback-public-page/feedback-public-page-component.ts @@ -0,0 +1,72 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { Component } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { map } from "rxjs/operators"; + +type SL = "1097" | "104" | "AAM" | "MMU" | "TM" | "ECD"; + +@Component({ + selector: "app-feedback-public-page", + template: ` +
+ + +
+ `, + styles: [ + ` + .page-wrap { + min-height: 100vh; + background: #f5f7fb; + } + `, + ], +}) +export class FeedbackPublicPageComponent { + serviceLine: SL = "AAM"; // default fallback + + constructor(private route: ActivatedRoute) { + // Check query param ?sl= + this.route.queryParamMap + .pipe(map((q) => (q.get("sl") as SL) || this.detectFromLocation())) + .subscribe((sl) => (this.serviceLine = sl)); + } + + private detectFromLocation(): SL { + const path = window.location.pathname.toLowerCase(); + + // path-based service lines + if (path.includes("/1097")) return "1097"; + if (path.includes("/104")) return "104"; + if (path.includes("/aam")) return "AAM"; + if (path.includes("/mmu")) return "MMU"; + if (path.includes("/tm")) return "TM"; + if (path.includes("/ecd")) return "ECD"; + + // fallback + return "AAM"; + } +} diff --git a/src/feedback/services/feedback.service.ts b/src/feedback/services/feedback.service.ts new file mode 100644 index 0000000..b168aec --- /dev/null +++ b/src/feedback/services/feedback.service.ts @@ -0,0 +1,63 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { Injectable } from "@angular/core"; +import { HttpClient, HttpParams } from "@angular/common/http"; +import { Observable } from "rxjs"; + +export type ServiceLine = "1097" | "104" | "AAM" | "MMU" | "TM" | "ECD"; + +export interface CategoryDto { + categoryID: string; + slug: string; + label: string; + scope: "GLOBAL" | ServiceLine; + active: boolean; +} + +export interface SubmitFeedbackRequest { + rating: number; + categorySlug: string; // FE sends slug; BE resolves to CategoryID + comment?: string; + isAnonymous: boolean; // true for logout flow + serviceLine: ServiceLine; +} + +@Injectable() +export class FeedbackService { + private readonly apiBase = `${window.location.origin}/common-api`; + + constructor(private http: HttpClient) {} + + listCategories(serviceLine: ServiceLine): Observable { + const params = new HttpParams().set("serviceLine", serviceLine); + return this.http.get( + `${this.apiBase}/platform-feedback/categories`, + ); + } + + submitFeedback(payload: SubmitFeedbackRequest) { + return this.http.post<{ id: string; createdAt?: string }>( + `${this.apiBase}/platform-feedback`, + payload, + ); + } +} diff --git a/src/feedback/shared/feedback-dialog/feedback-dialog.component.html b/src/feedback/shared/feedback-dialog/feedback-dialog.component.html new file mode 100644 index 0000000..781c6b0 --- /dev/null +++ b/src/feedback/shared/feedback-dialog/feedback-dialog.component.html @@ -0,0 +1,135 @@ +
+ +
diff --git a/src/feedback/shared/feedback-dialog/feedback-dialog.component.scss b/src/feedback/shared/feedback-dialog/feedback-dialog.component.scss new file mode 100644 index 0000000..4fe807b --- /dev/null +++ b/src/feedback/shared/feedback-dialog/feedback-dialog.component.scss @@ -0,0 +1,72 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +.fb-wrap { + min-height: 100vh; + background: #f5f7fb; + display: flex; + align-items: center; + justify-content: center; + padding: 32px; + } + .fb-card { + width: min(720px, 92vw); + background: #fff; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(16,24,40,.08); + padding: 28px 28px 32px; + } + + .fb-title { margin: 0 0 6px; font-size: 24px; line-height: 1.2; font-weight: 600; color: #111827; text-align: center; } + .fb-subtitle { margin: 0 0 18px; color: #6b7280; text-align: center; font-size: 14px; } + + .fb-stars { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; justify-items: center; margin: 8px 0 10px; } + .fb-star { display: grid; grid-template-rows: auto auto; gap: 6px; align-items: center; justify-items: center; border: none; background: transparent; cursor: pointer; } + .fb-star-icon { font-size: 40px; color: #dbe4ff; line-height: 1; } + .fb-star--active .fb-star-icon { color: #2563eb; } + .fb-star-text { font-size: 12px; color: #2563eb; } + + .fb-sep { border: 0; border-top: 1px solid #e5e7eb; margin: 16px 0 12px; } + + .fb-row { margin: 14px 0; } + .fb-label { display: block; margin-bottom: 8px; color: #374151; font-weight: 500; } + .fb-select { + width: 100%; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff; font-size: 14px; + } + .fb-textarea { + width: 100%; min-height: 110px; border: 1px solid #e5e7eb; border-radius: 8px; + padding: 12px; resize: vertical; font-size: 14px; line-height: 1.4; + overflow-wrap: anywhere; word-break: break-word; /* long text fix */ + } + .fb-textarea:focus, .fb-select:focus { outline: 2px solid #93c5fd; outline-offset: 2px; } + + .fb-hint { margin-top: 6px; font-size: 12px; color: #9ca3af; text-align: right; } + + .fb-actions { display: flex; justify-content: space-evenly; margin-top: 18px; } + .fb-btn { min-width: 120px; padding: 10px 16px; border: 0; border-radius: 8px; font-weight: 600; cursor: pointer; } + .fb-btn-ok {background: #2563eb; color: #fff;} + .fb-btn-cls { background: #fff; color: #2563eb; border-color: #2563eb ; border: solid 1px;} + .fb-btn[disabled] { opacity: .6; cursor: not-allowed; } + + .fb-foot { margin-top: 12px; text-align: center; } + .fb-error { color: #b91c1c; } + .fb-success { color: #15803d; } + \ No newline at end of file diff --git a/src/feedback/shared/feedback-dialog/feedback-dialog.component.ts b/src/feedback/shared/feedback-dialog/feedback-dialog.component.ts new file mode 100644 index 0000000..e193edb --- /dev/null +++ b/src/feedback/shared/feedback-dialog/feedback-dialog.component.ts @@ -0,0 +1,185 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { Component, Input, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { + FeedbackService, + ServiceLine, + CategoryDto, +} from "../../services/feedback.service"; +import { finalize } from "rxjs/operators"; +import { SessionStorageService } from "Common-UI/src/registrar/services/session-storage.service"; +import { HttpServiceService } from "src/app/app-modules/core/services/http-service.service"; +import { SetLanguageComponent } from "src/app/app-modules/core/components/set-language.component"; + +@Component({ + selector: "app-feedback-dialog", + templateUrl: "./feedback-dialog.component.html", + styleUrls: ["./feedback-dialog.component.scss"], +}) +export class FeedbackDialogComponent implements OnInit { + @Input() serviceLine: ServiceLine = "TM"; + @Input() defaultCategorySlug?: string; + + stars = [1, 2, 3, 4, 5]; + starLabels = ["Terrible", "Bad", "Okay", "Good", "Great"]; + categories: CategoryDto[] = []; + submitting = false; + error?: string; + successId?: string; + + isLoggedIn = false; + storedUserId?: string; + + // showCategory controls whether dropdown is shown (true if categories loaded) + showCategory = true; + + form = this.fb.nonNullable.group({ + rating: [0, [Validators.min(1), Validators.max(5)]], + categorySlug: ["", Validators.required], + comment: ["", Validators.maxLength(2000)], + // default to true for logged-out; we'll set actual default in ngOnInit + isAnonymous: [true], + }); + current_language_set: any; + + constructor( + private fb: FormBuilder, + private api: FeedbackService, + private sessionStorage: SessionStorageService, + public httpService: HttpServiceService, + public router: Router, + ) {} + + ngOnInit() { + this.assignSelectedLanguage(); + // sessionStorage check + try { + this.storedUserId = this.sessionStorage.getItem("userID") || undefined; + this.isLoggedIn = !!this.storedUserId; + } catch (e) { + // sessionStorage may be unavailable in some runners; fail safe to anonymous + this.isLoggedIn = false; + this.storedUserId = undefined; + } + + // If user is logged in, default to NOT anonymous so they explicitly can opt-in; if logged out, force anonymous + if (this.isLoggedIn) { + // default to anonymous=true to respect privacy; but you asked to ask consent — show unchecked by default + // we'll set it to true so users must actively uncheck to identify themselves OR you can flip to false to encourage identified + // choose default = true to be conservative: + this.form.controls.isAnonymous.setValue(true); + } else { + this.form.controls.isAnonymous.setValue(true); + } + + // load categories + this.api.listCategories(this.serviceLine).subscribe({ + next: (list) => { + this.categories = (list || []).filter( + (c: any) => (c as any).active ?? true + ); + this.showCategory = this.categories.length > 0; + const def = this.categories[0]?.slug || this.defaultCategorySlug || ""; + if (def) this.form.controls.categorySlug.setValue(def); + }, + error: () => (this.error = "Could not load categories."), + }); + } + + assignSelectedLanguage() { + const getLanguageJson = new SetLanguageComponent(this.httpService); + getLanguageJson.setLanguage(); + this.current_language_set = getLanguageJson.currentLanguageObject; + } + + setRating(n: number) { + this.form.controls.rating.setValue(n); + } + + toggleAnonymous(event: Event) { + const input = event.target as HTMLInputElement; + this.form.controls.isAnonymous.setValue(input.checked); + } + + formInvalidForNow(): boolean { + // require rating >=1 and category selected + return this.form.invalid; + } + + login() { + this.router.navigate(["/login"]); + } + + submit() { + this.error = undefined; + this.successId = undefined; + + if (this.formInvalidForNow()) { + this.error = "Pick a rating and a category."; + return; + } + + // build payload + const payload: any = { + rating: this.form.value.rating!, + categorySlug: this.form.value.categorySlug!, + comment: this.form.value.comment || undefined, + isAnonymous: this.form.value.isAnonymous!, + serviceLine: this.serviceLine, + }; + + if (!payload.isAnonymous && this.isLoggedIn && this.storedUserId) { + // include userId for identified submissions + // session storage stores as string, convert to integer if needed by backend + const parsed = parseInt(this.storedUserId as string, 10); + payload.userId = Number.isNaN(parsed) ? this.storedUserId : parsed; + } + + this.submitting = true; + this.api + .submitFeedback(payload) + .pipe(finalize(() => (this.submitting = false))) + .subscribe({ + next: (res) => { + this.successId = res?.id || "submitted"; + // reset form but keep identity default + this.form.reset({ + rating: 0, + categorySlug: this.categories[0]?.slug ?? "", + comment: "", + isAnonymous: this.isLoggedIn ? true : true, + }); + }, + error: (e) => { + if (e?.status === 429) { + this.error = "Too many attempts. Try later."; + } else if (e?.error?.error) { + this.error = e.error.error; + } else { + this.error = "Submission failed."; + } + }, + }); + } +} diff --git a/src/public-api.ts b/src/public-api.ts new file mode 100644 index 0000000..87c7030 --- /dev/null +++ b/src/public-api.ts @@ -0,0 +1 @@ +export * from './tracking'; \ No newline at end of file diff --git a/src/registrar/abha-components/abha-generation-success-component/abha-generation-success-component.component.html b/src/registrar/abha-components/abha-generation-success-component/abha-generation-success-component.component.html index 461a730..dbf5dbc 100644 --- a/src/registrar/abha-components/abha-generation-success-component/abha-generation-success-component.component.html +++ b/src/registrar/abha-components/abha-generation-success-component/abha-generation-success-component.component.html @@ -8,7 +8,7 @@

{{ currentLanguageSet?.common?.info }}

{{ currentLanguageSet?.abhaAddress }}: {{ abhaProfileData.ABHAProfile.healthId }}

- {{ currentLanguageSet?.aBHANumber }}: + {{ currentLanguageSet?.abhaNumber }}: {{ abhaProfileData.ABHAProfile.healthIdNumber }}


diff --git a/src/registrar/abha-components/abha-verify-success-component/abha-verify-success-component.component.html b/src/registrar/abha-components/abha-verify-success-component/abha-verify-success-component.component.html index 2f08ef5..e59f951 100644 --- a/src/registrar/abha-components/abha-verify-success-component/abha-verify-success-component.component.html +++ b/src/registrar/abha-components/abha-verify-success-component/abha-verify-success-component.component.html @@ -12,7 +12,7 @@

{{ currentLanguageSet?.common?.info }}

{{ (abhaDetails.preferredAbhaAddress !== undefined && abhaDetails.preferredAbhaAddress !== null) ? abhaDetails.preferredAbhaAddress : abhaDetails?.abhaAddress }}

- {{ currentLanguageSet?.aBHANumber }}: + {{ currentLanguageSet?.abhaNumber }}: {{ (abhaDetails.ABHANumber !== undefined && abhaDetails.ABHANumber !== null) ? abhaDetails.ABHANumber : abhaDetails?.abhaNumber}}

diff --git a/src/registrar/abha-components/download-search-abha/download-search-abha.component.html b/src/registrar/abha-components/download-search-abha/download-search-abha.component.html index 8b9d021..1a4f25b 100644 --- a/src/registrar/abha-components/download-search-abha/download-search-abha.component.html +++ b/src/registrar/abha-components/download-search-abha/download-search-abha.component.html @@ -90,7 +90,7 @@

{{ currentLanguageSet.searchAndDownloadAbha }}

Note*: {{ currentLanguageSet?.aBHA }}- xxx{{ abhaSuffix }}, xxx.xx{{ abhaSuffix }}
- {{ currentLanguageSet?.aBHANumber }} - xx-xxxx-xxxx-xxxx + {{ currentLanguageSet?.abhaNumber }} - xx-xxxx-xxxx-xxxx

diff --git a/src/registrar/abha-components/download-search-abha/download-search-abha.component.ts b/src/registrar/abha-components/download-search-abha/download-search-abha.component.ts index 0c8dab4..662e042 100644 --- a/src/registrar/abha-components/download-search-abha/download-search-abha.component.ts +++ b/src/registrar/abha-components/download-search-abha/download-search-abha.component.ts @@ -194,7 +194,7 @@ export class DownloadSearchAbhaComponent { let message = res.data.message; this.dialogRef.close(); this.confirmationValService.alert(message, 'success').afterClosed().subscribe(result => { - if(result){ + if(result !== false){ this.routeToEnterOtpPage(txnId, loginMethod, loginHint); } }) diff --git a/src/registrar/abha-components/generate-abha-component/generate-abha-component.component.ts b/src/registrar/abha-components/generate-abha-component/generate-abha-component.component.ts index 6a5e8f8..607ffe3 100644 --- a/src/registrar/abha-components/generate-abha-component/generate-abha-component.component.ts +++ b/src/registrar/abha-components/generate-abha-component/generate-abha-component.component.ts @@ -77,7 +77,7 @@ export class GenerateAbhaComponentComponent { this.dialogRef.close(); this.confirmationService.alert(res.data.message, "success").afterClosed().subscribe(result => { console.log("dialog ref after closed response returning", result) - if (result) { + if (result !== false) { this.routeToOtpPage(txnId); } }) diff --git a/src/registrar/abha-components/health-id-display-modal/health-id-display-modal.component.html b/src/registrar/abha-components/health-id-display-modal/health-id-display-modal.component.html index e5d118d..926d2f3 100644 --- a/src/registrar/abha-components/health-id-display-modal/health-id-display-modal.component.html +++ b/src/registrar/abha-components/health-id-display-modal/health-id-display-modal.component.html @@ -123,7 +123,7 @@

- {{ currentLanguageSet?.aBHANumber }} + {{ currentLanguageSet?.abhaNumber }} {{ element?.healthIdNumber ? element?.healthIdNumber : "" }} @@ -216,7 +216,7 @@

*matHeaderCellDef mat-sort-header > - {{ currentLanguageSet?.aBHANumber }} + {{ currentLanguageSet?.abhaNumber }} {{ element?.healthIdNumber ? element?.healthIdNumber : null }} diff --git a/src/registrar/registration/abha-information/abha-information.component.html b/src/registrar/registration/abha-information/abha-information.component.html index be5bd5c..e26c2ab 100644 --- a/src/registrar/registration/abha-information/abha-information.component.html +++ b/src/registrar/registration/abha-information/abha-information.component.html @@ -5,7 +5,7 @@ class="full-width-login pull-right m-r-5 mat_blue" id="viewHealthID" type="accent" - (click)="viewHealthIdData()" + (click)="viewHealthIdData(); trackFieldInteraction('ABHA Details')" > ABHA Details @@ -16,7 +16,7 @@ mat-raised-button color="primary" class="full-width-login pull-right m-r-5 mat_blue" - (click)="printHealthIDCard()" + (click)="printHealthIDCard(); trackFieldInteraction('Search ABHA')" > Search ABHA @@ -44,6 +44,7 @@ placeholder="{{ item.placeholder }}" [required]="item.isRequired || false" formControlName="{{ item.fieldName }}" + (focus)="trackFieldInteraction('ABHA Number')" />

diff --git a/src/registrar/registration/abha-information/abha-information.component.ts b/src/registrar/registration/abha-information/abha-information.component.ts index 53af336..ffdb365 100644 --- a/src/registrar/registration/abha-information/abha-information.component.ts +++ b/src/registrar/registration/abha-information/abha-information.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Injector, Input } from '@angular/core'; import { Router } from '@angular/router'; import { RegistrarService } from '../../services/registrar.service'; import { MatDialog } from '@angular/material/dialog'; @@ -11,6 +11,7 @@ import { Subscription } from 'rxjs'; import { GenerateAbhaComponentComponent } from '../../abha-components/generate-abha-component/generate-abha-component.component'; import { DownloadSearchAbhaComponent } from '../../abha-components/download-search-abha/download-search-abha.component'; import { AbhaConsentFormComponent } from '../../abha-components/abha-consent-form/abha-consent-form.component'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; @Component({ selector: 'app-abha-information', @@ -42,7 +43,9 @@ export class AbhaInformationComponent { private dialog: MatDialog, private confirmationService: ConfirmationService, private httpServiceService: HttpServiceService, - private languageComponent: SetLanguageComponent + private languageComponent: SetLanguageComponent, + private injector: Injector + ) { this.abhaInfoSubscription = this.registrarService.registrationABHADetails$.subscribe( @@ -227,4 +230,9 @@ export class AbhaInformationComponent { }); } + trackFieldInteraction(fieldName: string) { + const trackingService = this.injector.get(AmritTrackingService); + trackingService.trackFieldInteraction(fieldName, 'Abha Information'); + } + } diff --git a/src/registrar/registration/location-information/location-information.component.html b/src/registrar/registration/location-information/location-information.component.html index cea8abb..726cfc5 100644 --- a/src/registrar/registration/location-information/location-information.component.html +++ b/src/registrar/registration/location-information/location-information.component.html @@ -12,6 +12,7 @@ matInput type="input" (input)="onInputChanged($event, item.allowMax, item.fieldName)" + (focus)="trackFieldInteraction(item.fieldTitle)" placeholder="{{ item.placeholder }}" [required]="item.isRequired || false" formControlName="{{ item.fieldName }}" /> @@ -114,29 +115,31 @@ -
- - {{ item.fieldTitle }} - + + + {{ item.fieldTitle }} + + - + [formControlName]="item.fieldName" + [matAutocomplete]="auto" + (input)="filterOptions(item)" + (focus)="showAllOptions(item)" + /> + + + + {{ option }} - +
- + diff --git a/src/registrar/registration/location-information/location-information.component.ts b/src/registrar/registration/location-information/location-information.component.ts index badd2ad..d04aa5b 100644 --- a/src/registrar/registration/location-information/location-information.component.ts +++ b/src/registrar/registration/location-information/location-information.component.ts @@ -10,6 +10,8 @@ import { import { RegistrarService } from '../../services/registrar.service'; import { Subscription } from 'rxjs'; import { SessionStorageService } from '../../services/session-storage.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking' +import { Injector } from '@angular/core'; @Component({ selector: 'app-location-information', @@ -44,19 +46,22 @@ export class LocationInformationComponent { patchAbhaLocationDetails = false; patchAbhaBenLocationDetails: any; registrationSubscription!: Subscription; + filteredOptions: { [key: string]: string[] } = {}; constructor( private fb: FormBuilder, private registrarService: RegistrarService, private sessionstorage:SessionStorageService, + private injector: Injector + ) { - this.registrationSubscription = this.registrarService.abhaLocationDetails$.subscribe((result: any) => { - if(result){ - this.patchAbhaLocationDetails = true; - this.patchAbhaBenLocationDetails = result; - this.loadLocalMasterForDemographic(); - } - }); + this.registrationSubscription = this.registrarService.abhaLocationDetails$.subscribe((result: any) => { + if (result) { + this.patchAbhaLocationDetails = true; + this.patchAbhaBenLocationDetails = result; + this.loadLocalMasterForDemographic(); + } + }); } ngOnInit() { @@ -75,7 +80,12 @@ export class LocationInformationComponent { item.fieldName, new FormControl(null), ); + // Initialize filtered list with all options + if (item.options) { + this.filteredOptions[item.fieldName] = [...item.options]; + } } + }); this.locationInfoFormGroup.addControl('stateID', new FormControl()); this.locationInfoFormGroup.addControl('districtID', new FormControl()); @@ -100,6 +110,16 @@ export class LocationInformationComponent { console.log('location Form Data', this.formData); } + showAllOptions(item: any) { + item.filteredOptions = [...item.options]; + } + + filterOptions(item: any) { + const inputValue = this.locationInfoFormGroup.get(item.fieldName)?.value?.toLowerCase() || ''; + item.filteredOptions = item.options.filter((opt: string) => + opt.toLowerCase().includes(inputValue) + ); + } loadLocationFromStorage() { const locationData: any = this.sessionstorage.getItem('location'); const location = JSON.parse(locationData); @@ -173,6 +193,7 @@ export class LocationInformationComponent { } onChangeLocation(fieldNamevalue: any, selectedValue: any) { + if (fieldNamevalue === 'stateName') { const stateDetails = this.statesList.find((value: any) => { return value.stateName === selectedValue; @@ -210,6 +231,7 @@ export class LocationInformationComponent { const villageDetails = this.villageList.find((value: any) => { return value.villageName === selectedValue; }); + this.locationInfoFormGroup.patchValue({ districtBranchID: villageDetails?.districtBranchID, districtBranchName: villageDetails?.villageName, @@ -265,15 +287,15 @@ export class LocationInformationComponent { stateID: this.locationPatchDetails.stateID, stateName: this.locationPatchDetails.stateName, }); - } else if(this.patchAbhaLocationDetails){ + } else if (this.patchAbhaLocationDetails) { let localStateId; let localStateName; this.statesList.find((item: any) => { - if(item.govtLGDStateID === parseInt(this.patchAbhaBenLocationDetails.stateID)){ + if (item.govtLGDStateID === parseInt(this.patchAbhaBenLocationDetails.stateID)) { localStateId = item.stateID; localStateName = item.stateName; - } - }); + } + }); this.locationInfoFormGroup.patchValue({ stateID: localStateId, stateName: localStateName, @@ -309,15 +331,15 @@ export class LocationInformationComponent { districtID: this.locationPatchDetails.districtID, districtName: this.locationPatchDetails.districtName, }); - } else if(this.patchAbhaLocationDetails){ + } else if (this.patchAbhaLocationDetails) { let localDistrictId; let localDistrictName; this.districtList.find((item: any) => { - if(item.govtLGDDistrictID === parseInt(this.patchAbhaBenLocationDetails.districtID)){ + if (item.govtLGDDistrictID === parseInt(this.patchAbhaBenLocationDetails.districtID)) { localDistrictId = item.districtID; localDistrictName = item.districtName; - } - }); + } + }); this.locationInfoFormGroup.patchValue({ districtID: localDistrictId, districtName: localDistrictName, @@ -493,9 +515,14 @@ export class LocationInformationComponent { } } - ngOnDestroy(){ - if(this.registrationSubscription){ + ngOnDestroy() { + if (this.registrationSubscription) { this.registrationSubscription.unsubscribe(); } } + + trackFieldInteraction(fieldName: string) { + const trackingService = this.injector.get(AmritTrackingService); + trackingService.trackFieldInteraction(fieldName, 'Location'); + } } diff --git a/src/registrar/registration/personal-information/personal-information.component.html b/src/registrar/registration/personal-information/personal-information.component.html index 262e3b5..69ef10e 100644 --- a/src/registrar/registration/personal-information/personal-information.component.html +++ b/src/registrar/registration/personal-information/personal-information.component.html @@ -24,7 +24,7 @@ mat-raised-button id="captureButton" class="m-t-10 mat_captureButton" - (click)="captureImage()" + (click)="captureImage(); trackFieldInteraction('Upload Beneficiary Image')" type="button"> Capture Photo @@ -45,6 +45,7 @@ matInput type="input" (input)="onInputChanged($event, item.allowMax, item.fieldName)" + (focus)="trackFieldInteraction(item.fieldTitle || item.fieldName)" placeholder="{{ item.placeholder }}" [required]="item.isRequired || false" (change)="checkFieldValidations(item)" @@ -76,6 +77,7 @@ @@ -124,6 +127,7 @@ }} diff --git a/src/registrar/registration/personal-information/personal-information.component.ts b/src/registrar/registration/personal-information/personal-information.component.ts index b5c88fa..f90d181 100644 --- a/src/registrar/registration/personal-information/personal-information.component.ts +++ b/src/registrar/registration/personal-information/personal-information.component.ts @@ -26,6 +26,9 @@ import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, } from '@angular/material-moment-adapter'; import { HttpServiceService } from 'src/app/app-modules/core/services/http-service.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { Injector } from '@angular/core'; + @Component({ selector: 'app-personal-information', @@ -88,7 +91,8 @@ export class PersonalInformationComponent { private beneficiaryDetailsService: BeneficiaryDetailsService, private confirmationService: ConfirmationService, private languageComponent: SetLanguageComponent, - private httpServiceService: HttpServiceService + private httpServiceService: HttpServiceService, + private injector: Injector ) { this.personalInfoSubscription = this.registrarService.registrationABHADetails$.subscribe( @@ -110,7 +114,7 @@ export class PersonalInformationComponent { } ); } - + ngOnInit() { this.fetchLanguageResponse(); this.formData.forEach((item: any) => { @@ -326,22 +330,7 @@ export class PersonalInformationComponent { } } - changeLiteracyStatus() { - const literacyStatus = this.personalInfoFormGroup.value.literacyStatus; - - if (literacyStatus !== 'Literate') { - console.log(this.personalInfoFormGroup.controls, 'controls'); - // this.personalInfoFormGroup.controls['educationQualification'].clearValidators(); - console.log( - this.personalInfoFormGroup.controls['educationQualification'], - 'controls' - ); - } else { - this.personalInfoFormGroup.controls['educationQualification'].reset(); - } - } - - /** + /** * Phone Number Parent Relations */ getParentDetails() { @@ -857,6 +846,11 @@ export class PersonalInformationComponent { this.languageComponent.setLanguage(); this.currentLanguageSet = this.languageComponent.currentLanguageObject; } + + trackFieldInteraction(fieldName: string) { + const trackingService = this.injector.get(AmritTrackingService); + trackingService.trackFieldInteraction(fieldName, 'Personal Information'); + } } export function maxLengthValidator(maxLength: number): ValidatorFn { @@ -864,8 +858,6 @@ export function maxLengthValidator(maxLength: number): ValidatorFn { const value = control.value; if (value && value.length > maxLength) { - console.log('maxLnegthvalidator', value); - return { maxLengthExceeded: true }; } diff --git a/src/registrar/registration/registration.component.ts b/src/registrar/registration/registration.component.ts index 9f0e845..faf10d9 100644 --- a/src/registrar/registration/registration.component.ts +++ b/src/registrar/registration/registration.component.ts @@ -284,10 +284,11 @@ export class RegistrationComponent { this.otherInfoData = item.fields; } if(item.sectionName.toLowerCase() === "abha information"){ - if(this.serviceLine === 'HWC' || this.serviceLine === 'TM'){ + // commented for current purpose will uncomment or remove later + // if(this.serviceLine === 'HWC' || this.serviceLine === 'TM'){ this.abhaInfoData = item.fields; this.enableAbhaInfo = true; - } + // } } }); } diff --git a/src/registrar/search-dialog/search-dialog.component.html b/src/registrar/search-dialog/search-dialog.component.html index c6a865b..d870741 100644 --- a/src/registrar/search-dialog/search-dialog.component.html +++ b/src/registrar/search-dialog/search-dialog.component.html @@ -1,11 +1,8 @@

{{ currentLanguageSet?.common?.advanceBeneficiarySearch }}

-
@@ -17,64 +14,38 @@

{{ currentLanguageSet?.common?.advanceBeneficiarySearch }}

{{ currentLanguageSet?.ro?.personalInfo?.firstName - }} - - + }} + + {{ - currentLanguageSet?.benDetailsAlert?.firstNameMandatory - }} - + currentLanguageSet?.benDetailsAlert?.firstNameMandatory + }} + {{ - currentLanguageSet?.benDetailsAlert?.minCharRequired - }} + currentLanguageSet?.benDetailsAlert?.minCharRequired + }}
{{ currentLanguageSet?.ro?.personalInfo?.lastName - }} - + }} +
{{ currentLanguageSet?.ro?.otherInfo?.fatherName - }} - - {{ - currentLanguageSet?.common?.pleaseprovideatleast2character - }} + }} + + {{ + currentLanguageSet?.common?.pleaseprovideatleast2character + }}
@@ -82,18 +53,11 @@

{{ currentLanguageSet?.common?.advanceBeneficiarySearch }}

{{ currentLanguageSet?.ro?.personalInfo?.gender - }} - + - {{ gender.genderName }} + {{ + gender.genderName }} @@ -103,15 +67,9 @@

{{ currentLanguageSet?.common?.advanceBeneficiarySearch }}

{{ currentLanguageSet?.ro?.personalInfo?.dateOfBirth - }} - - + }} + +
@@ -119,78 +77,94 @@

{{ currentLanguageSet?.common?.advanceBeneficiarySearch }}

- - - {{ currentLanguageSet?.travel?.state }} - - - {{ state.stateName | titlecase }} + + + + {{ currentLanguageSet?.travel?.state }} + + + + + {{ state.stateName | titlecase }} - +
- - {{ - currentLanguageSet?.travel?.district - }} - - {{ district.districtName | titlecase }} + + + {{ currentLanguageSet?.travel?.district }} + + + + {{ district.districtName | titlecase }} - + + +
+
+
+
+ +
+
+ + + + {{ block.blockName | titlecase }} + + + {{ currentLanguageSet?.block }} + + + + {{ block.blockName | titlecase }} + + + + +
+
+
+
+ + + {{ currentLanguageSet?.village }} + + + + {{ village.villageName | titlecase }} + + + +
+ - - - -
-
+ \ No newline at end of file diff --git a/src/registrar/search-dialog/search-dialog.component.ts b/src/registrar/search-dialog/search-dialog.component.ts index 106fcf1..b503ded 100644 --- a/src/registrar/search-dialog/search-dialog.component.ts +++ b/src/registrar/search-dialog/search-dialog.component.ts @@ -28,7 +28,7 @@ import { AfterViewChecked, DoCheck, } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; import { MatDialogRef } from '@angular/material/dialog'; import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; import { ConfirmationService } from 'src/app/app-modules/core/services'; @@ -46,6 +46,7 @@ import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, } from '@angular/material-moment-adapter'; import { SessionStorageService } from '../services/session-storage.service'; +import { map, Observable, startWith } from 'rxjs'; interface Beneficary { firstName: string; @@ -111,6 +112,21 @@ export class SearchDialogComponent implements OnInit, DoCheck { newSearchForm!: FormGroup; maxDate = new Date(); + blockList: any[] = []; + + blockID: any; + villageID: any; + villageList: any[] = []; + + stateCtrl = new FormControl(); + districtCtrl = new FormControl(); + blockCtrl = new FormControl(); + villageCtrl = new FormControl(); + + filteredStates!: Observable; + filteredDistricts!: Observable; + filteredBlocks!: Observable; + filteredVillages!: Observable; constructor( private confirmationService: ConfirmationService, @@ -119,18 +135,135 @@ export class SearchDialogComponent implements OnInit, DoCheck { private fb: FormBuilder, private httpServiceService: HttpServiceService, private registrarService: RegistrarService, - private sessionstorage:SessionStorageService, + private sessionstorage: SessionStorageService, private changeDetectorRef: ChangeDetectorRef - ) {} + ) { } ngOnInit() { this.fetchLanguageResponse(); this.newSearchForm = this.createBeneficiaryForm(); - // Call For MAster Data which will be loaded in Sub Components this.callMasterDataObservable(); this.getStatesData(); //to be called from masterobservable method layter this.today = new Date(); + + // initialize filtering + this.filteredStates = this.stateCtrl.valueChanges.pipe( + startWith(''), + map(value => this._filter(value, this.states, 'stateName')) + ); + + this.filteredDistricts = this.districtCtrl.valueChanges.pipe( + startWith(''), + map(value => this._filter(value, this.districts, 'districtName')) + ); + + this.filteredBlocks = this.blockCtrl.valueChanges.pipe( + startWith(''), + map(value => this._filter(value, this.blockList, 'blockName')) + ); + + this.filteredVillages = this.villageCtrl.valueChanges.pipe( + startWith(''), + map(value => this._filter(value, this.villageList, 'villageName')) + ); + } + + private _filter(value: any, list: any[], key: string): any[] { + if (!value || !list) return list; + + let filterValue: string; + if (typeof value === 'string') { + filterValue = value.toLowerCase(); + } else if (value && value[key]) { + // If an object was selected, use its label + filterValue = value[key].toLowerCase(); + } else { + return list; + } + + return list.filter(option => + option[key]?.toLowerCase().includes(filterValue) + ); + } + + + onStateSelected(state: any) { + if (!state) return; + this.newSearchForm.get('stateID')?.setValue(state.stateID); + // Call service directly + this.registrarService.getDistrictList(state.stateID).subscribe((res: any) => { + if (res && res.statusCode === 200) { + this.districts = res.data; + this.districtCtrl.setValue(''); // clear district field + this.blockCtrl.setValue(''); // clear block field + this.villageCtrl.setValue(''); // clear village field + this.blockList = []; + this.villageList = []; + } else { + this.confirmationService.alert( + this.currentLanguageSet.alerts.info.issueFetching, + 'error' + ); + } + }); + } + + onDistrictSelected(district: any) { + if (!district) return; + this.newSearchForm.get('districtID')?.setValue(district.districtID); + // Call service directly + this.registrarService.getSubDistrictList(district.districtID).subscribe((res: any) => { + if (res && res.statusCode === 200) { + this.blockList = res.data; + this.blockCtrl.setValue(''); // clear block field + this.villageCtrl.setValue(''); // clear village field + this.villageList = []; + } else { + this.confirmationService.alert( + this.currentLanguageSet.alerts.info.IssuesInFetchingDemographics, + 'error' + ); + } + }); + } + + onBlockSelected(block: any) { + if (!block) return; + this.newSearchForm.get('blockID')?.setValue(block.blockID); + // Call service directly + this.registrarService.getVillageList(block.blockID).subscribe((res: any) => { + if (res && res.statusCode === 200) { + this.villageList = res.data; + this.villageCtrl.setValue(''); // clear village field + } else { + this.confirmationService.alert( + this.currentLanguageSet.alerts.info.IssuesInFetchingLocationDetails, + 'error' + ); + } + }); + } + + onVillageSelected(village: any) { + if (!village) return; + this.newSearchForm.get('villageID')?.setValue(village.districtBranchID); + } + + displayStateFn(state?: any): string { + return state ? state.stateName : ''; + } + + displayDistrictFn(district?: any): string { + return district ? district.districtName : ''; + } + + displayBlockFn(block?: any): string { + return block ? block.blockName : ''; + } + + displayVillageFn(village?: any): string { + return village ? village.villageName : ''; } AfterViewChecked() { @@ -146,14 +279,26 @@ export class SearchDialogComponent implements OnInit, DoCheck { gender: [null, Validators.required], stateID: [null, Validators.required], districtID: [null, Validators.required], + blockID: [null], + villageID: [null], }); } - resetBeneficiaryForm() { this.newSearchForm.reset(); + + // Reset the autocomplete FormControls + this.stateCtrl.setValue(''); + this.districtCtrl.setValue(''); + this.blockCtrl.setValue(''); + this.villageCtrl.setValue(''); + + // Clear the lists so dropdowns are empty + this.districts = []; + this.blockList = []; + this.villageList = []; + this.getStatesData(); } - /** * * Call Master Data Observable @@ -190,7 +335,6 @@ export class SearchDialogComponent implements OnInit, DoCheck { this.newSearchForm.controls['genderName'] = element.genderName; } }); - console.log(this.newSearchForm.controls, 'csdvde'); } /** @@ -198,7 +342,6 @@ export class SearchDialogComponent implements OnInit, DoCheck { */ govtIDData() { - console.log(this.masterData, 'govtidddds'); const govID = this.masterData.govIdEntityMaster; const otherGovID = this.masterData.otherGovIdEntityMaster; @@ -206,10 +349,9 @@ export class SearchDialogComponent implements OnInit, DoCheck { govID.push(element); }); this.govtIDs = govID; - console.log(this.govtIDs, 'idsss'); } - onIDCardSelected() {} + onIDCardSelected() { } /** * get states from localstorage and set default state @@ -225,33 +367,10 @@ export class SearchDialogComponent implements OnInit, DoCheck { this.locations.otherLoc.stateID; this.newSearchForm.controls['districtID'] = this.locations.otherLoc.districtList[0].districtID; - this.onStateChange(); } } } - onStateChange() { - const stateIDVal: any = this.newSearchForm.controls['stateID'].value; - if (stateIDVal) { - this.registrarService - .getDistrictList(stateIDVal) - .subscribe((res: any) => { - if (res && res.statusCode === 200) { - this.districts = res.data; - } else { - this.confirmationService.alert( - this.currentLanguageSet.alerts.info.issueFetching, - 'error' - ); - this.matDialogRef.close(false); - } - }); - } - } - // getStates() { - // this.commonService.getStates(this.countryId).subscribe(res => {this.states = res}); - - // } getDistricts(stateID: any) { this.commonService.getDistricts(stateID).subscribe(res => { @@ -271,6 +390,8 @@ export class SearchDialogComponent implements OnInit, DoCheck { i_bendemographics: { stateID: formValues.stateID, districtID: formValues.districtID, + blockID: formValues.blockID, + villageID: formValues.villageID, }, }; //Passing form data to component and closing the dialog diff --git a/src/registrar/search/search.component.ts b/src/registrar/search/search.component.ts index ff18762..814ea12 100644 --- a/src/registrar/search/search.component.ts +++ b/src/registrar/search/search.component.ts @@ -387,8 +387,8 @@ export class SearchComponent implements OnInit, DoCheck, AfterViewChecked { ); mdDialogRef.afterClosed().subscribe((result) => { + if (result) { - console.log('something fishy happening here', result); this.advanceSearchTerm = result; this.registrarService .advanceSearchIdentity(this.advanceSearchTerm) @@ -405,7 +405,7 @@ export class SearchComponent implements OnInit, DoCheck, AfterViewChecked { this.dataSource.paginator = this.paginator; this.quicksearchTerm = null; this.confirmationService.alert( - this.currentLanguageSet.alerts.info.beneficiarynotfound, + this.currentLanguageSet.alerts.info.beneficiaryNotFound, 'info', ); } else { diff --git a/src/tracking/index.ts b/src/tracking/index.ts new file mode 100644 index 0000000..c192d46 --- /dev/null +++ b/src/tracking/index.ts @@ -0,0 +1,6 @@ +export * from './lib/tracking-provider'; +export * from './lib/matomo-tracking.service'; +export * from './lib/ga-tracking.service'; +export * from './lib/amrit-tracking.service'; +export * from './lib/tracking.module'; +export * from './lib/tracking.tokens'; \ No newline at end of file diff --git a/src/tracking/lib/amrit-tracking.service.ts b/src/tracking/lib/amrit-tracking.service.ts new file mode 100644 index 0000000..44d3e5a --- /dev/null +++ b/src/tracking/lib/amrit-tracking.service.ts @@ -0,0 +1,110 @@ +import { Injectable, Inject, OnDestroy } from '@angular/core'; +import { TrackingProvider } from './tracking-provider'; +import { SessionStorageService } from '../../registrar/services/session-storage.service'; +import { Router, NavigationEnd } from '@angular/router'; +import { filter, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { TRACKING_PROVIDER } from './tracking.tokens'; + +@Injectable({ + providedIn: 'root' +}) +export class AmritTrackingService implements OnDestroy { + private destroy$ = new Subject(); + + constructor( + @Inject(TRACKING_PROVIDER) private trackingProvider: TrackingProvider, + private sessionStorage: SessionStorageService, + private router: Router + ) { + + try { + this.trackingProvider.init?.(); + this.setupPageViewTracking(); + this.setupUserTracking(); + } catch (error) { + console.error('Error initializing tracking provider:', error); + + this.trackingProvider = { + init: () => console.warn('Fallback: Tracking provider initialization failed'), + setUserId: () => console.warn('Fallback: Tracking provider setUserId failed'), + pageView: () => console.warn('Fallback: Tracking provider pageView failed'), + event: () => console.warn('Fallback: Tracking provider event failed'), + }; + + this.trackingProvider.init(); + } + } + + private setupPageViewTracking() { + this.router.events.pipe( + filter((event): event is NavigationEnd => event instanceof NavigationEnd), + takeUntil(this.destroy$) + ).subscribe((event) => { + this.trackingProvider.pageView(event.urlAfterRedirects); + }); + } + + private setupUserTracking() { + const userId = this.sessionStorage.getItem('userID'); + if (typeof userId === 'string' && userId !== null && userId !== undefined && userId !== '') { + this.trackingProvider.setUserId(userId); + } + } + + // Public methods to track events + trackEvent(category: string, action: string, label?: string, value?: number) { + try { + if (label !== undefined && value !== undefined) { + this.trackingProvider.event(category, action, label, value); + } else if (label !== undefined) { + this.trackingProvider.event(category, action, label); + } else if (value !== undefined) { + this.trackingProvider.event(category, action, undefined, value); + } else { + this.trackingProvider.event(category, action); + } + } catch (error) { + console.error(`Error tracking event ${category}:`, error); + } + } + + trackButtonClick(buttonName: string) { + this.trackEvent('UI', 'ButtonClick', buttonName); + } + + trackFormSubmit(formName: string) { + this.trackEvent('Form', 'Submit', formName); + } + + trackFeatureUsage(featureName: string) { + this.trackEvent('Feature', 'Usage', featureName); + } + + trackError(errorMessage: string, errorSource?: string) { + if (errorSource !== undefined) { + this.trackEvent('Error', errorMessage, errorSource); + } else { + this.trackEvent('Error', errorMessage); + } + } + + trackFieldInteraction(fieldName: string, category: string = 'Registration') { + this.trackEvent(category, 'Field Interaction', fieldName); + } + + setUserId(userId: string | number) { + if (userId && userId !== null && userId !== undefined && userId !== '') { + const userIdString = userId.toString(); + this.trackingProvider.setUserId(userIdString); + console.log('User ID manually set in tracking provider:', userIdString); + } else { + console.warn('Invalid user ID provided to setUserId:', userId); + } + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} \ No newline at end of file diff --git a/src/tracking/lib/ga-tracking.service.ts b/src/tracking/lib/ga-tracking.service.ts new file mode 100644 index 0000000..ac53b40 --- /dev/null +++ b/src/tracking/lib/ga-tracking.service.ts @@ -0,0 +1,192 @@ +declare let gtag: ( + command: 'config' | 'set' | 'event' | 'js', + targetId: string | Date, + config?: any +) => void; + +import { Injectable, Inject } from '@angular/core'; +import { TrackingProvider } from './tracking-provider'; +import { MATOMO_SITE_ID, TRACKING_ENABLED } from './tracking.tokens'; + +@Injectable() +export class GATrackingService implements TrackingProvider { + private isScriptLoaded = false; + private isInitialized = false; + private initQueue: (() => void)[] = []; + + constructor( + @Inject(MATOMO_SITE_ID) private gaTrackingId: string, + @Inject(TRACKING_ENABLED) private trackingEnabled: boolean + ) { + this.initializeDataLayer(); + } + + private initializeDataLayer() { + const _w = window as any; + if (!_w.dataLayer) { + _w.dataLayer = []; + } + if (!_w.gtag) { + _w.gtag = function() { + _w.dataLayer.push(arguments); + }; + } + } + + private executeWhenReady(callback: () => void) { + if (this.isScriptLoaded) { + callback(); + } else { + this.initQueue.push(callback); + } + } + + private processQueue() { + while (this.initQueue.length > 0) { + const callback = this.initQueue.shift(); + if (callback) callback(); + } + } + + init(gaTrackingId?: string | number, trackerUrl?: string) { + if (!this.trackingEnabled) { + console.log('GA Tracking is disabled'); + return; + } + + const finalTrackingId = gaTrackingId?.toString() || this.gaTrackingId?.toString(); + + if (!finalTrackingId || typeof finalTrackingId !== 'string') { + console.error('GATrackingService: Invalid tracking ID:', finalTrackingId); + return; + } + + console.log('Initializing GA with tracking ID:', finalTrackingId); + + this.loadGtagScript(finalTrackingId).then(() => { + const _w = window as any; + _w.gtag('js', new Date()); + _w.gtag('config', finalTrackingId, { + page_title: document.title, + page_location: window.location.href, + send_page_view: true + }); + + this.isInitialized = true; + console.log('GA initialized successfully'); + this.processQueue(); + }); + } + + private loadGtagScript(trackingId: string): Promise { + return new Promise((resolve, reject) => { + if (this.isScriptLoaded) { + resolve(); + return; + } + + const existingScript = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${trackingId}"]`); + if (existingScript) { + this.isScriptLoaded = true; + resolve(); + return; + } + + const script = document.createElement('script'); + script.async = true; + script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`; + + script.onload = () => { + this.isScriptLoaded = true; + console.log('GA script loaded successfully'); + resolve(); + }; + + script.onerror = (error) => { + console.error('Failed to load Google Analytics script:', error); + reject(error); + }; + + document.head.appendChild(script); + }); + } + + setUserId(userId: string) { + if (!this.trackingEnabled) return; + + if (!userId || userId === '' || userId === 'undefined' || userId === 'null') { + console.error('GATrackingService: Invalid userId:', userId); + return; + } + + this.executeWhenReady(() => { + if (typeof gtag !== 'undefined') { + gtag('config', this.gaTrackingId, { + user_id: userId + }); + console.log('GA User ID set:', userId); + } else { + console.error('gtag not available for setUserId'); + } + }); + } + + pageView(path: string) { + if (!this.trackingEnabled) return; + + if (!path || typeof path !== 'string') { + console.error('GATrackingService: Invalid path:', path); + return; + } + + this.executeWhenReady(() => { + if (typeof gtag !== 'undefined') { + gtag('config', this.gaTrackingId, { + page_path: path, + page_title: document.title, + page_location: window.location.origin + path + }); + console.log('GA Page view tracked:', path); + } else { + console.error('gtag not available for pageView'); + } + }); + } + + event(category: string, action: string, label?: string, value?: number) { + if (!this.trackingEnabled) { + return; + } + + if (!category || typeof category !== 'string') { + console.error('GATrackingService: Invalid category:', category); + return; + } + + if (!action || typeof action !== 'string') { + console.error('GATrackingService: Invalid action:', action); + return; + } + + this.executeWhenReady(() => { + if (typeof gtag !== 'undefined') { + const eventParams: any = { + event_category: category + }; + + if (label && typeof label === 'string') { + eventParams.event_label = label; + } + + if (value !== undefined && typeof value === 'number') { + eventParams.value = value; + } + + gtag('event', action, eventParams); + console.log('GA Event tracked:', { category, action, label, value }); + } else { + console.error('gtag not available for event tracking!'); + } + }); + } +} \ No newline at end of file diff --git a/src/tracking/lib/matomo-tracking.service.ts b/src/tracking/lib/matomo-tracking.service.ts new file mode 100644 index 0000000..b68b417 --- /dev/null +++ b/src/tracking/lib/matomo-tracking.service.ts @@ -0,0 +1,150 @@ +import { Injectable, Inject } from '@angular/core'; +import { TrackingProvider } from './tracking-provider'; +import { MATOMO_SITE_ID, MATOMO_URL, TRACKING_ENABLED } from './tracking.tokens'; + +@Injectable() +export class MatomoTrackingService implements TrackingProvider { + private isScriptLoaded = false; + private isInitialized = false; + + constructor( + @Inject(MATOMO_SITE_ID) private siteId: number, + @Inject(MATOMO_URL) private trackerUrl: string, + @Inject(TRACKING_ENABLED) private trackingEnabled: boolean + ) { + this.initializePaqQueue(); + } + + private initializePaqQueue() { + const _w = window as any; + if (!_w._paq) { + _w._paq = []; + } + } + + init(siteId?: number, trackerUrl?: string) { + + if (!this.trackingEnabled) { + return; + } + + const finalSiteId = siteId || this.siteId; + const finalTrackerUrl = trackerUrl || this.trackerUrl; + + if (!finalSiteId || typeof finalSiteId !== 'number') { + console.error('MatomoTrackingService: Invalid siteId:', finalSiteId); + return; + } + + if (!finalTrackerUrl || typeof finalTrackerUrl !== 'string') { + console.error('MatomoTrackingService: Invalid trackerUrl:', finalTrackerUrl); + return; + } + + const _w = window as any; + const _paq = _w._paq; + + if (!_paq) { + console.error('_paq queue is not available!'); + return; + } + + _paq.push(['setTrackerUrl', finalTrackerUrl + 'matomo.php']); + _paq.push(['setSiteId', finalSiteId]); + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + + this.loadMatomoScript(finalTrackerUrl); + this.isInitialized = true; + + } + + private loadMatomoScript(trackerUrl: string) { + if (this.isScriptLoaded) { + return; + } + + const script = document.createElement('script'); + script.async = true; + script.src = trackerUrl + 'matomo.js'; + + script.onload = () => { + this.isScriptLoaded = true; + }; + + script.onerror = (error) => { + console.error('Failed to load Matomo script:', error); + }; + + const firstScript = document.getElementsByTagName('script')[0]; + if (firstScript && firstScript.parentNode) { + firstScript.parentNode.insertBefore(script, firstScript); + } + } + + setUserId(userId: string) { + if (!this.trackingEnabled) return; + + if (!userId || userId === '' || userId === 'undefined' || userId === 'null') { + console.error('MatomoTrackingService: Invalid userId:', userId); + return; + } + + const _paq = (window as any)._paq; + if (!_paq) { + console.error('_paq not available for setUserId'); + return; + } + + _paq.push(['setUserId', userId]); + } + + pageView(path: string) { + if (!this.trackingEnabled) return; + + if (!path || typeof path !== 'string') { + console.error('MatomoTrackingService: Invalid path:', path); + return; + } + + const _paq = (window as any)._paq; + if (!_paq) { + console.error('_paq not available for pageView'); + return; + } + + _paq.push(['setCustomUrl', path]); + _paq.push(['trackPageView']); + } + + event(category: string, action: string, label?: string, value?: number) { + if (!this.trackingEnabled) { + return; + } + + const _paq = (window as any)._paq; + if (!_paq) { + console.error('_paq not available for event tracking!'); + return; + } + + if (!category || typeof category !== 'string') { + console.error('MatomoTrackingService: Invalid category:', category); + return; + } + + if (!action || typeof action !== 'string') { + console.error('MatomoTrackingService: Invalid action:', action); + return; + } + + const args: (string | number)[] = ['trackEvent', category, action]; + if (label && typeof label === 'string') { + args.push(label); + } + if (value !== undefined && typeof value === 'number') { + args.push(value); + } + _paq.push(args); + } +} \ No newline at end of file diff --git a/src/tracking/lib/tracking-provider.ts b/src/tracking/lib/tracking-provider.ts new file mode 100644 index 0000000..1fa15f8 --- /dev/null +++ b/src/tracking/lib/tracking-provider.ts @@ -0,0 +1,11 @@ +export interface TrackingProvider { + init(siteId?: number | string, trackerUrl?: string): void; + setUserId(userId: string): void; + pageView(path: string): void; + event( + category: string, + action: string, + label?: string, + value?: number + ): void; + } \ No newline at end of file diff --git a/src/tracking/lib/tracking.module.ts b/src/tracking/lib/tracking.module.ts new file mode 100644 index 0000000..a701f81 --- /dev/null +++ b/src/tracking/lib/tracking.module.ts @@ -0,0 +1,36 @@ +import { NgModule, ModuleWithProviders, Injector } from '@angular/core'; +import { TrackingProvider } from './tracking-provider'; +import { MatomoTrackingService } from './matomo-tracking.service'; +import { GATrackingService } from './ga-tracking.service'; +import { AmritTrackingService } from './amrit-tracking.service'; +import { TRACKING_PLATFORM, MATOMO_SITE_ID, MATOMO_URL, TRACKING_PROVIDER, TRACKING_ENABLED } from './tracking.tokens'; +import { environment } from 'src/environments/environment'; + +@NgModule({}) +export class TrackingModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: TrackingModule, + providers: [ + { provide: TRACKING_PLATFORM, useValue: environment.tracking?.platform || 'matomo' }, + { provide: TRACKING_ENABLED, useValue: environment.tracking?.enabled ?? false }, + { provide: MATOMO_SITE_ID, useValue: environment.tracking?.siteId }, + { provide: MATOMO_URL, useValue: environment.tracking?.trackerUrl }, + { + provide: TRACKING_PROVIDER, + useFactory: (platform: string, injector: Injector) => + platform === 'matomo' + ? injector.get(MatomoTrackingService) + : injector.get(GATrackingService), + deps: [TRACKING_PLATFORM, Injector], + }, + MatomoTrackingService, + GATrackingService, + AmritTrackingService, + ], + }; + } +} + +// Re-export the tokens from the module if needed elsewhere +export { TRACKING_PLATFORM, MATOMO_SITE_ID, MATOMO_URL, TRACKING_PROVIDER, TRACKING_ENABLED} from './tracking.tokens'; \ No newline at end of file diff --git a/src/tracking/lib/tracking.tokens.ts b/src/tracking/lib/tracking.tokens.ts new file mode 100644 index 0000000..528fa19 --- /dev/null +++ b/src/tracking/lib/tracking.tokens.ts @@ -0,0 +1,25 @@ +import { InjectionToken } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { TrackingProvider } from './tracking-provider'; + +export const TRACKING_PLATFORM = new InjectionToken('tracking.platform', { + providedIn: 'root', + factory: () => environment.tracking.trackingPlatform, +}); + +export const TRACKING_ENABLED = new InjectionToken('tracking.enabled', { + providedIn: 'root', + factory: () => environment.tracking.enabled, + }); + +export const MATOMO_SITE_ID = new InjectionToken('tracking.siteId', { + providedIn: 'root', + factory: () => environment.tracking.siteId, +}); + +export const MATOMO_URL = new InjectionToken('matomo.url', { + providedIn: 'root', + factory: () => environment.tracking.trackerUrl, +}); + +export const TRACKING_PROVIDER = new InjectionToken('tracking.provider'); \ No newline at end of file