diff --git a/Machine-Backend/.env b/Machine-Backend/.env index 55c51c2..0608171 100644 --- a/Machine-Backend/.env +++ b/Machine-Backend/.env @@ -6,14 +6,14 @@ PAYU_MERCHANT_SALT=DusXSSjqqSMTPSpw32hlFXF6LKY2Zm3y PAYU_SUCCESS_URL=http://localhost:4200/payment-success PAYU_FAILURE_URL=http://localhost:4200/payment-failure -FLASK_ENV=production +FLASK_ENV=development MYSQL_HOST=db MYSQL_USER=vendinguser MYSQL_PASSWORD=vendingpass MYSQL_DATABASE=vending -SQLITE_DB_PATH=machines +SQLITE_DB_PATH=machines.db BREVO_SMTP_EMAIL=smukeshsn2000@gmail.com BREVO_SMTP_KEY=your-brevo-smtp-key diff --git a/Machine-Backend/app/instance/machines.db b/Machine-Backend/app/instance/machines.db index 803bd6c..eef373b 100644 Binary files a/Machine-Backend/app/instance/machines.db and b/Machine-Backend/app/instance/machines.db differ diff --git a/Machine-Backend/app/routes/__pycache__/routes.cpython-313.pyc b/Machine-Backend/app/routes/__pycache__/routes.cpython-313.pyc index 348dfce..4707755 100644 Binary files a/Machine-Backend/app/routes/__pycache__/routes.cpython-313.pyc and b/Machine-Backend/app/routes/__pycache__/routes.cpython-313.pyc differ diff --git a/Machine-Backend/app/routes/routes.py b/Machine-Backend/app/routes/routes.py index 517c569..138e3e9 100644 --- a/Machine-Backend/app/routes/routes.py +++ b/Machine-Backend/app/routes/routes.py @@ -1,5 +1,5 @@ from app import db # Add this if not already there -from sqlalchemy import func # Add this +from sqlalchemy import func, extract # Add this from flask import Blueprint, request, jsonify, send_from_directory from app.services.services import MachineService, UserService, ProductService, serial_service, TransactionService, RoleService from app.models.models import Machine, User, Transaction, Product, VendingSlot, Role @@ -853,6 +853,10 @@ def get_current_user_from_token(): return None +from flask import jsonify, request +from sqlalchemy import func, extract +from datetime import datetime + @bp.route('/dashboard-metrics', methods=['GET']) def get_dashboard_metrics_role_based(): """Get dashboard metrics filtered by user role""" @@ -877,16 +881,12 @@ def get_dashboard_metrics_role_based(): # ROLE-BASED FILTERING if user_role in ['Management', 'SuperAdmin', 'Admin']: - # Show overall system stats response = get_admin_dashboard_metrics(filter_type) elif user_role == 'Client': - # Show only this client's data response = get_Client_dashboard_metrics(current_user.id, filter_type) elif user_role == 'Refiller': - # Show product/warehouse related stats response = get_refiller_dashboard_metrics(filter_type) elif user_role == 'Servicer': - # Show machine maintenance stats response = get_servicer_dashboard_metrics(filter_type) else: return jsonify({'error': 'Invalid role'}), 403 @@ -914,23 +914,52 @@ def get_admin_dashboard_metrics(filter_type): machine_count = Machine.query.count() machine_title = 'All Machines' - # Client count (users with role 'Client') + # Client count clients = User.query.filter_by(roles='Client').count() - # Company users (Admin, SuperAdmin, Management, Refiller, Servicer) + # Company users company_users = User.query.filter( User.roles.in_(['Management', 'SuperAdmin', 'Admin', 'Refiller', 'Servicer']) ).count() + # Active and inactive machines + active_machines = Machine.query.filter_by(operation_status='active').count() + inactive_machines = Machine.query.filter_by(operation_status='inactive').count() + # COUNT TRANSACTIONS AND CALCULATE SALES transactions_count = Transaction.query.count() - # Calculate total sales (sum of all successful transaction amounts) - from sqlalchemy import func + # Calculate total sales total_sales = db.session.query(func.sum(Transaction.amount))\ .filter(Transaction.status == 'Success')\ .scalar() or 0.0 + # PAYMENT BREAKDOWN + payment_breakdown = get_payment_breakdown() + + # MACHINE OPERATION STATUS + machine_operation_status = { + 'Online': Machine.query.filter_by(connection_status='online').count(), + 'Offline': Machine.query.filter_by(connection_status='offline').count(), + 'Down (Planned)': Machine.query.filter_by(operation_status='down_planned').count(), + 'Down': Machine.query.filter_by(operation_status='down').count(), + 'Standby': Machine.query.filter_by(operation_status='standby').count(), + 'Terminated': Machine.query.filter_by(operation_status='terminated').count(), + 'N/A': Machine.query.filter(Machine.operation_status.is_(None)).count() + } + + # MACHINE STOCK STATUS - Calculate based on vending slots + machine_stock_status = calculate_machine_stock_status() + + # PRODUCT SALES YEARLY + product_sales_yearly = get_product_sales_yearly() + + # SALES YEARLY + sales_yearly = get_sales_yearly() + + # TOP SELLING PRODUCTS + top_selling_products = get_top_selling_products() + return { 'machine_title': machine_title, 'machine_count': machine_count, @@ -939,13 +968,21 @@ def get_admin_dashboard_metrics(filter_type): 'client_users': clients, 'transactions': transactions_count, 'sales': f'{total_sales:.2f}', + 'active_machines': active_machines, + 'inactive_machines': inactive_machines, + 'payment_breakdown': payment_breakdown, + 'machine_operation_status': machine_operation_status, + 'machine_stock_status': machine_stock_status, + 'product_sales_yearly': product_sales_yearly, + 'sales_yearly': sales_yearly, + 'top_selling_products': top_selling_products, 'user_role': 'admin', 'user_type': 'Overall System' } def get_Client_dashboard_metrics(user_id, filter_type): - """Get metrics for Client/Client - Only their own data""" + """Get metrics for Client - Only their own data""" # Get only machines belonging to this client if filter_type == 'active': machine_count = Machine.query.filter_by( @@ -963,28 +1000,53 @@ def get_Client_dashboard_metrics(user_id, filter_type): machine_count = Machine.query.filter_by(client_id=user_id).count() machine_title = 'My Machines' - # For Client, clients = 1 (themselves) clients = 1 - - # Company users not relevant for Client company_users = 0 - # GET THIS CLIENT'S TRANSACTIONS AND SALES # Get machine IDs for this client client_machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=user_id).all()] + # Active and inactive machines for this client + active_machines = Machine.query.filter_by(client_id=user_id, operation_status='active').count() + inactive_machines = Machine.query.filter_by(client_id=user_id, operation_status='inactive').count() + # Count transactions for this client's machines transactions_count = Transaction.query.filter( Transaction.machine_id.in_(client_machine_ids) ).count() if client_machine_ids else 0 # Calculate sales for this client's machines - from sqlalchemy import func total_sales = db.session.query(func.sum(Transaction.amount))\ .filter(Transaction.machine_id.in_(client_machine_ids))\ .filter(Transaction.status == 'Success')\ .scalar() or 0.0 if client_machine_ids else 0.0 + # PAYMENT BREAKDOWN for client's machines + payment_breakdown = get_payment_breakdown(client_machine_ids) + + # MACHINE OPERATION STATUS for client's machines + machine_operation_status = { + 'Online': Machine.query.filter(Machine.client_id == user_id, Machine.connection_status == 'online').count(), + 'Offline': Machine.query.filter(Machine.client_id == user_id, Machine.connection_status == 'offline').count(), + 'Down (Planned)': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status == 'down_planned').count(), + 'Down': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status == 'down').count(), + 'Standby': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status == 'standby').count(), + 'Terminated': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status == 'terminated').count(), + 'N/A': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status.is_(None)).count() + } + + # MACHINE STOCK STATUS for client's machines + machine_stock_status = calculate_machine_stock_status(client_machine_ids) + + # PRODUCT SALES YEARLY for client's machines + product_sales_yearly = get_product_sales_yearly(client_machine_ids) + + # SALES YEARLY for client's machines + sales_yearly = get_sales_yearly(client_machine_ids) + + # TOP SELLING PRODUCTS for client's machines + top_selling_products = get_top_selling_products(client_machine_ids) + return { 'machine_title': machine_title, 'machine_count': machine_count, @@ -993,6 +1055,14 @@ def get_Client_dashboard_metrics(user_id, filter_type): 'client_users': clients, 'transactions': transactions_count, 'sales': f'{total_sales:.2f}', + 'active_machines': active_machines, + 'inactive_machines': inactive_machines, + 'payment_breakdown': payment_breakdown, + 'machine_operation_status': machine_operation_status, + 'machine_stock_status': machine_stock_status, + 'product_sales_yearly': product_sales_yearly, + 'sales_yearly': sales_yearly, + 'top_selling_products': top_selling_products, 'user_role': 'Client', 'user_type': 'My Business' } @@ -1000,21 +1070,32 @@ def get_Client_dashboard_metrics(user_id, filter_type): def get_refiller_dashboard_metrics(filter_type): """Get metrics for Refiller - Product/warehouse focused""" - # Machines they need to service machine_count = Machine.query.count() - - # Products in warehouse product_count = Product.query.count() - - # Transaction count (all) transactions_count = Transaction.query.count() - # Total sales - from sqlalchemy import func total_sales = db.session.query(func.sum(Transaction.amount))\ .filter(Transaction.status == 'Success')\ .scalar() or 0.0 + active_machines = Machine.query.filter_by(operation_status='active').count() + inactive_machines = Machine.query.filter_by(operation_status='inactive').count() + + payment_breakdown = get_payment_breakdown() + machine_operation_status = { + 'Online': Machine.query.filter_by(connection_status='online').count(), + 'Offline': Machine.query.filter_by(connection_status='offline').count(), + 'Down (Planned)': Machine.query.filter_by(operation_status='down_planned').count(), + 'Down': Machine.query.filter_by(operation_status='down').count(), + 'Standby': Machine.query.filter_by(operation_status='standby').count(), + 'Terminated': Machine.query.filter_by(operation_status='terminated').count(), + 'N/A': Machine.query.filter(Machine.operation_status.is_(None)).count() + } + machine_stock_status = calculate_machine_stock_status() + product_sales_yearly = get_product_sales_yearly() + sales_yearly = get_sales_yearly() + top_selling_products = get_top_selling_products() + return { 'machine_title': 'Machines to Service', 'machine_count': machine_count, @@ -1023,7 +1104,15 @@ def get_refiller_dashboard_metrics(filter_type): 'client_users': 0, 'transactions': transactions_count, 'sales': f'{total_sales:.2f}', + 'active_machines': active_machines, + 'inactive_machines': inactive_machines, 'product_count': product_count, + 'payment_breakdown': payment_breakdown, + 'machine_operation_status': machine_operation_status, + 'machine_stock_status': machine_stock_status, + 'product_sales_yearly': product_sales_yearly, + 'sales_yearly': sales_yearly, + 'top_selling_products': top_selling_products, 'user_role': 'refiller', 'user_type': 'Inventory Management' } @@ -1031,7 +1120,6 @@ def get_refiller_dashboard_metrics(filter_type): def get_servicer_dashboard_metrics(filter_type): """Get metrics for Servicer - Machine maintenance focused""" - # All machines they can service if filter_type == 'active': machine_count = Machine.query.filter_by(operation_status='active').count() machine_title = 'Active Machines' @@ -1042,18 +1130,31 @@ def get_servicer_dashboard_metrics(filter_type): machine_count = Machine.query.count() machine_title = 'Total Machines' - # Machines needing maintenance maintenance_count = Machine.query.filter_by(operation_status='maintenance').count() - - # Transaction count (all) transactions_count = Transaction.query.count() - # Total sales - from sqlalchemy import func total_sales = db.session.query(func.sum(Transaction.amount))\ .filter(Transaction.status == 'Success')\ .scalar() or 0.0 + active_machines = Machine.query.filter_by(operation_status='active').count() + inactive_machines = Machine.query.filter_by(operation_status='inactive').count() + + payment_breakdown = get_payment_breakdown() + machine_operation_status = { + 'Online': Machine.query.filter_by(connection_status='online').count(), + 'Offline': Machine.query.filter_by(connection_status='offline').count(), + 'Down (Planned)': Machine.query.filter_by(operation_status='down_planned').count(), + 'Down': Machine.query.filter_by(operation_status='down').count(), + 'Standby': Machine.query.filter_by(operation_status='standby').count(), + 'Terminated': Machine.query.filter_by(operation_status='terminated').count(), + 'N/A': Machine.query.filter(Machine.operation_status.is_(None)).count() + } + machine_stock_status = calculate_machine_stock_status() + product_sales_yearly = get_product_sales_yearly() + sales_yearly = get_sales_yearly() + top_selling_products = get_top_selling_products() + return { 'machine_title': machine_title, 'machine_count': machine_count, @@ -1062,11 +1163,374 @@ def get_servicer_dashboard_metrics(filter_type): 'client_users': 0, 'transactions': transactions_count, 'sales': f'{total_sales:.2f}', + 'active_machines': active_machines, + 'inactive_machines': inactive_machines, 'maintenance_count': maintenance_count, + 'payment_breakdown': payment_breakdown, + 'machine_operation_status': machine_operation_status, + 'machine_stock_status': machine_stock_status, + 'product_sales_yearly': product_sales_yearly, + 'sales_yearly': sales_yearly, + 'top_selling_products': top_selling_products, 'user_role': 'servicer', 'user_type': 'Machine Maintenance' } + +# HELPER FUNCTIONS + +def get_payment_breakdown(machine_ids=None): + """Calculate payment breakdown from transactions""" + query = db.session.query( + Transaction.payment_type, + func.sum(Transaction.amount).label('total') + ) + + if machine_ids: + query = query.filter(Transaction.machine_id.in_(machine_ids)) + + query = query.filter(Transaction.status == 'Success')\ + .group_by(Transaction.payment_type) + + payment_data = query.all() + + breakdown = { + 'cash': 0.0, + 'cashless': 0.0, + 'upi_wallet_card': 0.0, + 'upi_wallet_paytm': 0.0, + 'refund': 0.0, + 'refund_processed': 0.0, + 'total': 0.0 + } + + for payment_type, total in payment_data: + if payment_type and total: + payment_type_lower = payment_type.lower() + if 'cash' in payment_type_lower and 'cashless' not in payment_type_lower: + breakdown['cash'] += float(total) + elif 'cashless' in payment_type_lower: + breakdown['cashless'] += float(total) + elif 'phonepe' in payment_type_lower or 'card' in payment_type_lower: + breakdown['upi_wallet_card'] += float(total) + elif 'paytm' in payment_type_lower: + breakdown['upi_wallet_paytm'] += float(total) + + # Calculate refunds + refund_query = db.session.query(func.sum(Transaction.return_amount)) + if machine_ids: + refund_query = refund_query.filter(Transaction.machine_id.in_(machine_ids)) + + breakdown['refund'] = float(refund_query.filter( + Transaction.amount_receiving_status == 'Dispense failed refund' + ).scalar() or 0.0) + + breakdown['refund_processed'] = float(refund_query.filter( + Transaction.amount_receiving_status == 'Refund processed' + ).scalar() or 0.0) + + breakdown['total'] = sum([ + breakdown['cash'], + breakdown['cashless'], + breakdown['upi_wallet_card'], + breakdown['upi_wallet_paytm'] + ]) + + return breakdown + + +def calculate_machine_stock_status(machine_ids=None): + """Calculate machine stock status based on vending slots""" + query = Machine.query + if machine_ids: + query = query.filter(Machine.machine_id.in_(machine_ids)) + + machines = query.all() + + status_counts = { + '75 - 100%': 0, + '50 - 75%': 0, + '25 - 50%': 0, + '0 - 25%': 0, + 'N/A': 0 + } + + for machine in machines: + slots = VendingSlot.query.filter_by(machine_id=machine.machine_id).all() + if not slots: + status_counts['N/A'] += 1 + continue + + total_slots = len(slots) + filled_slots = sum(1 for slot in slots if slot.units and slot.units > 0) + + if total_slots == 0: + status_counts['N/A'] += 1 + else: + fill_percentage = (filled_slots / total_slots) * 100 + if fill_percentage >= 75: + status_counts['75 - 100%'] += 1 + elif fill_percentage >= 50: + status_counts['50 - 75%'] += 1 + elif fill_percentage >= 25: + status_counts['25 - 50%'] += 1 + elif fill_percentage > 0: + status_counts['0 - 25%'] += 1 + else: + status_counts['0 - 25%'] += 1 + + return status_counts + + +def get_product_sales_yearly(machine_ids=None): + """Get product sales aggregated by year""" + query = db.session.query( + extract('year', Transaction.created_at).label('year'), + func.sum(Transaction.amount).label('total_sales') + ) + + if machine_ids: + query = query.filter(Transaction.machine_id.in_(machine_ids)) + + query = query.filter(Transaction.status == 'Success')\ + .group_by(extract('year', Transaction.created_at))\ + .order_by('year') + + results = query.all() + + return [{'year': str(int(year)), 'amount': float(total_sales)} + for year, total_sales in results if year] + + +def get_sales_yearly(machine_ids=None): + """Get sales aggregated by year""" + return get_product_sales_yearly(machine_ids) + + +def get_top_selling_products(machine_ids=None, limit=10): + """Get top selling products""" + query = db.session.query( + Transaction.product_name, + func.sum(Transaction.quantity).label('total_quantity') + ) + + if machine_ids: + query = query.filter(Transaction.machine_id.in_(machine_ids)) + + query = query.filter(Transaction.status == 'Success')\ + .group_by(Transaction.product_name)\ + .order_by(func.sum(Transaction.quantity).desc())\ + .limit(limit) + + results = query.all() + + return [{'product_name': product_name, 'quantity': int(total_quantity)} + for product_name, total_quantity in results] + +@bp.route('/product-sales', methods=['GET']) +def get_product_sales_filtered(): + """Get product sales data filtered by time range""" + try: + current_user = get_current_user_from_token() + if not current_user: + return jsonify({'error': 'Authentication required'}), 401 + + time_range = request.args.get('time_range', 'year') + user_role = current_user.roles + + print(f"Product Sales Filter - User: {current_user.username}, Range: {time_range}") + + # Get machine IDs based on role + machine_ids = None + if user_role == 'Client': + machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=current_user.id).all()] + + # Fetch data based on time range + product_sales = get_sales_data_by_range(time_range, machine_ids) + + return jsonify({'product_sales': product_sales}), 200 + + except Exception as e: + print(f"Error in product sales filter: {str(e)}") + return jsonify({'error': str(e)}), 500 + + +@bp.route('/sales-data', methods=['GET']) +def get_sales_filtered(): + """Get sales data filtered by time range""" + try: + current_user = get_current_user_from_token() + if not current_user: + return jsonify({'error': 'Authentication required'}), 401 + + time_range = request.args.get('time_range', 'year') + user_role = current_user.roles + + print(f"Sales Filter - User: {current_user.username}, Range: {time_range}") + + # Get machine IDs based on role + machine_ids = None + if user_role == 'Client': + machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=current_user.id).all()] + + # Fetch data based on time range + sales_data = get_sales_data_by_range(time_range, machine_ids) + + return jsonify({'sales_data': sales_data}), 200 + + except Exception as e: + print(f"Error in sales filter: {str(e)}") + return jsonify({'error': str(e)}), 500 + + +@bp.route('/top-products', methods=['GET']) +def get_top_products_filtered(): + """Get top selling products filtered by time range""" + try: + current_user = get_current_user_from_token() + if not current_user: + return jsonify({'error': 'Authentication required'}), 401 + + time_range = request.args.get('time_range', 'year') + user_role = current_user.roles + + print(f"Top Products Filter - User: {current_user.username}, Range: {time_range}") + + # Get machine IDs based on role + machine_ids = None + if user_role == 'Client': + machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=current_user.id).all()] + + # Fetch data based on time range + top_products = get_top_products_by_range(time_range, machine_ids) + + return jsonify({'top_products': top_products}), 200 + + except Exception as e: + print(f"Error in top products filter: {str(e)}") + return jsonify({'error': str(e)}), 500 + + +# Helper function to get date filter based on time range +def get_date_filter(time_range): + """Return date filter based on time range""" + now = datetime.now() + + if time_range == 'day': + # Last 24 hours + return now - timedelta(days=1) + elif time_range == 'week': + # Last 7 days + return now - timedelta(weeks=1) + elif time_range == 'month': + # Last 30 days + return now - timedelta(days=30) + elif time_range == 'year': + # All time, grouped by year + return None + else: + return None + + +def get_sales_data_by_range(time_range, machine_ids=None): + """Get sales data aggregated by time range""" + date_filter = get_date_filter(time_range) + + # Base query + query = db.session.query(Transaction.created_at, Transaction.amount)\ + .filter(Transaction.status == 'Success') + + # Apply machine filter if needed + if machine_ids: + query = query.filter(Transaction.machine_id.in_(machine_ids)) + + # Apply date filter + if date_filter: + query = query.filter(Transaction.created_at >= date_filter) + + transactions = query.all() + + # Aggregate based on time range + if time_range == 'day': + # Group by hour + aggregated = {} + for txn in transactions: + hour = txn.created_at.strftime('%H:00') + if hour not in aggregated: + aggregated[hour] = 0 + aggregated[hour] += float(txn.amount) + + return [{'year': k, 'amount': v} for k, v in sorted(aggregated.items())] + + elif time_range == 'week': + # Group by day + aggregated = {} + for txn in transactions: + day = txn.created_at.strftime('%a') # Mon, Tue, etc. + if day not in aggregated: + aggregated[day] = 0 + aggregated[day] += float(txn.amount) + + # Sort by weekday order + days_order = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + return [{'year': day, 'amount': aggregated.get(day, 0)} for day in days_order] + + elif time_range == 'month': + # Group by day + aggregated = {} + for txn in transactions: + day = txn.created_at.strftime('%d') + if day not in aggregated: + aggregated[day] = 0 + aggregated[day] += float(txn.amount) + + return [{'year': k, 'amount': v} for k, v in sorted(aggregated.items())] + + else: # year or all time + # Group by year + query = db.session.query( + extract('year', Transaction.created_at).label('year'), + func.sum(Transaction.amount).label('total_sales') + ).filter(Transaction.status == 'Success') + + if machine_ids: + query = query.filter(Transaction.machine_id.in_(machine_ids)) + + query = query.group_by(extract('year', Transaction.created_at))\ + .order_by('year') + + results = query.all() + + return [{'year': str(int(year)), 'amount': float(total_sales)} + for year, total_sales in results if year] + + +def get_top_products_by_range(time_range, machine_ids=None, limit=10): + """Get top selling products filtered by time range""" + date_filter = get_date_filter(time_range) + + # Base query + query = db.session.query( + Transaction.product_name, + func.sum(Transaction.quantity).label('total_quantity') + ).filter(Transaction.status == 'Success') + + # Apply machine filter if needed + if machine_ids: + query = query.filter(Transaction.machine_id.in_(machine_ids)) + + # Apply date filter + if date_filter: + query = query.filter(Transaction.created_at >= date_filter) + + query = query.group_by(Transaction.product_name)\ + .order_by(func.sum(Transaction.quantity).desc())\ + .limit(limit) + + results = query.all() + + return [{'product_name': product_name, 'quantity': int(total_quantity)} + for product_name, total_quantity in results] # Additional utility routes for PayU integration @bp.route('/verify-payu-hash', methods=['POST']) @@ -1603,7 +2067,7 @@ def get_roles(): return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can view roles - if current_user.roles not in ['Management', 'SuperAdmin']: + if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 roles = RoleService.get_all_roles() @@ -1622,7 +2086,7 @@ def create_role(): return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can create roles - if current_user.roles not in ['Management', 'SuperAdmin']: + if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 data = request.json @@ -1645,7 +2109,7 @@ def update_role(role_id): return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can update roles - if current_user.roles not in ['Management', 'SuperAdmin']: + if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 data = request.json @@ -1668,7 +2132,7 @@ def delete_role(role_id): return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can delete roles - if current_user.roles not in ['Management', 'SuperAdmin']: + if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 RoleService.delete_role(role_id) @@ -1690,7 +2154,7 @@ def get_available_permissions(): return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can view permissions - if current_user.roles not in ['Management', 'SuperAdmin']: + if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 # Define all available permissions diff --git a/Machine-Backend/app/uploads/f736133f7f6b40c3a836747a87567606_download.png b/Machine-Backend/app/uploads/070423cbc97247739946779e8bef485b_download.png similarity index 100% rename from Machine-Backend/app/uploads/f736133f7f6b40c3a836747a87567606_download.png rename to Machine-Backend/app/uploads/070423cbc97247739946779e8bef485b_download.png diff --git a/Machine-Backend/app/uploads/b30aac7345554870812ccc0df4647213_7up.png b/Machine-Backend/app/uploads/a054b74e7a5c4714bf8199c43c8e58e2_7up.png similarity index 100% rename from Machine-Backend/app/uploads/b30aac7345554870812ccc0df4647213_7up.png rename to Machine-Backend/app/uploads/a054b74e7a5c4714bf8199c43c8e58e2_7up.png diff --git a/Machine-Backend/app/uploads/company_logos/30a4d83d8d924884abb79f715941f1e8_social-support.png b/Machine-Backend/app/uploads/company_logos/30a4d83d8d924884abb79f715941f1e8_social-support.png deleted file mode 100644 index c83de0d..0000000 Binary files a/Machine-Backend/app/uploads/company_logos/30a4d83d8d924884abb79f715941f1e8_social-support.png and /dev/null differ diff --git a/Machine-Backend/app/uploads/user_documents/0ffd085d728f4993a0a5a632a9e53d85_sample.pdf b/Machine-Backend/app/uploads/user_documents/0ffd085d728f4993a0a5a632a9e53d85_sample.pdf deleted file mode 100644 index 94d9477..0000000 Binary files a/Machine-Backend/app/uploads/user_documents/0ffd085d728f4993a0a5a632a9e53d85_sample.pdf and /dev/null differ diff --git a/Machine-Backend/app/uploads/user_photos/701adc9c512748cba515c0021c5cd1f1_doctor.jpg b/Machine-Backend/app/uploads/user_photos/aa3622cbfa17457faea2a8a9da4ee99a_doctor.jpg similarity index 100% rename from Machine-Backend/app/uploads/user_photos/701adc9c512748cba515c0021c5cd1f1_doctor.jpg rename to Machine-Backend/app/uploads/user_photos/aa3622cbfa17457faea2a8a9da4ee99a_doctor.jpg diff --git a/Machine-Backend/instance/machines.db b/Machine-Backend/instance/machines.db index 2568e95..6250316 100644 Binary files a/Machine-Backend/instance/machines.db and b/Machine-Backend/instance/machines.db differ diff --git a/Machine-Backend/reset_password.py b/Machine-Backend/reset_password.py new file mode 100644 index 0000000..529dc8e --- /dev/null +++ b/Machine-Backend/reset_password.py @@ -0,0 +1,18 @@ +from app import create_app, db +from app.models.models import User +from werkzeug.security import generate_password_hash + +app = create_app() + +with app.app_context(): + email = "test@example.com" + new_password = "test123" + + user = User.query.filter_by(email=email).first() + if not user: + print("User not found ❌") + else: + # Hash password in the way your model expects + user.password = generate_password_hash(new_password, method='pbkdf2:sha256') + db.session.commit() + print(f"Password reset successfully for {email}. Login with: {new_password}") diff --git a/fuse-starter-v20.0.0/src/app/app.routes.ts b/fuse-starter-v20.0.0/src/app/app.routes.ts index 738a599..428b538 100644 --- a/fuse-starter-v20.0.0/src/app/app.routes.ts +++ b/fuse-starter-v20.0.0/src/app/app.routes.ts @@ -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') }, diff --git a/fuse-starter-v20.0.0/src/app/modules/admin/example/example.component.html b/fuse-starter-v20.0.0/src/app/modules/admin/example/example.component.html index 20b022e..996d99d 100644 --- a/fuse-starter-v20.0.0/src/app/modules/admin/example/example.component.html +++ b/fuse-starter-v20.0.0/src/app/modules/admin/example/example.component.html @@ -1,162 +1,305 @@ +
-
-
- {{ errorMessage }} +
+ +
+
+ {{ errorMessage }} + +
-
- -
-
-
- {{ machineTitle }} -
-
- + + - - - - - -
-
-
-
- {{ machineCount }} -
+ + +
+
{{ machineCount }}
+ -
-
-
- Clients +
+
Clients
+
{{ clients }}
+
+ + +
+
Active Machines
+
{{ activeMachines }}
+
+ + +
+
Inactive Machines
+
{{ inactiveMachines }}
+
+
+ + +
+ +
+
Payment Overview
+
+
+ Cash + ₹{{ paymentCash | number:'1.0-0' }} +
+
+ Cashless + ₹{{ paymentCashless | number:'1.0-0' }} +
+
+ UPI/Card (PhonePe) + ₹{{ paymentUPIWalletCard | number:'1.0-0' }} +
+
+ UPI/Wallet (Paytm) + ₹{{ paymentUPIWalletPaytm | number:'1.0-0' }}
-
-
- {{ clients }} +
+
+ Refund + ₹{{ refund | number:'1.2-2' }} +
+
+ Refund Processed + ₹{{ refundProcessed | number:'1.2-2' }} +
+
+ Total + ₹{{ paymentTotal | number:'1.0-0' }}
- -
-
-
- Company Users -
+ + +
+ +
+
Company Users
+
{{ companyUsers }}
-
-
- {{ companyUsers }} + + +
+
Client Users
+
{{ clientUsers }}
+
+ + +
+
Transactions
+
{{ transactions | number }}
+
+ + +
+
Total Sales
+
{{ sales }}
+
+
+
+ + +
+ +
+
+

Machine Operation Status

+ filter_list +
+
+
+ {{ key }} + {{ machineOperationStatus[key] }}
- -
-
-
- Client User -
+ + +
+
+

Machine Stock Status

+ filter_list
-
-
- {{ clientUsers }} -
-
-
- -
-
-
- Transactions -
-
-
-
- {{ transactions }} -
-
-
- -
-
-
- Sales -
-
-
-
- {{ sales }} +
+
+
{{ key }}
+
{{ machineStockStatus[key] }}
- -
- -
-
- Key Metrics Overview + + +
+ +
+
+

Product Sales

+ + + + + + +
- -
-
- Transaction Trends + + +
+
+

Top Selling Products

+ + + + + + +
- -
- -
-
- Sales Distribution +
+ + + + + + + + + + + + + + + + +
ProductQuantity
{{ product.product_name }}{{ product.quantity | number }}
+ inbox +
No data available
+
-
- -
-
- Client User Categories -
- +
+ + +
+
+

Sales Trend

+ + + + + + +
+
+
\ No newline at end of file diff --git a/fuse-starter-v20.0.0/src/app/modules/admin/example/example.component.ts b/fuse-starter-v20.0.0/src/app/modules/admin/example/example.component.ts index 0ed1853..58b7147 100644 --- a/fuse-starter-v20.0.0/src/app/modules/admin/example/example.component.ts +++ b/fuse-starter-v20.0.0/src/app/modules/admin/example/example.component.ts @@ -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; - lineChartOptions: Partial; - doughnutChartOptions: Partial; - pieChartOptions: Partial; + + // 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; + salesChartOptions: Partial; 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(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(' - +