add changes

This commit is contained in:
AD2025
2025-12-26 00:18:28 +02:00
parent efb4f69e20
commit 54be275e05
17 changed files with 599 additions and 402 deletions

View File

@@ -313,7 +313,7 @@ router.get('/users/:userId', verifyToken, isAdmin, adminController.getUserById);
* type: string * type: string
* questionType: * questionType:
* type: string * type: string
* enum: [multiple_choice, true_false, short_answer] * enum: [multiple_choice, trueFalse, short_answer]
* options: * options:
* type: array * type: array
* items: * items:

View File

@@ -1,5 +1,5 @@
import { ApplicationConfig, ErrorHandler, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core'; import { ApplicationConfig, ErrorHandler, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router'; import { provideRouter, withPreloading, PreloadAllModules, withInMemoryScrolling } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
@@ -13,7 +13,10 @@ export const appConfig: ApplicationConfig = {
provideZonelessChangeDetection(), provideZonelessChangeDetection(),
provideRouter( provideRouter(
routes, routes,
withPreloading(PreloadAllModules) withPreloading(PreloadAllModules),
withInMemoryScrolling({
scrollPositionRestoration: 'top'
})
), ),
provideAnimationsAsync(), provideAnimationsAsync(),
provideHttpClient( provideHttpClient(

View File

@@ -8,40 +8,55 @@
*/ */
export interface UserGrowthData { export interface UserGrowthData {
date: string; date: string;
count: number; newUsers: number;
} }
/** /**
* Category popularity data for chart * Category popularity data for chart
*/ */
export interface CategoryPopularity { export interface CategoryPopularity {
categoryId: string; id: string;
categoryName: string; name: string;
slug: string;
icon: any;
color: string;
quizCount: number; quizCount: number;
percentage: number; averageScore: number;
} }
/** /**
* System-wide statistics response * System-wide statistics response
*/ */
export interface AdminStatistics { export interface AdminStatistics {
totalUsers: number; users: AdminStatisticsUsers;
activeUsers: number; // Last 7 days quizzes: AdminStatisticsQuizzes;
totalQuizSessions: number; content: AdminStatisticsContent;
totalQuestions: number; quizActivity: QuizActivity[];
averageQuizScore: number;
userGrowth: UserGrowthData[]; userGrowth: UserGrowthData[];
popularCategories: CategoryPopularity[]; popularCategories: CategoryPopularity[];
stats: { }
newUsersToday: number; export interface AdminStatisticsContent {
newUsersThisWeek: number; totalCategories: number;
newUsersThisMonth: number; totalQuestions: number;
quizzesToday: number; questionsByDifficulty: {
quizzesThisWeek: number; easy: number;
quizzesThisMonth: number; medium: number;
hard: number;
}; };
} }
export interface AdminStatisticsQuizzes {
totalSessions: number;
averageScore: number;
averageScorePercentage: number;
passRate: number;
passedQuizzes: number;
failedQuizzes: number;
}
export interface AdminStatisticsUsers {
total: number;
active: number;
inactiveLast7Days: number;
}
/** /**
* API response wrapper for statistics * API response wrapper for statistics
*/ */
@@ -51,6 +66,10 @@ export interface AdminStatisticsResponse {
message?: string; message?: string;
} }
export interface QuizActivity {
date: string;
quizzesCompleted: number;
}
/** /**
* Date range filter for statistics * Date range filter for statistics
*/ */
@@ -71,43 +90,58 @@ export interface AdminCacheEntry<T> {
/** /**
* Guest session timeline data point * Guest session timeline data point
*/ */
export interface GuestSessionTimelineData { // export interface GuestSessionTimelineData {
date: string; // date: string;
activeSessions: number; // activeSessions: number;
newSessions: number; // newSessions: number;
convertedSessions: number; // convertedSessions: number;
} // }
/** /**
* Conversion funnel stage * Conversion funnel stage
*/ */
export interface ConversionFunnelStage { // export interface ConversionFunnelStage {
stage: string; // stage: string;
count: number; // count: number;
percentage: number; // percentage: number;
dropoff?: number; // dropoff?: number;
} // }
/** /**
* Guest analytics data * Guest analytics data
*/ */
export interface GuestAnalytics {
export interface GuestAnalyticsOverview {
totalGuestSessions: number; totalGuestSessions: number;
activeGuestSessions: number; activeGuestSessions: number;
conversionRate: number; // Percentage expiredGuestSessions: number;
averageQuizzesPerGuest: number; convertedGuestSessions: number;
totalConversions: number; conversionRate: number;
timeline: GuestSessionTimelineData[]; }
conversionFunnel: ConversionFunnelStage[]; export interface GuestAnalyticsQuizActivity {
stats: { totalGuestQuizzes: number;
sessionsToday: number; completedGuestQuizzes: number;
sessionsThisWeek: number; guestQuizCompletionRate: number;
sessionsThisMonth: number; avgQuizzesPerGuest: number;
conversionsToday: number; avgQuizzesBeforeConversion: number;
conversionsThisWeek: number; }
conversionsThisMonth: number;
export interface GuestAnalyticsBehavior {
bounceRate: number;
avgSessionDurationMinutes: number;
}
export interface GuestAnalyticsRecentActivity {
last30Days: {
newGuestSessions: number;
conversions: number;
}; };
} }
export interface GuestAnalytics {
overview: GuestAnalyticsOverview;
quizActivity: GuestAnalyticsQuizActivity;
behavior: GuestAnalyticsBehavior;
recentActivity: GuestAnalyticsRecentActivity;
}
/** /**
* API response wrapper for guest analytics * API response wrapper for guest analytics
@@ -183,11 +217,11 @@ export interface AdminUserListResponse {
pagination: { pagination: {
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
totalUsers: number; totalItems: number;
limit: number; itemsPerPage: number;
hasNext: boolean; hasNextPage: boolean;
hasPrev: boolean; hasPreviousPage: boolean;
}; };
}; };
message?: string; message?: string;
} }
@@ -197,7 +231,13 @@ export interface AdminUserListResponse {
*/ */
export interface UserActivity { export interface UserActivity {
id: string; id: string;
type: 'login' | 'quiz_start' | 'quiz_complete' | 'bookmark' | 'profile_update' | 'role_change'; type:
| 'login'
| 'quiz_start'
| 'quiz_complete'
| 'bookmark'
| 'profile_update'
| 'role_change';
description: string; description: string;
timestamp: string; timestamp: string;
metadata?: { metadata?: {

View File

@@ -73,12 +73,14 @@ export class AdminService {
readonly isLoadingUsers = signal<boolean>(false); readonly isLoadingUsers = signal<boolean>(false);
readonly usersError = signal<string | null>(null); readonly usersError = signal<string | null>(null);
readonly usersPagination = signal<{ readonly usersPagination = signal<{
currentPage: number;
totalPages: number; currentPage: number;
totalUsers: number; totalPages: number;
limit: number; totalItems: number;
hasNext: boolean; itemsPerPage: number;
hasPrev: boolean; hasNextPage: boolean;
hasPreviousPage: boolean;
} | null>(null); } | null>(null);
readonly currentUserFilters = signal<UserListParams>({}); readonly currentUserFilters = signal<UserListParams>({});
@@ -95,18 +97,18 @@ export class AdminService {
// Computed signals - Statistics // Computed signals - Statistics
readonly hasStats = computed(() => this.adminStatsState() !== null); readonly hasStats = computed(() => this.adminStatsState() !== null);
readonly totalUsers = computed(() => this.adminStatsState()?.totalUsers ?? 0); readonly totalUsers = computed(() => this.adminStatsState()?.users.total ?? 0);
readonly activeUsers = computed(() => this.adminStatsState()?.activeUsers ?? 0); readonly activeUsers = computed(() => this.adminStatsState()?.users.active ?? 0);
readonly totalQuizSessions = computed(() => this.adminStatsState()?.totalQuizSessions ?? 0); readonly totalQuizSessions = computed(() => this.adminStatsState()?.quizzes.totalSessions ?? 0);
readonly totalQuestions = computed(() => this.adminStatsState()?.totalQuestions ?? 0); readonly totalQuestions = computed(() => this.adminStatsState()?.content.totalQuestions ?? 0);
readonly averageScore = computed(() => this.adminStatsState()?.averageQuizScore ?? 0); readonly averageScore = computed(() => this.adminStatsState()?.quizzes.averageScore ?? 0);
// Computed signals - Guest Analytics // Computed signals - Guest Analytics
readonly hasAnalytics = computed(() => this.guestAnalyticsState() !== null); readonly hasAnalytics = computed(() => this.guestAnalyticsState() !== null);
readonly totalGuestSessions = computed(() => this.guestAnalyticsState()?.totalGuestSessions ?? 0); readonly totalGuestSessions = computed(() => this.guestAnalyticsState()?.overview.totalGuestSessions ?? 0);
readonly activeGuestSessions = computed(() => this.guestAnalyticsState()?.activeGuestSessions ?? 0); readonly activeGuestSessions = computed(() => this.guestAnalyticsState()?.overview.activeGuestSessions ?? 0);
readonly conversionRate = computed(() => this.guestAnalyticsState()?.conversionRate ?? 0); readonly conversionRate = computed(() => this.guestAnalyticsState()?.overview.conversionRate ?? 0);
readonly avgQuizzesPerGuest = computed(() => this.guestAnalyticsState()?.averageQuizzesPerGuest ?? 0); readonly avgQuizzesPerGuest = computed(() => this.guestAnalyticsState()?.quizActivity.avgQuizzesPerGuest ?? 0);
// Computed signals - Guest Settings // Computed signals - Guest Settings
readonly hasSettings = computed(() => this.guestSettingsState() !== null); readonly hasSettings = computed(() => this.guestSettingsState() !== null);
@@ -116,7 +118,7 @@ export class AdminService {
// Computed signals - User Management // Computed signals - User Management
readonly hasUsers = computed(() => this.adminUsersState().length > 0); readonly hasUsers = computed(() => this.adminUsersState().length > 0);
readonly totalUsersCount = computed(() => this.usersPagination()?.totalUsers ?? 0); readonly totalUsersCount = computed(() => this.usersPagination()?.totalItems ?? 0);
readonly currentPage = computed(() => this.usersPagination()?.currentPage ?? 1); readonly currentPage = computed(() => this.usersPagination()?.currentPage ?? 1);
readonly totalPages = computed(() => this.usersPagination()?.totalPages ?? 1); readonly totalPages = computed(() => this.usersPagination()?.totalPages ?? 1);

View File

@@ -16,13 +16,13 @@ export interface PaginationConfig {
*/ */
export interface PaginationState { export interface PaginationState {
currentPage: number; currentPage: number;
pageSize: number; itemsPerPage: number;
totalItems: number; totalItems: number;
totalPages: number; totalPages: number;
startIndex: number; startIndex: number;
endIndex: number; endIndex: number;
hasPrev: boolean; hasPreviousPage: boolean;
hasNext: boolean; hasNextPage: boolean;
} }
/** /**
@@ -61,13 +61,13 @@ export class PaginationService {
return { return {
currentPage: validCurrentPage, currentPage: validCurrentPage,
pageSize, itemsPerPage:pageSize,
totalItems, totalItems,
totalPages, totalPages,
startIndex, startIndex,
endIndex, endIndex,
hasPrev, hasNextPage:hasNext,
hasNext hasPreviousPage:hasPrev
}; };
} }
@@ -208,13 +208,13 @@ export class PaginationService {
}, },
nextPage: () => { nextPage: () => {
if (state().hasNext) { if (state().hasNextPage) {
config.update(c => ({ ...c, currentPage: c.currentPage + 1 })); config.update(c => ({ ...c, currentPage: c.currentPage + 1 }));
} }
}, },
prevPage: () => { prevPage: () => {
if (state().hasPrev) { if (state().hasPreviousPage) {
config.update(c => ({ ...c, currentPage: c.currentPage - 1 })); config.update(c => ({ ...c, currentPage: c.currentPage - 1 }));
} }
}, },

View File

@@ -9,7 +9,12 @@
<p class="subtitle">System-wide statistics and analytics</p> <p class="subtitle">System-wide statistics and analytics</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button mat-icon-button (click)="refreshStats()" [disabled]="isLoading()" matTooltip="Refresh statistics"> <button
mat-icon-button
(click)="refreshStats()"
[disabled]="isLoading()"
matTooltip="Refresh statistics"
>
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
</button> </button>
</div> </div>
@@ -26,27 +31,47 @@
<div class="date-inputs"> <div class="date-inputs">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
<input matInput [matDatepicker]="startPicker" formControlName="startDate"> <input
<mat-datepicker-toggle matIconSuffix [for]="startPicker"></mat-datepicker-toggle> matInput
[matDatepicker]="startPicker"
formControlName="startDate"
/>
<mat-datepicker-toggle
matIconSuffix
[for]="startPicker"
></mat-datepicker-toggle>
<mat-datepicker #startPicker></mat-datepicker> <mat-datepicker #startPicker></mat-datepicker>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>End Date</mat-label> <mat-label>End Date</mat-label>
<input matInput [matDatepicker]="endPicker" formControlName="endDate"> <input
<mat-datepicker-toggle matIconSuffix [for]="endPicker"></mat-datepicker-toggle> matInput
[matDatepicker]="endPicker"
formControlName="endDate"
/>
<mat-datepicker-toggle
matIconSuffix
[for]="endPicker"
></mat-datepicker-toggle>
<mat-datepicker #endPicker></mat-datepicker> <mat-datepicker #endPicker></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="applyDateFilter()" <button
[disabled]="!dateRangeForm.value.startDate || !dateRangeForm.value.endDate"> mat-raised-button
color="primary"
(click)="applyDateFilter()"
[disabled]="
!dateRangeForm.value.startDate || !dateRangeForm.value.endDate
"
>
Apply Filter Apply Filter
</button> </button>
@if (hasDateFilter()) { @if (hasDateFilter()) {
<button mat-raised-button (click)="clearDateFilter()"> <button mat-raised-button (click)="clearDateFilter()">
Clear Filter Clear Filter
</button> </button>
} }
</div> </div>
</form> </form>
@@ -55,221 +80,345 @@
<!-- Loading State --> <!-- Loading State -->
@if (isLoading()) { @if (isLoading()) {
<div class="loading-container"> <div class="loading-container">
<mat-spinner diameter="60"></mat-spinner> <mat-spinner diameter="60"></mat-spinner>
<p>Loading statistics...</p> <p>Loading statistics...</p>
</div> </div>
} }
<!-- Error State --> <!-- Error State -->
@if (error() && !isLoading()) { @if (error() && !isLoading()) {
<mat-card class="error-card"> <mat-card class="error-card">
<mat-card-content> <mat-card-content>
<div class="error-content"> <div class="error-content">
<mat-icon color="warn">error_outline</mat-icon> <mat-icon color="warn">error_outline</mat-icon>
<h3>Failed to Load Statistics</h3> <h3>Failed to Load Statistics</h3>
<p>{{ error() }}</p> <p>{{ error() }}</p>
<button mat-raised-button color="primary" (click)="refreshStats()"> <button mat-raised-button color="primary" (click)="refreshStats()">
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
Try Again Try Again
</button> </button>
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} }
<!-- Statistics Content --> <!-- Statistics Content -->
@if (stats() && !isLoading()) { @if (stats() && !isLoading()) {
<!-- Statistics Cards --> <!-- Statistics Cards -->
<div class="stats-grid"> <div class="stats-grid">
<mat-card class="stat-card users-card"> <mat-card class="stat-card users-card">
<mat-card-content>
<div class="stat-icon">
<mat-icon>people</mat-icon>
</div>
<div class="stat-info">
<h3>Total Users</h3>
<p class="stat-value">{{ formatNumber(totalUsers()) }}</p>
@if (stats() && stats()!.stats.newUsersThisWeek) {
<p class="stat-detail">+{{ stats()!.stats.newUsersThisWeek }} this week</p>
}
</div>
</mat-card-content>
</mat-card>
<mat-card class="stat-card active-card">
<mat-card-content>
<div class="stat-icon">
<mat-icon>trending_up</mat-icon>
</div>
<div class="stat-info">
<h3>Active Users</h3>
<p class="stat-value">{{ formatNumber(activeUsers()) }}</p>
<p class="stat-detail">Last 7 days</p>
</div>
</mat-card-content>
</mat-card>
<mat-card class="stat-card quizzes-card">
<mat-card-content>
<div class="stat-icon">
<mat-icon>quiz</mat-icon>
</div>
<div class="stat-info">
<h3>Total Quizzes</h3>
<p class="stat-value">{{ formatNumber(totalQuizSessions()) }}</p>
@if (stats() && stats()!.stats.quizzesThisWeek) {
<p class="stat-detail">+{{ stats()!.stats.quizzesThisWeek }} this week</p>
}
</div>
</mat-card-content>
</mat-card>
<mat-card class="stat-card questions-card">
<mat-card-content>
<div class="stat-icon">
<mat-icon>help_outline</mat-icon>
</div>
<div class="stat-info">
<h3>Total Questions</h3>
<p class="stat-value">{{ formatNumber(totalQuestions()) }}</p>
<p class="stat-detail">In database</p>
</div>
</mat-card-content>
</mat-card>
</div>
<!-- Average Score Card -->
<mat-card class="score-card">
<mat-card-header>
<mat-card-title>
<mat-icon>bar_chart</mat-icon>
Average Quiz Score
</mat-card-title>
</mat-card-header>
<mat-card-content> <mat-card-content>
<div class="score-display"> <div class="stat-icon">
<div class="score-circle"> <mat-icon>people</mat-icon>
<span class="score-value">{{ formatPercentage(averageScore()) }}</span> </div>
</div> <div class="stat-info">
<p class="score-description"> <h3>Total Users</h3>
@if (averageScore() >= 80) { <p class="stat-value">{{ formatNumber(totalUsers()) }}</p>
<span class="excellent">Excellent performance across all quizzes</span> @if (stats() && stats()!.users.inactiveLast7Days) {
} @else if (averageScore() >= 60) { <p class="stat-detail">
<span class="good">Good performance overall</span> +{{ stats()!.users.inactiveLast7Days }} this week
} @else {
<span class="needs-improvement">Room for improvement</span>
}
</p> </p>
}
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<!-- User Growth Chart --> <mat-card class="stat-card active-card">
@if (userGrowthData().length > 0) { <mat-card-content>
<mat-card class="chart-card"> <div class="stat-icon">
<mat-card-header> <mat-icon>trending_up</mat-icon>
<mat-card-title> </div>
<mat-icon>show_chart</mat-icon> <div class="stat-info">
User Growth Over Time <h3>Active Users</h3>
</mat-card-title> <p class="stat-value">{{ formatNumber(activeUsers()) }}</p>
</mat-card-header> <p class="stat-detail">Last 7 days</p>
<mat-card-content> </div>
<div class="chart-container"> </mat-card-content>
<svg [attr.width]="chartWidth" [attr.height]="chartHeight" class="line-chart"> </mat-card>
<!-- Grid lines -->
<line x1="40" y1="40" x2="760" y2="40" stroke="#e0e0e0" stroke-width="1"/>
<line x1="40" y1="120" x2="760" y2="120" stroke="#e0e0e0" stroke-width="1"/>
<line x1="40" y1="200" x2="760" y2="200" stroke="#e0e0e0" stroke-width="1"/>
<line x1="40" y1="260" x2="760" y2="260" stroke="#e0e0e0" stroke-width="1"/>
<!-- Axes --> <mat-card class="stat-card quizzes-card">
<line x1="40" y1="40" x2="40" y2="260" stroke="#333" stroke-width="2"/> <mat-card-content>
<line x1="40" y1="260" x2="760" y2="260" stroke="#333" stroke-width="2"/> <div class="stat-icon">
<mat-icon>quiz</mat-icon>
</div>
<div class="stat-info">
<h3>Total Quizzes</h3>
<p class="stat-value">{{ formatNumber(totalQuizSessions()) }}</p>
@if (stats() && stats()!.quizzes) {
<p class="stat-detail">{{ stats()!.quizzes.averageScore }} Average score</p>
<p class="stat-detail">{{ stats()!.quizzes.averageScorePercentage }} Average score percentage</p>
<p class="stat-detail">{{ stats()!.quizzes.failedQuizzes }} Failed quizzes</p>
<p class="stat-detail">{{ stats()!.quizzes.passRate }} Pass rate</p>
<p class="stat-detail">{{ stats()!.quizzes.passedQuizzes }} Passed quizzes</p>
<p class="stat-detail">{{ stats()!.quizzes.totalSessions }} Total sessions</p>
}
</div>
</mat-card-content>
</mat-card>
<!-- Data line --> <mat-card class="stat-card questions-card">
<path [attr.d]="getUserGrowthPath()" fill="none" stroke="#3f51b5" stroke-width="3"/> <mat-card-content>
<div class="stat-icon">
<!-- Data points -->
@for (point of userGrowthData(); track point.date; let i = $index) {
<circle [attr.cx]="calculateChartX(i, userGrowthData().length)"
[attr.cy]="calculateChartY(point.count, i)"
r="4" fill="#3f51b5"/>
}
</svg>
</div>
</mat-card-content>
</mat-card>
}
<!-- Popular Categories Chart -->
@if (popularCategories().length > 0) {
<mat-card class="chart-card">
<mat-card-header>
<mat-card-title>
<mat-icon>category</mat-icon>
Most Popular Categories
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="chart-container">
<svg [attr.width]="chartWidth" [attr.height]="chartHeight" class="bar-chart">
<!-- Grid lines -->
<line x1="40" y1="40" x2="760" y2="40" stroke="#e0e0e0" stroke-width="1"/>
<line x1="40" y1="120" x2="760" y2="120" stroke="#e0e0e0" stroke-width="1"/>
<line x1="40" y1="200" x2="760" y2="200" stroke="#e0e0e0" stroke-width="1"/>
<!-- Axes -->
<line x1="40" y1="40" x2="40" y2="260" stroke="#333" stroke-width="2"/>
<line x1="40" y1="260" x2="760" y2="260" stroke="#333" stroke-width="2"/>
<!-- Bars -->
@for (bar of getCategoryBars(); track bar.label) {
<rect [attr.x]="bar.x" [attr.y]="bar.y" [attr.width]="bar.width"
[attr.height]="bar.height" fill="#4caf50" opacity="0.8"/>
<text [attr.x]="bar.x + bar.width / 2" [attr.y]="bar.y - 5"
text-anchor="middle" font-size="12" fill="#333">{{ bar.value }}</text>
<text [attr.x]="bar.x + bar.width / 2" y="280"
text-anchor="middle" font-size="11" fill="#666">{{ bar.label }}</text>
}
</svg>
</div>
</mat-card-content>
</mat-card>
}
<!-- Quick Actions -->
<div class="quick-actions">
<h2>Quick Actions</h2>
<div class="actions-grid">
<button mat-raised-button color="primary" (click)="goToUsers()">
<mat-icon>people</mat-icon>
Manage Users
</button>
<button mat-raised-button color="primary" (click)="goToQuestions()">
<mat-icon>help_outline</mat-icon> <mat-icon>help_outline</mat-icon>
Manage Questions </div>
</button> <div class="stat-info">
<button mat-raised-button color="primary" (click)="goToAnalytics()"> <h3>Total Questions</h3>
<mat-icon>analytics</mat-icon> <p class="stat-value">{{ formatNumber(totalQuestions()) }}</p>
View Analytics <p class="stat-detail">In database</p>
</button> </div>
<button mat-raised-button color="primary" (click)="goToSettings()"> </mat-card-content>
<mat-icon>settings</mat-icon> </mat-card>
System Settings </div>
</button>
<!-- Average Score Card -->
<mat-card class="score-card">
<mat-card-header>
<mat-card-title>
<mat-icon>bar_chart</mat-icon>
Average Quiz Score
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="score-display">
<div class="score-circle">
<span class="score-value">{{
formatPercentage(averageScore())
}}</span>
</div>
<p class="score-description">
@if (averageScore() >= 80) {
<span class="excellent"
>Excellent performance across all quizzes</span
>
} @else if (averageScore() >= 60) {
<span class="good">Good performance overall</span>
} @else {
<span class="needs-improvement">Room for improvement</span>
}
</p>
</div> </div>
</mat-card-content>
</mat-card>
<!-- User Growth Chart -->
<!-- @if (userGrowthData().length > 0) {
<mat-card class="chart-card">
<mat-card-header>
<mat-card-title>
<mat-icon>show_chart</mat-icon>
User Growth Over Time
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="chart-container">
<svg
[attr.width]="chartWidth"
[attr.height]="chartHeight"
class="line-chart"
> -->
<!-- Grid lines -->
<!-- <line
x1="40"
y1="40"
x2="760"
y2="40"
stroke="#e0e0e0"
stroke-width="1"
/>
<line
x1="40"
y1="120"
x2="760"
y2="120"
stroke="#e0e0e0"
stroke-width="1"
/>
<line
x1="40"
y1="200"
x2="760"
y2="200"
stroke="#e0e0e0"
stroke-width="1"
/>
<line
x1="40"
y1="260"
x2="760"
y2="260"
stroke="#e0e0e0"
stroke-width="1"
/> -->
<!-- Axes -->
<!-- <line
x1="40"
y1="40"
x2="40"
y2="260"
stroke="#333"
stroke-width="2"
/>
<line
x1="40"
y1="260"
x2="760"
y2="260"
stroke="#333"
stroke-width="2"
/> -->
<!-- Data line -->
<!-- <path
[attr.d]="getUserGrowthPath()"
fill="none"
stroke="#3f51b5"
stroke-width="3"
/> -->
<!-- Data points -->
<!-- @for (point of userGrowthData(); track point.date; let i = $index) {
<circle
[attr.cx]="calculateChartX(i, userGrowthData().length)"
[attr.cy]="calculateChartY(point.newUsers, i)"
r="4"
fill="#3f51b5"
/>
}
</svg>
</div>
</mat-card-content>
</mat-card>
} -->
<!-- Popular Categories Chart -->
@if (popularCategories().length > 0) {
<mat-card class="chart-card">
<mat-card-header>
<mat-card-title>
<mat-icon>category</mat-icon>
Most Popular Categories
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="chart-container">
<svg
[attr.width]="chartWidth"
[attr.height]="chartHeight"
class="bar-chart"
>
<!-- Grid lines -->
<line
x1="40"
y1="40"
x2="760"
y2="40"
stroke="#e0e0e0"
stroke-width="1"
/>
<line
x1="40"
y1="120"
x2="760"
y2="120"
stroke="#e0e0e0"
stroke-width="1"
/>
<line
x1="40"
y1="200"
x2="760"
y2="200"
stroke="#e0e0e0"
stroke-width="1"
/>
<!-- Axes -->
<line
x1="40"
y1="40"
x2="40"
y2="260"
stroke="#333"
stroke-width="2"
/>
<line
x1="40"
y1="260"
x2="760"
y2="260"
stroke="#333"
stroke-width="2"
/>
<!-- Bars -->
@for (bar of getCategoryBars(); track bar.label) {
<rect
[attr.x]="bar.x"
[attr.y]="bar.y"
[attr.width]="bar.width"
[attr.height]="bar.height"
fill="#4caf50"
opacity="0.8"
/>
<text
[attr.x]="bar.x + bar.width / 2"
[attr.y]="bar.y - 5"
text-anchor="middle"
font-size="12"
fill="#333"
>
{{ bar.value }}
</text>
<text
[attr.x]="bar.x + bar.width / 2"
y="280"
text-anchor="middle"
font-size="11"
fill="#666"
>
{{ bar.label }}
</text>
}
</svg>
</div>
</mat-card-content>
</mat-card>
}
<!-- Quick Actions -->
<div class="quick-actions">
<h2>Quick Actions</h2>
<div class="actions-grid">
<button mat-raised-button color="primary" (click)="goToUsers()">
<mat-icon>people</mat-icon>
Manage Users
</button>
<button mat-raised-button color="primary" (click)="goToQuestions()">
<mat-icon>help_outline</mat-icon>
Manage Questions
</button>
<button mat-raised-button color="primary" (click)="goToAnalytics()">
<mat-icon>analytics</mat-icon>
View Analytics
</button>
<button mat-raised-button color="primary" (click)="goToSettings()">
<mat-icon>settings</mat-icon>
System Settings
</button>
</div> </div>
</div>
} }
<!-- Empty State (no data yet) --> <!-- Empty State (no data yet) -->
@if (!stats() && !isLoading() && !error()) { @if (!stats() && !isLoading() && !error()) {
<mat-card class="empty-state"> <mat-card class="empty-state">
<mat-card-content> <mat-card-content>
<mat-icon>analytics</mat-icon> <mat-icon>analytics</mat-icon>
<h3>No Statistics Available</h3> <h3>No Statistics Available</h3>
<p>Statistics will appear here once users start taking quizzes</p> <p>Statistics will appear here once users start taking quizzes</p>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} }
</div> </div>

View File

@@ -166,7 +166,7 @@ export class AdminDashboardComponent implements OnInit, OnDestroy {
getMaxUserCount(): number { getMaxUserCount(): number {
const data = this.userGrowthData(); const data = this.userGrowthData();
if (data.length === 0) return 1; if (data.length === 0) return 1;
return Math.max(...data.map(d => d.count), 1); return Math.max(...data.map(d => d.newUsers), 1);
} }
/** /**
@@ -197,7 +197,7 @@ export class AdminDashboardComponent implements OnInit, OnDestroy {
const data = this.userGrowthData(); const data = this.userGrowthData();
if (data.length === 0) return ''; if (data.length === 0) return '';
const maxCount = Math.max(...data.map(d => d.count), 1); const maxCount = Math.max(...data.map(d => d.newUsers), 1);
const width = this.chartWidth; const width = this.chartWidth;
const height = this.chartHeight; const height = this.chartHeight;
const padding = 40; const padding = 40;
@@ -206,7 +206,7 @@ export class AdminDashboardComponent implements OnInit, OnDestroy {
const points = data.map((d, i) => { const points = data.map((d, i) => {
const x = padding + (i / (data.length - 1)) * plotWidth; const x = padding + (i / (data.length - 1)) * plotWidth;
const y = height - padding - (d.count / maxCount) * plotHeight; const y = height - padding - (d.newUsers / maxCount) * plotHeight;
return `${x},${y}`; return `${x},${y}`;
}); });
@@ -235,7 +235,7 @@ export class AdminDashboardComponent implements OnInit, OnDestroy {
y: height - padding - barHeight, y: height - padding - barHeight,
width: barWidth, width: barWidth,
height: barHeight, height: barHeight,
label: cat.categoryName, label: cat.name,
value: cat.quizCount value: cat.quizCount
}; };
}); });

View File

@@ -84,8 +84,8 @@ export class AdminQuestionFormComponent implements OnInit {
// Available options // Available options
readonly questionTypes = [ readonly questionTypes = [
{ value: 'multiple_choice', label: 'Multiple Choice' }, { value: 'multiple', label: 'Multiple Choice' },
{ value: 'true_false', label: 'True/False' }, { value: 'trueFalse', label: 'True/False' },
{ value: 'written', label: 'Written Answer' } { value: 'written', label: 'Written Answer' }
]; ];
@@ -198,7 +198,7 @@ export class AdminQuestionFormComponent implements OnInit {
private initializeForm(): void { private initializeForm(): void {
this.questionForm = this.fb.group({ this.questionForm = this.fb.group({
questionText: ['', [Validators.required, Validators.minLength(10)]], questionText: ['', [Validators.required, Validators.minLength(10)]],
questionType: ['multiple_choice', Validators.required], questionType: ['multiple', Validators.required],
categoryId: ['', Validators.required], categoryId: ['', Validators.required],
difficulty: ['medium', Validators.required], difficulty: ['medium', Validators.required],
options: this.fb.array([ options: this.fb.array([
@@ -333,7 +333,7 @@ export class AdminQuestionFormComponent implements OnInit {
const correctAnswer = formGroup.get('correctAnswer')?.value; const correctAnswer = formGroup.get('correctAnswer')?.value;
const options = formGroup.get('options') as FormArray; const options = formGroup.get('options') as FormArray;
if (questionType === 'multiple_choice' && correctAnswer && options) { if (questionType === 'multiple' && correctAnswer && options) {
const optionTexts = options.controls.map(opt => opt.get('text')?.value); const optionTexts = options.controls.map(opt => opt.get('text')?.value);
const isValid = optionTexts.includes(correctAnswer); const isValid = optionTexts.includes(correctAnswer);
@@ -378,7 +378,7 @@ export class AdminQuestionFormComponent implements OnInit {
}; };
// Add options for multiple choice // Add options for multiple choice
if (formValue.questionType === 'multiple_choice') { if (formValue.questionType === 'multiple') {
questionData.options = this.getOptionTexts(); questionData.options = this.getOptionTexts();
} }

View File

@@ -54,8 +54,8 @@
<mat-label>Type</mat-label> <mat-label>Type</mat-label>
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-option value="all">All Types</mat-option> <mat-option value="all">All Types</mat-option>
<mat-option value="multiple_choice">Multiple Choice</mat-option> <mat-option value="multiple">Multiple Choice</mat-option>
<mat-option value="true_false">True/False</mat-option> <mat-option value="trueFalse">True/False</mat-option>
<mat-option value="written">Written</mat-option> <mat-option value="written">Written</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@@ -137,15 +137,15 @@
<th mat-header-cell *matHeaderCellDef>Type</th> <th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let question"> <td mat-cell *matCellDef="let question">
<mat-chip> <mat-chip>
@if (question.questionType === 'multiple_choice') { @if (question.questionType === 'multiple') {
<mat-icon>radio_button_checked</mat-icon> <mat-icon>radio_button_checked</mat-icon>
<span>MCQ</span> <span class="px-5"> MCQ</span>
} @else if (question.questionType === 'true_false') { } @else if (question.questionType === 'trueFalse') {
<mat-icon>check_circle</mat-icon> <mat-icon>check_circle</mat-icon>
<span>T/F</span> <span> T/F</span>
} @else { } @else {
<mat-icon>edit_note</mat-icon> <mat-icon>edit_note</mat-icon>
<span>Written</span> <span> Written</span>
} }
</mat-chip> </mat-chip>
</td> </td>

View File

@@ -116,7 +116,7 @@
<h2>Users</h2> <h2>Users</h2>
@if (pagination()) { @if (pagination()) {
<span class="total-count"> <span class="total-count">
Total: {{ pagination()?.totalUsers }} user{{ pagination()?.totalUsers !== 1 ? 's' : '' }} Total: {{ pagination()?.totalItems }} user{{ pagination()?.totalItems !== 1 ? 's' : '' }}
</span> </span>
} }
</div> </div>

View File

@@ -83,8 +83,8 @@ export class AdminUsersComponent implements OnInit {
return this.paginationService.calculatePaginationState({ return this.paginationService.calculatePaginationState({
currentPage: pag.currentPage, currentPage: pag.currentPage,
pageSize: pag.limit, pageSize: pag.itemsPerPage,
totalItems: pag.totalUsers totalItems: pag.totalItems
}); });
}); });

View File

@@ -61,8 +61,8 @@
<div class="stat-info"> <div class="stat-info">
<h3>Total Guest Sessions</h3> <h3>Total Guest Sessions</h3>
<p class="stat-value">{{ formatNumber(totalSessions()) }}</p> <p class="stat-value">{{ formatNumber(totalSessions()) }}</p>
@if (analytics() && analytics()!.stats.sessionsThisWeek) { @if (analytics() && analytics()!.recentActivity.last30Days) {
<p class="stat-detail">+{{ analytics()!.stats.sessionsThisWeek }} this week</p> <p class="stat-detail">+{{ analytics()!.recentActivity.last30Days }} this 30 days</p>
} }
</div> </div>
</mat-card-content> </mat-card-content>
@@ -89,8 +89,8 @@
<div class="stat-info"> <div class="stat-info">
<h3>Conversion Rate</h3> <h3>Conversion Rate</h3>
<p class="stat-value">{{ formatPercentage(conversionRate()) }}</p> <p class="stat-value">{{ formatPercentage(conversionRate()) }}</p>
@if (analytics() && analytics()!.totalConversions) { @if (analytics() && analytics()!.overview.conversionRate) {
<p class="stat-detail">{{ analytics()!.totalConversions }} conversions</p> <p class="stat-detail">{{ analytics()!.overview.conversionRate }} conversions</p>
} }
</div> </div>
</mat-card-content> </mat-card-content>
@@ -111,7 +111,7 @@
</div> </div>
<!-- Session Timeline Chart --> <!-- Session Timeline Chart -->
@if (timelineData().length > 0) { <!-- @if (timelineData().length > 0) {
<mat-card class="chart-card"> <mat-card class="chart-card">
<mat-card-header> <mat-card-header>
<mat-card-title> <mat-card-title>
@@ -135,28 +135,28 @@
</div> </div>
</div> </div>
<div class="chart-container"> <div class="chart-container">
<svg [attr.width]="chartWidth" [attr.height]="chartHeight" class="timeline-chart"> <svg [attr.width]="chartWidth" [attr.height]="chartHeight" class="timeline-chart"> -->
<!-- Grid lines --> <!-- Grid lines -->
<line x1="40" y1="40" x2="760" y2="40" stroke="#e0e0e0" stroke-width="1"/> <!-- <line x1="40" y1="40" x2="760" y2="40" stroke="#e0e0e0" stroke-width="1"/>
<line x1="40" y1="120" x2="760" y2="120" stroke="#e0e0e0" stroke-width="1"/> <line x1="40" y1="120" x2="760" y2="120" stroke="#e0e0e0" stroke-width="1"/>
<line x1="40" y1="200" x2="760" y2="200" stroke="#e0e0e0" stroke-width="1"/> <line x1="40" y1="200" x2="760" y2="200" stroke="#e0e0e0" stroke-width="1"/>
<line x1="40" y1="260" x2="760" y2="260" stroke="#e0e0e0" stroke-width="1"/> <line x1="40" y1="260" x2="760" y2="260" stroke="#e0e0e0" stroke-width="1"/>
-->
<!-- Axes --> <!-- Axes -->
<line x1="40" y1="40" x2="40" y2="260" stroke="#333" stroke-width="2"/> <!-- <line x1="40" y1="40" x2="40" y2="260" stroke="#333" stroke-width="2"/>
<line x1="40" y1="260" x2="760" y2="260" stroke="#333" stroke-width="2"/> <line x1="40" y1="260" x2="760" y2="260" stroke="#333" stroke-width="2"/>
-->
<!-- Active Sessions Line --> <!-- Active Sessions Line -->
<path [attr.d]="getTimelinePath('activeSessions')" fill="none" stroke="#3f51b5" stroke-width="3"/> <!-- <path [attr.d]="getTimelinePath('activeSessions')" fill="none" stroke="#3f51b5" stroke-width="3"/> -->
<!-- New Sessions Line --> <!-- New Sessions Line -->
<path [attr.d]="getTimelinePath('newSessions')" fill="none" stroke="#4caf50" stroke-width="3"/> <!-- <path [attr.d]="getTimelinePath('newSessions')" fill="none" stroke="#4caf50" stroke-width="3"/> -->
<!-- Converted Sessions Line --> <!-- Converted Sessions Line -->
<path [attr.d]="getTimelinePath('convertedSessions')" fill="none" stroke="#ff9800" stroke-width="3"/> <!-- <path [attr.d]="getTimelinePath('convertedSessions')" fill="none" stroke="#ff9800" stroke-width="3"/> -->
<!-- Data points --> <!-- Data points -->
@for (point of timelineData(); track point.date; let i = $index) { <!-- @for (point of timelineData(); track point.date; let i = $index) {
<circle [attr.cx]="calculateTimelineX(i, timelineData().length)" <circle [attr.cx]="calculateTimelineX(i, timelineData().length)"
[attr.cy]="calculateTimelineY(point.activeSessions)" [attr.cy]="calculateTimelineY(point.activeSessions)"
r="4" fill="#3f51b5"/> r="4" fill="#3f51b5"/>
@@ -171,10 +171,10 @@
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} } -->
<!-- Conversion Funnel Chart --> <!-- Conversion Funnel Chart -->
@if (funnelData().length > 0) { <!-- @if (funnelData().length > 0) {
<mat-card class="chart-card"> <mat-card class="chart-card">
<mat-card-header> <mat-card-header>
<mat-card-title> <mat-card-title>
@@ -184,21 +184,21 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<div class="chart-container"> <div class="chart-container">
<svg [attr.width]="chartWidth" [attr.height]="funnelHeight" class="funnel-chart"> <svg [attr.width]="chartWidth" [attr.height]="funnelHeight" class="funnel-chart"> -->
<!-- Funnel Bars --> <!-- Funnel Bars -->
@for (bar of getFunnelBars(); track bar.label) { <!-- @for (bar of getFunnelBars(); track bar.label) {
<g> <g> -->
<!-- Bar --> <!-- Bar -->
<rect [attr.x]="bar.x" [attr.y]="bar.y" [attr.width]="bar.width" <!-- <rect [attr.x]="bar.x" [attr.y]="bar.y" [attr.width]="bar.width"
[attr.height]="bar.height" [attr.fill]="$index === 0 ? '#4caf50' : $index === getFunnelBars().length - 1 ? '#ff9800' : '#2196f3'" [attr.height]="bar.height" [attr.fill]="$index === 0 ? '#4caf50' : $index === getFunnelBars().length - 1 ? '#ff9800' : '#2196f3'"
opacity="0.8"/> opacity="0.8"/>
-->
<!-- Label --> <!-- Label -->
<text [attr.x]="bar.x + 10" [attr.y]="bar.y + bar.height / 2 - 5" <!-- <text [attr.x]="bar.x + 10" [attr.y]="bar.y + bar.height / 2 - 5"
font-size="14" font-weight="600" fill="#fff">{{ bar.label }}</text> font-size="14" font-weight="600" fill="#fff">{{ bar.label }}</text>
-->
<!-- Count and Percentage --> <!-- Count and Percentage -->
<text [attr.x]="bar.x + 10" [attr.y]="bar.y + bar.height / 2 + 15" <!-- <text [attr.x]="bar.x + 10" [attr.y]="bar.y + bar.height / 2 + 15"
font-size="12" fill="#fff">{{ formatNumber(bar.count) }} ({{ formatPercentage(bar.percentage) }})</text> font-size="12" fill="#fff">{{ formatNumber(bar.count) }} ({{ formatPercentage(bar.percentage) }})</text>
</g> </g>
} }
@@ -216,7 +216,7 @@
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} } -->
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="quick-actions"> <div class="quick-actions">

View File

@@ -58,8 +58,8 @@ export class GuestAnalyticsComponent implements OnInit, OnDestroy {
readonly avgQuizzes = this.adminService.avgQuizzesPerGuest; readonly avgQuizzes = this.adminService.avgQuizzesPerGuest;
// Chart data computed signals // Chart data computed signals
readonly timelineData = computed(() => this.analytics()?.timeline ?? []); // readonly timelineData = computed(() => this.analytics()?.timeline ?? []);
readonly funnelData = computed(() => this.analytics()?.conversionFunnel ?? []); // readonly funnelData = computed(() => this.analytics()?.conversionFunnel ?? []);
// Chart dimensions // Chart dimensions
readonly chartWidth = 800; readonly chartWidth = 800;
@@ -100,25 +100,25 @@ export class GuestAnalyticsComponent implements OnInit, OnDestroy {
/** /**
* Calculate max value for timeline chart * Calculate max value for timeline chart
*/ */
getMaxTimelineValue(): number { // getMaxTimelineValue(): number {
const data = this.timelineData(); // const data = this.timelineData();
if (data.length === 0) return 1; // if (data.length === 0) return 1;
return Math.max( // return Math.max(
...data.map(d => Math.max(d.activeSessions, d.newSessions, d.convertedSessions)), // ...data.map(d => Math.max(d.activeSessions, d.newSessions, d.convertedSessions)),
1 // 1
); // );
} // }
/** /**
* Calculate Y coordinate for timeline chart * Calculate Y coordinate for timeline chart
*/ */
calculateTimelineY(value: number): number { // calculateTimelineY(value: number): number {
const maxValue = this.getMaxTimelineValue(); // const maxValue = this.getMaxTimelineValue();
const height = this.chartHeight; // const height = this.chartHeight;
const padding = 40; // const padding = 40;
const plotHeight = height - 2 * padding; // const plotHeight = height - 2 * padding;
return height - padding - (value / maxValue) * plotHeight; // return height - padding - (value / maxValue) * plotHeight;
} // }
/** /**
* Calculate X coordinate for timeline chart * Calculate X coordinate for timeline chart
@@ -134,55 +134,55 @@ export class GuestAnalyticsComponent implements OnInit, OnDestroy {
/** /**
* Generate SVG path for timeline line * Generate SVG path for timeline line
*/ */
getTimelinePath(dataKey: 'activeSessions' | 'newSessions' | 'convertedSessions'): string { // getTimelinePath(dataKey: 'activeSessions' | 'newSessions' | 'convertedSessions'): string {
const data = this.timelineData(); // const data = this.timelineData();
if (data.length === 0) return ''; // if (data.length === 0) return '';
const points = data.map((d, i) => { // const points = data.map((d, i) => {
const x = this.calculateTimelineX(i, data.length); // const x = this.calculateTimelineX(i, data.length);
const y = this.calculateTimelineY(d[dataKey]); // const y = this.calculateTimelineY(d[dataKey]);
return `${x},${y}`; // return `${x},${y}`;
}); // });
return `M ${points.join(' L ')}`; // return `M ${points.join(' L ')}`;
} // }
/** /**
* Get conversion funnel bar data * Get conversion funnel bar data
*/ */
getFunnelBars(): Array<{ // getFunnelBars(): Array<{
x: number; // x: number;
y: number; // y: number;
width: number; // width: number;
height: number; // height: number;
label: string; // label: string;
count: number; // count: number;
percentage: number; // percentage: number;
}> { // }> {
const stages = this.funnelData(); // const stages = this.funnelData();
if (stages.length === 0) return []; // if (stages.length === 0) return [];
const maxCount = Math.max(...stages.map(s => s.count), 1); // const maxCount = Math.max(...stages.map(s => s.count), 1);
const width = this.chartWidth; // const width = this.chartWidth;
const height = this.funnelHeight; // const height = this.funnelHeight;
const padding = 60; // const padding = 60;
const plotWidth = width - 2 * padding; // const plotWidth = width - 2 * padding;
const plotHeight = height - 2 * padding; // const plotHeight = height - 2 * padding;
const barHeight = plotHeight / stages.length - 20; // const barHeight = plotHeight / stages.length - 20;
return stages.map((stage, i) => { // return stages.map((stage, i) => {
const barWidth = (stage.count / maxCount) * plotWidth; // const barWidth = (stage.count / maxCount) * plotWidth;
return { // return {
x: padding, // x: padding,
y: padding + i * (plotHeight / stages.length) + 10, // y: padding + i * (plotHeight / stages.length) + 10,
width: barWidth, // width: barWidth,
height: barHeight, // height: barHeight,
label: stage.stage, // label: stage.stage,
count: stage.count, // count: stage.count,
percentage: stage.percentage // percentage: stage.percentage
}; // };
}); // });
} // }
/** /**
* Export analytics data to CSV * Export analytics data to CSV
@@ -197,26 +197,26 @@ export class GuestAnalyticsComponent implements OnInit, OnDestroy {
// Summary statistics // Summary statistics
csvContent += 'Summary Statistics\n'; csvContent += 'Summary Statistics\n';
csvContent += 'Metric,Value\n'; csvContent += 'Metric,Value\n';
csvContent += `Total Guest Sessions,${analytics.totalGuestSessions}\n`; csvContent += `Total Guest Sessions,${analytics.overview.activeGuestSessions}\n`;
csvContent += `Active Guest Sessions,${analytics.activeGuestSessions}\n`; csvContent += `Active Guest Sessions,${analytics.overview.activeGuestSessions}\n`;
csvContent += `Conversion Rate,${analytics.conversionRate}%\n`; csvContent += `Conversion Rate,${analytics.overview.conversionRate}%\n`;
csvContent += `Average Quizzes per Guest,${analytics.averageQuizzesPerGuest}\n`; csvContent += `Average Quizzes per Guest,${analytics.quizActivity.avgQuizzesPerGuest}\n`;
csvContent += `Total Conversions,${analytics.totalConversions}\n\n`; csvContent += `Total Conversions,${analytics.overview.conversionRate}\n\n`;
// Timeline data // Timeline data
csvContent += 'Timeline Data\n'; csvContent += 'Timeline Data\n';
csvContent += 'Date,Active Sessions,New Sessions,Converted Sessions\n'; csvContent += 'Date,Active Sessions,New Sessions,Converted Sessions\n';
analytics.timeline.forEach(item => { // analytics.timeline.forEach(item => {
csvContent += `${item.date},${item.activeSessions},${item.newSessions},${item.convertedSessions}\n`; // csvContent += `${item.date},${item.activeSessions},${item.newSessions},${item.convertedSessions}\n`;
}); // });
csvContent += '\n'; csvContent += '\n';
// Funnel data // Funnel data
csvContent += 'Conversion Funnel\n'; csvContent += 'Conversion Funnel\n';
csvContent += 'Stage,Count,Percentage,Dropoff\n'; csvContent += 'Stage,Count,Percentage,Dropoff\n';
analytics.conversionFunnel.forEach(stage => { // analytics.conversionFunnel.forEach(stage => {
csvContent += `${stage.stage},${stage.count},${stage.percentage}%,${stage.dropoff ?? 'N/A'}\n`; // csvContent += `${stage.stage},${stage.count},${stage.percentage}%,${stage.dropoff ?? 'N/A'}\n`;
}); // });
// Create and download file // Create and download file
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });

View File

@@ -194,9 +194,9 @@ export class QuizResultsComponent implements OnInit, OnDestroy {
*/ */
getQuestionTypeText(type: string): string { getQuestionTypeText(type: string): string {
switch (type) { switch (type) {
case 'multiple_choice': case 'multiple':
return 'Multiple Choice'; return 'Multiple Choice';
case 'true_false': case 'trueFalse':
return 'True/False'; return 'True/False';
case 'written': case 'written':
return 'Written'; return 'Written';

View File

@@ -215,9 +215,9 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
*/ */
getQuestionTypeText(type: string): string { getQuestionTypeText(type: string): string {
switch (type) { switch (type) {
case 'multiple_choice': case 'multiple':
return 'Multiple Choice'; return 'Multiple Choice';
case 'true_false': case 'trueFalse':
return 'True/False'; return 'True/False';
case 'written': case 'written':
return 'Written'; return 'Written';

View File

@@ -1,4 +1,5 @@
<h2 mat-dialog-title> <div style="padding: 10px;">
<h2 mat-dialog-title>
@if (data.icon) { @if (data.icon) {
<mat-icon>{{ data.icon }}</mat-icon> <mat-icon>{{ data.icon }}</mat-icon>
} }
@@ -22,3 +23,5 @@
{{ data.confirmText || 'Confirm' }} {{ data.confirmText || 'Confirm' }}
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>
</div>

View File

@@ -57,7 +57,7 @@ import { PaginationState } from '../../../core/services/pagination.service';
@if (showPageSizeSelector()) { @if (showPageSizeSelector()) {
<mat-form-field class="page-size-selector" appearance="outline"> <mat-form-field class="page-size-selector" appearance="outline">
<mat-select <mat-select
[value]="state()!.pageSize" [value]="state()!.itemsPerPage"
(selectionChange)="onPageSizeChange($event.value)" (selectionChange)="onPageSizeChange($event.value)"
aria-label="Items per page"> aria-label="Items per page">
@for (option of pageSizeOptions(); track option) { @for (option of pageSizeOptions(); track option) {
@@ -74,7 +74,7 @@ import { PaginationState } from '../../../core/services/pagination.service';
<button <button
mat-icon-button mat-icon-button
(click)="onPageChange(1)" (click)="onPageChange(1)"
[disabled]="!state()!.hasPrev" [disabled]="!state()!.hasPreviousPage"
matTooltip="First page" matTooltip="First page"
aria-label="Go to first page"> aria-label="Go to first page">
<mat-icon>first_page</mat-icon> <mat-icon>first_page</mat-icon>
@@ -85,7 +85,7 @@ import { PaginationState } from '../../../core/services/pagination.service';
<button <button
mat-icon-button mat-icon-button
(click)="onPageChange(state()!.currentPage - 1)" (click)="onPageChange(state()!.currentPage - 1)"
[disabled]="!state()!.hasPrev" [disabled]="!state()!.hasPreviousPage"
matTooltip="Previous page" matTooltip="Previous page"
aria-label="Go to previous page"> aria-label="Go to previous page">
<mat-icon>chevron_left</mat-icon> <mat-icon>chevron_left</mat-icon>
@@ -114,7 +114,7 @@ import { PaginationState } from '../../../core/services/pagination.service';
<button <button
mat-icon-button mat-icon-button
(click)="onPageChange(state()!.currentPage + 1)" (click)="onPageChange(state()!.currentPage + 1)"
[disabled]="!state()!.hasNext" [disabled]="!state()!.hasNextPage"
matTooltip="Next page" matTooltip="Next page"
aria-label="Go to next page"> aria-label="Go to next page">
<mat-icon>chevron_right</mat-icon> <mat-icon>chevron_right</mat-icon>
@@ -125,7 +125,7 @@ import { PaginationState } from '../../../core/services/pagination.service';
<button <button
mat-icon-button mat-icon-button
(click)="onPageChange(state()!.totalPages)" (click)="onPageChange(state()!.totalPages)"
[disabled]="!state()!.hasNext" [disabled]="!state()!.hasNextPage"
matTooltip="Last page" matTooltip="Last page"
aria-label="Go to last page"> aria-label="Go to last page">
<mat-icon>last_page</mat-icon> <mat-icon>last_page</mat-icon>