500 lines
17 KiB
TypeScript
500 lines
17 KiB
TypeScript
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
|
|
import { CurrencyPipe, NgClass } from '@angular/common';
|
|
import { RouterModule } from '@angular/router';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
|
import { MatRippleModule } from '@angular/material/core';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { MatMenuModule } from '@angular/material/menu';
|
|
import { MatTableModule } from '@angular/material/table';
|
|
import { MatTabsModule } from '@angular/material/tabs';
|
|
import { TranslocoModule } from '@ngneat/transloco';
|
|
import { NgApexchartsModule } from 'ng-apexcharts';
|
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
import { CommonModule } from '@angular/common';
|
|
import { environment } from '../../../../environments/environment';
|
|
import { ApexOptions } from 'ng-apexcharts';
|
|
|
|
interface DashboardMetrics {
|
|
machine_title: string;
|
|
machine_count: number;
|
|
clients: number;
|
|
company_users: number;
|
|
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;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'dashboard-example',
|
|
standalone: true,
|
|
templateUrl: './example.component.html',
|
|
encapsulation: ViewEncapsulation.None,
|
|
imports: [
|
|
RouterModule,
|
|
CommonModule,
|
|
TranslocoModule,
|
|
MatIconModule,
|
|
MatButtonModule,
|
|
MatRippleModule,
|
|
MatMenuModule,
|
|
MatTabsModule,
|
|
MatButtonToggleModule,
|
|
NgApexchartsModule,
|
|
MatTableModule,
|
|
]
|
|
})
|
|
export class ExampleComponent implements OnInit {
|
|
machineTitle: string = 'Loading...';
|
|
machineCount: number = 0;
|
|
clients: number = 0;
|
|
companyUsers: number = 0;
|
|
clientUsers: number = 0;
|
|
transactions: number = 0;
|
|
sales: string = '₹0.00';
|
|
activeMachines: number = 0;
|
|
inactiveMachines: number = 0;
|
|
errorMessage: string = '';
|
|
loading: boolean = false;
|
|
|
|
// 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) {
|
|
this.initializeCharts();
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.fetchDashboardMetrics('all');
|
|
}
|
|
|
|
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 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',
|
|
'Authorization': token ? `Bearer ${token}` : ''
|
|
}
|
|
}).subscribe({
|
|
next: (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('✗ Dashboard HTTP Error:', error);
|
|
this.loading = false;
|
|
this.handleHttpError(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
private updateDashboardData(response: DashboardMetrics) {
|
|
// Update basic metrics
|
|
this.machineTitle = response.machine_title || 'Machines';
|
|
this.machineCount = response.machine_count || 0;
|
|
this.clients = response.clients || 0;
|
|
this.companyUsers = response.company_users || 0;
|
|
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 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 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: 'Sales',
|
|
data: response.product_sales_yearly.map(item => item.amount)
|
|
}],
|
|
xaxis: {
|
|
...this.productSalesChartOptions.xaxis,
|
|
categories: response.product_sales_yearly.map(item => item.year)
|
|
}
|
|
};
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
getStockStatusKeys(): string[] {
|
|
return Object.keys(this.machineStockStatus).sort();
|
|
}
|
|
|
|
getOperationStatusKeys(): string[] {
|
|
return Object.keys(this.machineOperationStatus).sort();
|
|
}
|
|
|
|
private handleError(message: string) {
|
|
this.errorMessage = message;
|
|
this.resetDashboardData();
|
|
}
|
|
|
|
private handleHttpError(error: HttpErrorResponse) {
|
|
let errorMessage = 'Failed to load data. ';
|
|
|
|
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}. `;
|
|
errorMessage += `Details: ${error.error?.error || error.message || 'Unknown error'}`;
|
|
}
|
|
|
|
this.handleError(errorMessage);
|
|
}
|
|
|
|
private resetDashboardData() {
|
|
this.machineTitle = 'Error';
|
|
this.machineCount = 0;
|
|
this.clients = 0;
|
|
this.companyUsers = 0;
|
|
this.clientUsers = 0;
|
|
this.transactions = 0;
|
|
this.sales = '₹0.00';
|
|
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() {
|
|
this.fetchDashboardMetrics('all');
|
|
}
|
|
} |