Dashboard update

This commit is contained in:
2025-10-25 09:35:47 +05:30
parent 7cfe522512
commit e5c5044ff3
15 changed files with 1137 additions and 350 deletions

View File

@ -153,7 +153,7 @@ export const appRoutes: Route[] = [
{
path: 'role-management',
canActivate: [RoleGuard],
data: { roles: ['Management', 'SuperAdmin'] },
data: { roles: ['Management', 'SuperAdmin', 'Admin'] },
loadChildren: () =>import('app/modules/admin/dashboard/role-management/role-management.routes')
},

View File

@ -1,162 +1,305 @@
<div class="bg-card flex min-w-0 flex-auto flex-col dark:bg-transparent sm:absolute sm:inset-0 sm:overflow-hidden">
<div class="flex min-w-0 flex-auto flex-col h-full">
<div class="flex-auto p-6 sm:p-10 overflow-auto max-h-[calc(100vh-100px)]">
<div *ngIf="errorMessage" class="text-red-500 text-center mb-4">
{{ errorMessage }}
<div class="flex-auto p-6 sm:p-10 overflow-auto">
<!-- Error Message -->
<div *ngIf="errorMessage" class="bg-red-50 border-l-4 border-red-500 text-red-700 px-4 py-3 rounded mb-6">
<div class="flex items-center justify-between">
<span>{{ errorMessage }}</span>
<button (click)="retryFetch()" class="text-red-700 underline hover:text-red-900">Retry</button>
</div>
</div>
<div class="grid w-full min-w-0 grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
<!-- Machines -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full h-[150px]">
<div class="flex items-start justify-between">
<div class="truncate text-lg font-medium leading-6 tracking-tight">
{{ machineTitle }}
</div>
<div class="-mr-3 -mt-2 ml-2">
<button mat-icon-button [matMenuTriggerFor]="summaryMenu">
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_mini:ellipsis-vertical'"></mat-icon>
<!-- Top Metrics Row -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Machines Card with Menu -->
<div class="bg-blue-600 text-white rounded-lg p-5 shadow relative">
<div class="flex items-start justify-between mb-3">
<div class="text-sm font-medium">{{ machineTitle }}</div>
<button mat-icon-button [matMenuTriggerFor]="machineMenu" class="text-white -mt-2 -mr-2">
<mat-icon class="text-white text-xl">more_vert</mat-icon>
</button>
<mat-menu #machineMenu="matMenu">
<button mat-menu-item (click)="updateMachine('all')">
<mat-icon>list</mat-icon>
<span>All Machines</span>
</button>
<mat-menu #summaryMenu="matMenu">
<button mat-menu-item (click)="updateMachine('active')">
Active Machines
</button>
<button mat-menu-item (click)="updateMachine('inactive')">
Inactive Machines
</button>
<button mat-menu-item (click)="updateMachine('all')">
Reset Machines
</button>
</mat-menu>
</div>
</div>
<div class="mt-2 flex flex-col items-center">
<div class="text-7xl font-bold leading-none tracking-tight text-blue-500 sm:text-8xl">
{{ machineCount }}
</div>
<button mat-menu-item (click)="updateMachine('active')">
<mat-icon>check_circle</mat-icon>
<span>Active Machines</span>
</button>
<button mat-menu-item (click)="updateMachine('inactive')">
<mat-icon>cancel</mat-icon>
<span>Inactive Machines</span>
</button>
</mat-menu>
</div>
<div class="text-5xl font-bold">{{ machineCount }}</div>
</div>
<!-- Clients -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full h-[150px]">
<div class="flex items-start justify-between">
<div class="truncate text-lg font-medium leading-6 tracking-tight">
Clients
<div class="bg-indigo-600 text-white rounded-lg p-5 shadow">
<div class="text-sm font-medium mb-3">Clients</div>
<div class="text-5xl font-bold">{{ clients }}</div>
</div>
<!-- Active Machines -->
<div class="bg-cyan-500 text-white rounded-lg p-5 shadow">
<div class="text-sm font-medium mb-3">Active Machines</div>
<div class="text-5xl font-bold">{{ activeMachines }}</div>
</div>
<!-- Inactive Machines -->
<div class="bg-purple-600 text-white rounded-lg p-5 shadow">
<div class="text-sm font-medium mb-3">Inactive Machines</div>
<div class="text-5xl font-bold">{{ inactiveMachines }}</div>
</div>
</div>
<!-- Payment and Bottom Metrics Row -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
<!-- Payment Overview - Takes 1 column -->
<div class="bg-orange-500 text-white rounded-lg p-5 shadow">
<div class="text-sm font-semibold mb-4 uppercase tracking-wide">Payment Overview</div>
<div class="space-y-2 mb-4">
<div class="flex justify-between items-center text-sm">
<span>Cash</span>
<span class="font-semibold">₹{{ paymentCash | number:'1.0-0' }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span>Cashless</span>
<span class="font-semibold">₹{{ paymentCashless | number:'1.0-0' }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-xs">UPI/Card (PhonePe)</span>
<span class="font-semibold">₹{{ paymentUPIWalletCard | number:'1.0-0' }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-xs">UPI/Wallet (Paytm)</span>
<span class="font-semibold">₹{{ paymentUPIWalletPaytm | number:'1.0-0' }}</span>
</div>
</div>
<div class="mt-2 flex flex-col items-center">
<div class="text-7xl font-bold leading-none tracking-tight text-red-500 sm:text-8xl">
{{ clients }}
<div class="border-t border-white/30 pt-3 space-y-1">
<div class="flex justify-between text-xs">
<span>Refund</span>
<span>₹{{ refund | number:'1.2-2' }}</span>
</div>
<div class="flex justify-between text-xs">
<span>Refund Processed</span>
<span>₹{{ refundProcessed | number:'1.2-2' }}</span>
</div>
<div class="flex justify-between text-base font-bold pt-2 border-t border-white/30">
<span>Total</span>
<span>₹{{ paymentTotal | number:'1.0-0' }}</span>
</div>
</div>
</div>
<!-- Company Users -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full h-[150px]">
<div class="flex items-start justify-between">
<div class="truncate text-lg font-medium leading-6 tracking-tight">
Company Users
</div>
<!-- Other Metrics - Takes 2 columns -->
<div class="lg:col-span-2 grid grid-cols-2 sm:grid-cols-4 gap-4">
<!-- Company Users -->
<div class="bg-teal-600 text-white rounded-lg p-5 shadow">
<div class="text-sm font-medium mb-3">Company Users</div>
<div class="text-5xl font-bold">{{ companyUsers }}</div>
</div>
<div class="mt-2 flex flex-col items-center">
<div class="text-7xl font-bold leading-none tracking-tight text-amber-500 sm:text-8xl">
{{ companyUsers }}
<!-- Client Users -->
<div class="bg-slate-600 text-white rounded-lg p-5 shadow">
<div class="text-sm font-medium mb-3">Client Users</div>
<div class="text-5xl font-bold">{{ clientUsers }}</div>
</div>
<!-- Transactions -->
<div class="bg-green-700 text-white rounded-lg p-5 shadow">
<div class="text-sm font-medium mb-3">Transactions</div>
<div class="text-5xl font-bold">{{ transactions | number }}</div>
</div>
<!-- Sales -->
<div class="bg-green-600 text-white rounded-lg p-5 shadow">
<div class="text-sm font-medium mb-3">Total Sales</div>
<div class="text-4xl font-bold">{{ sales }}</div>
</div>
</div>
</div>
<!-- Machine Status Panels -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<!-- Machine Operation Status -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold text-gray-900">Machine Operation Status</h3>
<mat-icon class="text-blue-600 cursor-pointer">filter_list</mat-icon>
</div>
<div class="grid grid-cols-2 gap-x-6 gap-y-3">
<div *ngFor="let key of getOperationStatusKeys()" class="flex justify-between items-center">
<span class="text-sm text-gray-700">{{ key }}</span>
<span class="text-lg font-semibold text-gray-900">{{ machineOperationStatus[key] }}</span>
</div>
</div>
</div>
<!-- Client User -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full h-[150px]">
<div class="flex items-start justify-between">
<div class="truncate text-lg font-medium leading-6 tracking-tight">
Client User
</div>
<!-- Machine Stock Status -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold text-gray-900">Machine Stock Status</h3>
<mat-icon class="text-blue-600 cursor-pointer">filter_list</mat-icon>
</div>
<div class="mt-2 flex flex-col items-center">
<div class="text-7xl font-bold leading-none tracking-tight text-red-500 sm:text-8xl">
{{ clientUsers }}
</div>
</div>
</div>
<!-- Transactions -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full h-[150px]">
<div class="flex items-start justify-between">
<div class="truncate text-lg font-medium leading-6 tracking-tight">
Transactions
</div>
</div>
<div class="mt-2 flex flex-col items-center">
<div class="text-7xl font-bold leading-none tracking-tight text-red-500 sm:text-8xl">
{{ transactions }}
</div>
</div>
</div>
<!-- Sales -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full h-[150px]">
<div class="flex items-start justify-between">
<div class="truncate text-lg font-medium leading-6 tracking-tight">
Sales
</div>
</div>
<div class="mt-2 flex flex-col items-center">
<div class="text-7xl font-bold leading-none tracking-tight text-green-500 sm:text-8xl">
{{ sales }}
<div class="grid grid-cols-5 gap-2">
<div *ngFor="let key of getStockStatusKeys()"
class="text-center p-3 rounded-lg"
[ngClass]="{
'bg-green-100 text-green-800': key === '75 - 100%',
'bg-yellow-100 text-yellow-800': key === '50 - 75%',
'bg-orange-100 text-orange-800': key === '25 - 50%',
'bg-red-100 text-red-800': key === '0 - 25%',
'bg-pink-100 text-pink-800': key === 'N/A'
}">
<div class="text-[10px] font-medium mb-1">{{ key }}</div>
<div class="text-2xl font-bold">{{ machineStockStatus[key] }}</div>
</div>
</div>
</div>
</div>
<!-- Chart Visualizations -->
<div class="grid w-full min-w-0 grid-cols-1 gap-6 sm:grid-cols-1 md:grid-cols-2 mt-6">
<!-- Bar Chart for Key Metrics -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full">
<div class="text-lg font-medium leading-6 tracking-tight mb-4">
Key Metrics Overview
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<!-- Product Sales Chart -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold text-gray-900">Product Sales</h3>
<button mat-stroked-button [matMenuTriggerFor]="productSalesMenu" class="text-sm border border-blue-300 text-blue-700 rounded-lg px-4 py-2 hover:bg-blue-50 flex items-center gap-2">
<mat-icon class="text-lg">schedule</mat-icon>
<span class="capitalize font-medium">{{ productSalesTimeRange }}</span>
<mat-icon class="text-lg">expand_more</mat-icon>
</button>
<mat-menu #productSalesMenu="matMenu">
<button mat-menu-item (click)="updateProductSalesTimeRange('day')">
<mat-icon>today</mat-icon>
<span>Daily</span>
</button>
<button mat-menu-item (click)="updateProductSalesTimeRange('week')">
<mat-icon>date_range</mat-icon>
<span>Weekly</span>
</button>
<button mat-menu-item (click)="updateProductSalesTimeRange('month')">
<mat-icon>calendar_month</mat-icon>
<span>Monthly</span>
</button>
<button mat-menu-item (click)="updateProductSalesTimeRange('year')">
<mat-icon>calendar_today</mat-icon>
<span>Yearly</span>
</button>
</mat-menu>
</div>
<apx-chart
[series]="barChartOptions.series"
[chart]="barChartOptions.chart"
[xaxis]="barChartOptions.xaxis"
[colors]="barChartOptions.colors"
[plotOptions]="barChartOptions.plotOptions"
[dataLabels]="barChartOptions.dataLabels"
[tooltip]="barChartOptions.tooltip"
[series]="productSalesChartOptions.series"
[chart]="productSalesChartOptions.chart"
[xaxis]="productSalesChartOptions.xaxis"
[yaxis]="productSalesChartOptions.yaxis"
[colors]="productSalesChartOptions.colors"
[plotOptions]="productSalesChartOptions.plotOptions"
[dataLabels]="productSalesChartOptions.dataLabels"
[fill]="productSalesChartOptions.fill"
[legend]="productSalesChartOptions.legend"
[grid]="productSalesChartOptions.grid"
></apx-chart>
</div>
<!-- Line Chart for Trends -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full">
<div class="text-lg font-medium leading-6 tracking-tight mb-4">
Transaction Trends
<!-- Top Selling Products -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold text-gray-900">Top Selling Products</h3>
<button mat-stroked-button [matMenuTriggerFor]="topProductsMenu" class="text-sm border border-purple-300 text-purple-700 rounded-lg px-4 py-2 hover:bg-purple-50 flex items-center gap-2">
<mat-icon class="text-lg">schedule</mat-icon>
<span class="capitalize font-medium">{{ topProductsTimeRange }}</span>
<mat-icon class="text-lg">expand_more</mat-icon>
</button>
<mat-menu #topProductsMenu="matMenu">
<button mat-menu-item (click)="updateTopProductsTimeRange('day')">
<mat-icon>today</mat-icon>
<span>Daily</span>
</button>
<button mat-menu-item (click)="updateTopProductsTimeRange('week')">
<mat-icon>date_range</mat-icon>
<span>Weekly</span>
</button>
<button mat-menu-item (click)="updateTopProductsTimeRange('month')">
<mat-icon>calendar_month</mat-icon>
<span>Monthly</span>
</button>
<button mat-menu-item (click)="updateTopProductsTimeRange('year')">
<mat-icon>calendar_today</mat-icon>
<span>Yearly</span>
</button>
</mat-menu>
</div>
<apx-chart
[series]="lineChartOptions.series"
[chart]="lineChartOptions.chart"
[xaxis]="lineChartOptions.xaxis"
[yaxis]="lineChartOptions.yaxis"
[colors]="lineChartOptions.colors"
[stroke]="lineChartOptions.stroke"
[tooltip]="lineChartOptions.tooltip"
></apx-chart>
</div>
<!-- Doughnut Chart for Sales Distribution -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full">
<div class="text-lg font-medium leading-6 tracking-tight mb-4">
Sales Distribution
<div class="overflow-auto max-h-[320px]">
<table class="w-full">
<thead class="bg-teal-600 text-white sticky top-0">
<tr>
<th class="text-left p-3 text-xs font-semibold">Product</th>
<th class="text-right p-3 text-xs font-semibold">Quantity</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let product of topSellingProducts; let i = index"
class="border-b border-gray-100 hover:bg-gray-50 transition-colors"
[ngClass]="{'bg-gray-50': i % 2 === 0}">
<td class="p-3 text-sm text-gray-700">{{ product.product_name }}</td>
<td class="p-3 text-sm text-right font-semibold text-gray-900">{{ product.quantity | number }}</td>
</tr>
<tr *ngIf="topSellingProducts.length === 0">
<td colspan="2" class="p-6 text-center text-gray-500">
<mat-icon class="text-4xl text-gray-300 mb-2">inbox</mat-icon>
<div class="text-sm">No data available</div>
</td>
</tr>
</tbody>
</table>
</div>
<apx-chart
[series]="doughnutChartOptions.series"
[chart]="doughnutChartOptions.chart"
[labels]="doughnutChartOptions.labels"
[colors]="doughnutChartOptions.colors"
[responsive]="doughnutChartOptions.responsive"
></apx-chart>
</div>
<!-- Pie Chart for Client User Categories -->
<div class="bg-card flex flex-grow flex-col overflow-hidden rounded-2xl p-6 shadow w-full">
<div class="text-lg font-medium leading-6 tracking-tight mb-4">
Client User Categories
</div>
<apx-chart
[series]="pieChartOptions.series"
[chart]="pieChartOptions.chart"
[labels]="pieChartOptions.labels"
[colors]="pieChartOptions.colors"
[responsive]="pieChartOptions.responsive"
></apx-chart>
</div>
<!-- Sales Area Chart -->
<div class="bg-white rounded-lg shadow p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold text-gray-900">Sales Trend</h3>
<button mat-stroked-button [matMenuTriggerFor]="salesMenu" class="text-sm border border-green-300 text-green-700 rounded-lg px-4 py-2 hover:bg-green-50 flex items-center gap-2">
<mat-icon class="text-lg">schedule</mat-icon>
<span class="capitalize font-medium">{{ salesTimeRange }}</span>
<mat-icon class="text-lg">expand_more</mat-icon>
</button>
<mat-menu #salesMenu="matMenu">
<button mat-menu-item (click)="updateSalesTimeRange('day')">
<mat-icon>today</mat-icon>
<span>Daily</span>
</button>
<button mat-menu-item (click)="updateSalesTimeRange('week')">
<mat-icon>date_range</mat-icon>
<span>Weekly</span>
</button>
<button mat-menu-item (click)="updateSalesTimeRange('month')">
<mat-icon>calendar_month</mat-icon>
<span>Monthly</span>
</button>
<button mat-menu-item (click)="updateSalesTimeRange('year')">
<mat-icon>calendar_today</mat-icon>
<span>Yearly</span>
</button>
</mat-menu>
</div>
<apx-chart
[series]="salesChartOptions.series"
[chart]="salesChartOptions.chart"
[xaxis]="salesChartOptions.xaxis"
[yaxis]="salesChartOptions.yaxis"
[colors]="salesChartOptions.colors"
[stroke]="salesChartOptions.stroke"
[dataLabels]="salesChartOptions.dataLabels"
[fill]="salesChartOptions.fill"
[grid]="salesChartOptions.grid"
></apx-chart>
</div>
</div>
</div>
</div>

View File

@ -23,10 +23,23 @@ interface DashboardMetrics {
client_users: number;
transactions: number;
sales: string;
active_machines?: number;
inactive_machines?: number;
machine_operation_status?: { [key: string]: number };
machine_stock_status?: { [key: string]: number };
payment_breakdown?: {
cash: number;
cashless: number;
upi_wallet_card: number;
upi_wallet_paytm: number;
refund: number;
refund_processed: number;
total: number;
};
product_sales_yearly?: { year: string; amount: number }[];
sales_yearly?: { year: string; amount: number }[];
top_selling_products?: { product_name: string; quantity: number }[];
error?: string;
transaction_trends?: { date: string; transactions: number }[];
sales_distribution?: { [key: string]: number };
client_user_categories?: { [key: string]: number };
}
@Component({
@ -56,112 +69,41 @@ export class ExampleComponent implements OnInit {
clientUsers: number = 0;
transactions: number = 0;
sales: string = '₹0.00';
activeMachines: number = 0;
inactiveMachines: number = 0;
errorMessage: string = '';
loading: boolean = false;
barChartOptions: Partial<ApexOptions>;
lineChartOptions: Partial<ApexOptions>;
doughnutChartOptions: Partial<ApexOptions>;
pieChartOptions: Partial<ApexOptions>;
// Payment breakdown
paymentCash: number = 0;
paymentCashless: number = 0;
paymentUPIWalletCard: number = 0;
paymentUPIWalletPaytm: number = 0;
refund: number = 0;
refundProcessed: number = 0;
paymentTotal: number = 0;
// Machine operation status
machineOperationStatus: { [key: string]: number } = {};
// Machine stock status
machineStockStatus: { [key: string]: number } = {};
// Top selling products
topSellingProducts: { product_name: string; quantity: number }[] = [];
// Filter states
productSalesTimeRange: string = 'year';
salesTimeRange: string = 'year';
topProductsTimeRange: string = 'year';
productSalesChartOptions: Partial<ApexOptions>;
salesChartOptions: Partial<ApexOptions>;
private readonly baseUrl = environment.apiUrl || 'http://localhost:5000';
constructor(private http: HttpClient) {
// Initialize bar chart options with mock data
this.barChartOptions = {
series: [{
name: 'Metrics',
data: [150, 80, 120, 60]
}],
chart: {
type: 'bar',
height: 350
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%'
}
},
dataLabels: {
enabled: false
},
xaxis: {
categories: ['Machines', 'Clients', 'Company Users', 'Client Users']
},
colors: ['#3B82F6', '#EF4444', '#F59E0B', '#EF4444'],
tooltip: {
y: {
formatter: (val) => `${val}`
}
}
};
// Initialize line chart options with mock data
this.lineChartOptions = {
series: [{
name: 'Transactions',
data: [10, 15, 8, 12, 20]
}],
chart: {
type: 'line',
height: 350
},
stroke: {
width: 3,
curve: 'smooth'
},
xaxis: {
type: 'category',
categories: ['2025-08-15', '2025-08-16', '2025-08-17', '2025-08-18', '2025-08-19']
},
yaxis: {
title: {
text: 'Transactions'
}
},
colors: ['#EF4444'],
tooltip: {
x: {
format: 'dd MMM yyyy'
}
}
};
// Initialize doughnut chart options with mock data
this.doughnutChartOptions = {
series: [40, 30, 20],
chart: {
type: 'donut',
height: 350
},
labels: ['Product A', 'Product B', 'Product C'],
colors: ['#10B981', '#F59E0B', '#8B5CF6'],
responsive: [{
breakpoint: 480,
options: {
chart: { width: 200 },
legend: { position: 'bottom' }
}
}]
};
// Initialize pie chart options with mock data
this.pieChartOptions = {
series: [50, 30, 20],
chart: {
type: 'pie',
height: 350
},
labels: ['Premium', 'Standard', 'Basic'],
colors: ['#EF4444', '#6B7280', '#34D399'],
responsive: [{
breakpoint: 480,
options: {
chart: { width: 200 },
legend: { position: 'bottom' }
}
}]
};
this.initializeCharts();
}
ngOnInit() {
@ -169,35 +111,259 @@ export class ExampleComponent implements OnInit {
}
updateMachine(filter: 'active' | 'inactive' | 'all') {
console.log('Filter clicked:', filter);
this.fetchDashboardMetrics(filter);
}
updateProductSalesTimeRange(range: string) {
this.productSalesTimeRange = range;
console.log('Product Sales Time Range:', range);
this.fetchProductSalesData(range);
}
updateSalesTimeRange(range: string) {
this.salesTimeRange = range;
console.log('Sales Time Range:', range);
this.fetchSalesData(range);
}
updateTopProductsTimeRange(range: string) {
this.topProductsTimeRange = range;
console.log('Top Products Time Range:', range);
this.fetchTopProductsData(range);
}
private fetchProductSalesData(timeRange: string) {
const url = `${this.baseUrl}/product-sales?time_range=${timeRange}`;
console.log('Fetching product sales data:', timeRange);
const token = localStorage.getItem('token') || localStorage.getItem('access_token');
this.http.get<{ product_sales: { year: string; amount: number }[] }>(url, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
}
}).subscribe({
next: (response) => {
console.log('✓ Product Sales Data:', response);
if (response.product_sales && response.product_sales.length > 0) {
this.productSalesChartOptions = {
...this.productSalesChartOptions,
series: [{
name: 'Sales',
data: response.product_sales.map(item => item.amount)
}],
xaxis: {
...this.productSalesChartOptions.xaxis,
categories: response.product_sales.map(item => item.year)
}
};
}
},
error: (error) => {
console.error('✗ Error fetching product sales data:', error);
}
});
}
private fetchSalesData(timeRange: string) {
const url = `${this.baseUrl}/sales-data?time_range=${timeRange}`;
console.log('Fetching sales data:', timeRange);
const token = localStorage.getItem('token') || localStorage.getItem('access_token');
this.http.get<{ sales_data: { year: string; amount: number }[] }>(url, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
}
}).subscribe({
next: (response) => {
console.log('✓ Sales Data:', response);
if (response.sales_data && response.sales_data.length > 0) {
this.salesChartOptions = {
...this.salesChartOptions,
series: [{
name: 'Sales',
data: response.sales_data.map(item => item.amount)
}],
xaxis: {
...this.salesChartOptions.xaxis,
categories: response.sales_data.map(item => item.year)
}
};
}
},
error: (error) => {
console.error('✗ Error fetching sales data:', error);
}
});
}
private fetchTopProductsData(timeRange: string) {
const url = `${this.baseUrl}/top-products?time_range=${timeRange}`;
console.log('Fetching top products data:', timeRange);
const token = localStorage.getItem('token') || localStorage.getItem('access_token');
this.http.get<{ top_products: { product_name: string; quantity: number }[] }>(url, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
}
}).subscribe({
next: (response) => {
console.log('✓ Top Products Data:', response);
if (response.top_products) {
this.topSellingProducts = response.top_products;
}
},
error: (error) => {
console.error('✗ Error fetching top products data:', error);
}
});
}
private initializeCharts() {
// Product Sales Chart
this.productSalesChartOptions = {
series: [{
name: 'Sales',
data: []
}],
chart: {
type: 'bar',
height: 350,
stacked: true,
toolbar: {
show: false
}
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '80%',
}
},
dataLabels: {
enabled: false
},
xaxis: {
categories: [],
labels: {
style: {
fontSize: '12px'
}
}
},
yaxis: {
title: {
text: 'Rupees'
},
labels: {
formatter: (val) => `${(val / 1000000).toFixed(1)}M`
}
},
colors: ['#3B82F6', '#EF4444', '#F59E0B', '#10B981', '#8B5CF6'],
fill: {
opacity: 1
},
legend: {
show: false
},
grid: {
borderColor: '#f1f1f1'
}
};
// Sales Chart (Area)
this.salesChartOptions = {
series: [{
name: 'Sales',
data: []
}],
chart: {
type: 'area',
height: 350,
toolbar: {
show: false
}
},
dataLabels: {
enabled: false
},
stroke: {
curve: 'smooth',
width: 2
},
xaxis: {
categories: [],
labels: {
style: {
fontSize: '12px'
}
}
},
yaxis: {
title: {
text: 'Rupees'
},
labels: {
formatter: (val) => `${(val / 1000000).toFixed(1)}M`
}
},
colors: ['#14B8A6'],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.3,
stops: [0, 90, 100]
}
},
grid: {
borderColor: '#f1f1f1'
}
};
}
private fetchDashboardMetrics(filter: string) {
this.loading = true;
this.machineTitle = 'Loading...';
this.errorMessage = '';
const url = `${this.baseUrl}/dashboard-metrics?machine_filter=${filter}`;
console.log('Fetching from URL:', url);
console.log('Fetching dashboard data with filter:', filter);
console.log('URL:', url);
// Get token from localStorage
const token = localStorage.getItem('token') || localStorage.getItem('access_token');
this.http.get<DashboardMetrics>(url, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
}
}).subscribe({
next: (response) => {
console.log('API Response:', response);
console.log('✓ Dashboard API Response:', response);
this.loading = false;
if (response.error) {
this.handleError(`Server Error: ${response.error}`);
} else {
this.updateDashboardData(response);
console.log('✓ Dashboard data updated successfully');
}
},
error: (error: HttpErrorResponse) => {
console.error('HTTP Error:', error);
console.error('✗ Dashboard HTTP Error:', error);
this.loading = false;
this.handleHttpError(error);
}
@ -205,6 +371,7 @@ export class ExampleComponent implements OnInit {
}
private updateDashboardData(response: DashboardMetrics) {
// Update basic metrics
this.machineTitle = response.machine_title || 'Machines';
this.machineCount = response.machine_count || 0;
this.clients = response.clients || 0;
@ -212,54 +379,73 @@ export class ExampleComponent implements OnInit {
this.clientUsers = response.client_users || 0;
this.transactions = response.transactions || 0;
this.sales = `${response.sales || '0.00'}`;
this.activeMachines = response.active_machines || 0;
this.inactiveMachines = response.inactive_machines || 0;
this.errorMessage = '';
// Update bar chart
this.barChartOptions = {
...this.barChartOptions,
series: [{
name: 'Metrics',
data: [
this.machineCount,
this.clients,
this.companyUsers,
this.clientUsers
]
}]
};
// Update payment breakdown
if (response.payment_breakdown) {
this.paymentCash = response.payment_breakdown.cash || 0;
this.paymentCashless = response.payment_breakdown.cashless || 0;
this.paymentUPIWalletCard = response.payment_breakdown.upi_wallet_card || 0;
this.paymentUPIWalletPaytm = response.payment_breakdown.upi_wallet_paytm || 0;
this.refund = response.payment_breakdown.refund || 0;
this.refundProcessed = response.payment_breakdown.refund_processed || 0;
this.paymentTotal = response.payment_breakdown.total || 0;
}
// Update line chart with transaction trends
if (response.transaction_trends) {
this.lineChartOptions = {
...this.lineChartOptions,
// Update machine operation status
if (response.machine_operation_status) {
this.machineOperationStatus = response.machine_operation_status;
}
// Update machine stock status
if (response.machine_stock_status) {
this.machineStockStatus = response.machine_stock_status;
}
// Update top selling products
if (response.top_selling_products) {
this.topSellingProducts = response.top_selling_products;
}
// Update Product Sales Chart
if (response.product_sales_yearly && response.product_sales_yearly.length > 0) {
this.productSalesChartOptions = {
...this.productSalesChartOptions,
series: [{
name: 'Transactions',
data: response.transaction_trends.map(item => item.transactions)
name: 'Sales',
data: response.product_sales_yearly.map(item => item.amount)
}],
xaxis: {
...this.lineChartOptions.xaxis,
categories: response.transaction_trends.map(item => item.date)
...this.productSalesChartOptions.xaxis,
categories: response.product_sales_yearly.map(item => item.year)
}
};
}
// Update doughnut chart with sales distribution
if (response.sales_distribution) {
this.doughnutChartOptions = {
...this.doughnutChartOptions,
series: Object.values(response.sales_distribution),
labels: Object.keys(response.sales_distribution)
// Update Sales Chart
if (response.sales_yearly && response.sales_yearly.length > 0) {
this.salesChartOptions = {
...this.salesChartOptions,
series: [{
name: 'Sales',
data: response.sales_yearly.map(item => item.amount)
}],
xaxis: {
...this.salesChartOptions.xaxis,
categories: response.sales_yearly.map(item => item.year)
}
};
}
}
// Update pie chart with client user categories
if (response.client_user_categories) {
this.pieChartOptions = {
...this.pieChartOptions,
series: Object.values(response.client_user_categories),
labels: Object.keys(response.client_user_categories)
};
}
getStockStatusKeys(): string[] {
return Object.keys(this.machineStockStatus).sort();
}
getOperationStatusKeys(): string[] {
return Object.keys(this.machineOperationStatus).sort();
}
private handleError(message: string) {
@ -272,18 +458,15 @@ export class ExampleComponent implements OnInit {
if (error.status === 0) {
errorMessage += 'Cannot connect to server. Please check if the Flask server is running on the correct port.';
} else if (error.status === 401) {
errorMessage += 'Authentication required. Please login again.';
} else if (error.status === 404) {
errorMessage += 'API endpoint not found. Please check the server routing.';
} else if (error.status >= 500) {
errorMessage += 'Internal server error. Please check the server logs.';
} else {
errorMessage += `Status: ${error.status} - ${error.statusText}. `;
if (error.error && typeof error.error === 'string' && error.error.includes('<!doctype')) {
errorMessage += 'Server returned HTML instead of JSON. Check your proxy configuration.';
} else {
errorMessage += `Details: ${error.error?.error || error.message || 'Unknown error'}`;
}
errorMessage += `Details: ${error.error?.error || error.message || 'Unknown error'}`;
}
this.handleError(errorMessage);
@ -297,39 +480,18 @@ export class ExampleComponent implements OnInit {
this.clientUsers = 0;
this.transactions = 0;
this.sales = '₹0.00';
// Reset chart data
this.barChartOptions = {
...this.barChartOptions,
series: [{
name: 'Metrics',
data: [0, 0, 0, 0]
}]
};
this.lineChartOptions = {
...this.lineChartOptions,
series: [{
name: 'Transactions',
data: []
}],
xaxis: {
...this.lineChartOptions.xaxis,
categories: []
}
};
this.doughnutChartOptions = {
...this.doughnutChartOptions,
series: [],
labels: []
};
this.pieChartOptions = {
...this.pieChartOptions,
series: [],
labels: []
};
this.activeMachines = 0;
this.inactiveMachines = 0;
this.paymentCash = 0;
this.paymentCashless = 0;
this.paymentUPIWalletCard = 0;
this.paymentUPIWalletPaytm = 0;
this.refund = 0;
this.refundProcessed = 0;
this.paymentTotal = 0;
this.machineOperationStatus = {};
this.machineStockStatus = {};
this.topSellingProducts = [];
}
retryFetch() {

View File

@ -29,14 +29,14 @@
<body class="mat-typography">
<!-- Splash screen -->
<fuse-splash-screen>
<!-- <fuse-splash-screen>
<img src="images/logo/logo.svg" alt="Fuse logo" />
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</fuse-splash-screen>
</fuse-splash-screen> -->
<!-- App root -->
<app-root></app-root>