init commit
This commit is contained in:
495
transportmanager/client/pages/dashboard_page.py
Normal file
495
transportmanager/client/pages/dashboard_page.py
Normal file
@@ -0,0 +1,495 @@
|
||||
import flet as ft
|
||||
from flet import canvas
|
||||
import requests
|
||||
from pages.clients_page import ClientsPage
|
||||
from pages.transporters_page import TransportersPage
|
||||
from pages.destinations_page import DestinationsPage
|
||||
from pages.orders_page import OrdersPage
|
||||
from pages.report_page import ReportPage
|
||||
from pages.profile_page import ProfilePage
|
||||
from pages.subscription_page import Subscription
|
||||
from datetime import datetime
|
||||
from pages.admin_page import Admin
|
||||
from config import API_BASE_URL
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
class DashboardPage:
|
||||
def __init__(self, page: ft.Page):
|
||||
self.page = page
|
||||
self.placeholder = ft.Container(
|
||||
expand=True,
|
||||
padding=ft.padding.only(
|
||||
top=30,
|
||||
left=20,
|
||||
right=20,
|
||||
bottom=20
|
||||
),
|
||||
)
|
||||
self.all_clients = self.get_all_clients()
|
||||
self.all_orders = self.get_all_orders()
|
||||
self.all_transporters = self.get_all_transporters()
|
||||
self.orders = []
|
||||
self.profit = self.get_profit()
|
||||
self.logo = ft.Image(
|
||||
src=f"images/{self.page.client_storage.get('logo_filename') or 'truck_logo_black.png'}",
|
||||
width=60,
|
||||
height=60,
|
||||
fit=ft.ImageFit.CONTAIN
|
||||
)
|
||||
self.subscription_status = ft.Text(
|
||||
"",
|
||||
color=ft.Colors.RED
|
||||
)
|
||||
self.user = self.get_user()
|
||||
|
||||
# --- helper to parse created_at values coming from API in multiple formats ---
|
||||
def _parse_dt(self, s):
|
||||
if not s:
|
||||
return None
|
||||
if isinstance(s, datetime):
|
||||
return s
|
||||
try:
|
||||
# First try ISO 8601 (with or without microseconds)
|
||||
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
# Then try RFC 2822 / RFC 1123 (e.g., 'Mon, 18 Aug 2025 19:22:19 GMT')
|
||||
return parsedate_to_datetime(s)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
# Fallback: common ISO without microseconds
|
||||
return datetime.strptime(s, "%Y-%m-%dT%H:%M:%S")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user(self):
|
||||
token = self.page.client_storage.get("token")
|
||||
if not token:
|
||||
self.message.value = "Unauthorized: No token"
|
||||
return
|
||||
response = requests.get(f"{API_BASE_URL}/profile/", headers={"Authorization": f"Bearer {token}"})
|
||||
#print(response.text)
|
||||
return response.json() if response.status_code == 200 else None
|
||||
|
||||
def on_admin_btn_click(self, e):
|
||||
admin = Admin(self.page, self)
|
||||
self.placeholder.content = admin.build()
|
||||
self.placeholder.update()
|
||||
|
||||
def get_all_transporters(self):
|
||||
try:
|
||||
token = self.page.client_storage.get("token")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(f"{API_BASE_URL}/transporters/", headers=headers)
|
||||
return response.json() if response.status_code == 200 else []
|
||||
except Exception as e:
|
||||
print("Error loading transporters:", e)
|
||||
|
||||
|
||||
def get_all_orders(self):
|
||||
try:
|
||||
token = self.page.client_storage.get("token")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(f"{API_BASE_URL}/orders/list", headers=headers)
|
||||
return response.json() if response.status_code == 200 else []
|
||||
except Exception as e:
|
||||
print("Error loading orders:", e)
|
||||
|
||||
def get_all_clients(self):
|
||||
try:
|
||||
token = self.page.client_storage.get("token")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(f"{API_BASE_URL}/clients/", headers=headers)
|
||||
return response.json() if response.status_code == 200 else []
|
||||
except Exception as e:
|
||||
print("Error loading clients:", e)
|
||||
|
||||
def get_profit(self):
|
||||
date_today = datetime.now()
|
||||
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
#print(month_first_day)
|
||||
if len(self.all_orders)>0:
|
||||
for order in self.all_orders:
|
||||
dt = self._parse_dt(order.get('created_at'))
|
||||
if dt and dt.date() >= month_first_day.date():
|
||||
self.orders.append(order)
|
||||
all_profit = 0
|
||||
for order in self.orders:
|
||||
profit = round(order['received_price'] - order['paid_price'], 2)
|
||||
all_profit += profit
|
||||
return all_profit
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def _build_dashboard_content(self):
|
||||
self.all_clients = self.get_all_clients()
|
||||
self.all_orders = self.get_all_orders()
|
||||
self.all_transporters = self.get_all_transporters()
|
||||
self.orders = []
|
||||
self.profit = self.get_profit()
|
||||
|
||||
summary_cards = ft.Row(
|
||||
controls=[
|
||||
#self._summary_card("Clients", len(self.all_clients), ft.Icons.PEOPLE),
|
||||
#self._summary_card("Transporters", len(self.all_transporters), ft.Icons.LOCAL_SHIPPING),
|
||||
self._summary_card("Monthly Orders", len(self.orders), ft.Icons.RECEIPT_LONG),
|
||||
self._summary_card("Monthly Profit", self.profit, ft.Icons.SHOW_CHART),
|
||||
],
|
||||
spacing=20,
|
||||
wrap=True
|
||||
)
|
||||
|
||||
# Prepare data for BarChart using flet_charts
|
||||
from collections import defaultdict
|
||||
daily_orders = defaultdict(int)
|
||||
for order in self.orders:
|
||||
try:
|
||||
dt = self._parse_dt(order.get('created_at'))
|
||||
if dt:
|
||||
daily_orders[dt.day] += 1
|
||||
except Exception as e:
|
||||
print("Invalid date:", order['created_at'])
|
||||
|
||||
max_value = max(daily_orders.values(), default=1)
|
||||
|
||||
bar_groups = []
|
||||
for day in sorted(daily_orders.keys()):
|
||||
value = daily_orders[day]
|
||||
bar_groups.append(
|
||||
ft.BarChartGroup(
|
||||
x=day,
|
||||
bar_rods=[
|
||||
ft.BarChartRod(
|
||||
from_y=0,
|
||||
to_y=value,
|
||||
width=20,
|
||||
color=ft.Colors.BLUE
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
chart_1 = ft.Container(
|
||||
expand=True,
|
||||
bgcolor=ft.Colors.BLUE_50,
|
||||
padding=10,
|
||||
border_radius=20,
|
||||
content=ft.Column([
|
||||
ft.Text("Monthly Orders", size=16, weight=ft.FontWeight.BOLD),
|
||||
ft.BarChart(
|
||||
bar_groups=bar_groups,
|
||||
border=ft.border.all(1, ft.Colors.GREY_300),
|
||||
left_axis=ft.ChartAxis(
|
||||
labels_size=40,
|
||||
labels=[
|
||||
ft.ChartAxisLabel(
|
||||
value=i,
|
||||
label=ft.Text(str(i), size=10)
|
||||
) for i in range(0, max_value + 1, max(1, max_value // 5))
|
||||
],
|
||||
),
|
||||
bottom_axis=ft.ChartAxis(
|
||||
labels=[
|
||||
ft.ChartAxisLabel(
|
||||
value=day,
|
||||
label=ft.Text(str(day), size=10)
|
||||
) for day in sorted(daily_orders.keys())
|
||||
],
|
||||
),
|
||||
tooltip_bgcolor=ft.Colors.with_opacity(0.8, ft.Colors.BLUE),
|
||||
expand=True,
|
||||
animate=True
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
# Calculate daily profit
|
||||
from collections import defaultdict
|
||||
daily_profit = defaultdict(float)
|
||||
for order in self.orders:
|
||||
try:
|
||||
dt = self._parse_dt(order.get('created_at'))
|
||||
if dt:
|
||||
profit = order["received_price"] - order["paid_price"]
|
||||
daily_profit[dt.day] += profit
|
||||
except Exception as e:
|
||||
print("Invalid date in profit chart:", order['created_at'])
|
||||
|
||||
max_profit = max(daily_profit.values(), default=1)
|
||||
|
||||
profit_bar_groups = []
|
||||
for day in sorted(daily_profit.keys()):
|
||||
value = daily_profit[day]
|
||||
profit_bar_groups.append(
|
||||
ft.BarChartGroup(
|
||||
x=day,
|
||||
bar_rods=[
|
||||
ft.BarChartRod(
|
||||
from_y=0,
|
||||
to_y=value,
|
||||
width=20,
|
||||
color=ft.Colors.GREEN
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
chart_2 = ft.Container(
|
||||
expand=True,
|
||||
bgcolor=ft.Colors.BLUE_50,
|
||||
padding=10,
|
||||
border_radius=20,
|
||||
content=ft.Column([
|
||||
ft.Text("Daily Profit", size=16, weight=ft.FontWeight.BOLD),
|
||||
ft.BarChart(
|
||||
bar_groups=profit_bar_groups,
|
||||
border=ft.border.all(1, ft.Colors.GREY_300),
|
||||
left_axis=ft.ChartAxis(
|
||||
labels_size=40,
|
||||
labels=[
|
||||
ft.ChartAxisLabel(
|
||||
value=i,
|
||||
label=ft.Text(str(i), size=10)
|
||||
) for i in range(0, int(max_profit)+1, max(1, int(max_profit)//5))
|
||||
],
|
||||
),
|
||||
bottom_axis=ft.ChartAxis(
|
||||
labels=[
|
||||
ft.ChartAxisLabel(
|
||||
value=day,
|
||||
label=ft.Text(str(day), size=10)
|
||||
) for day in sorted(daily_profit.keys())
|
||||
],
|
||||
),
|
||||
tooltip_bgcolor=ft.Colors.with_opacity(0.8, ft.Colors.GREEN),
|
||||
expand=True,
|
||||
animate=True
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
charts = ft.Row(
|
||||
controls=[chart_1, chart_2],
|
||||
expand=True,
|
||||
spacing=20
|
||||
)
|
||||
|
||||
if self.user:
|
||||
if self.get_subscription():
|
||||
if self.get_subscription()['end_date'] < datetime.today()-3:
|
||||
self.subscription_status.value = "Your subscription will expire soon. Please Renew!"
|
||||
self.subscription_status.update()
|
||||
elif self.get_subscription()['end_date'] > datetime.today():
|
||||
self.subscription_status.value = "Your subscription has expired!"
|
||||
self.subscription_status.update()
|
||||
return ft.Column(
|
||||
[
|
||||
ft.Row(
|
||||
[
|
||||
summary_cards,
|
||||
self.subscription_status
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
|
||||
),
|
||||
charts
|
||||
],
|
||||
spacing=20,
|
||||
expand=True
|
||||
)
|
||||
else:
|
||||
return ft.Column(
|
||||
controls=[
|
||||
summary_cards,
|
||||
charts
|
||||
],
|
||||
spacing=20,
|
||||
expand=True
|
||||
)
|
||||
else:
|
||||
return ft.Column(
|
||||
controls=[
|
||||
summary_cards,
|
||||
charts
|
||||
],
|
||||
spacing=20,
|
||||
expand=True
|
||||
)
|
||||
|
||||
def _summary_card(self, title, value, icon):
|
||||
return ft.Container(
|
||||
content=ft.Column(
|
||||
controls=[
|
||||
ft.Icon(name=icon, size=30),
|
||||
ft.Text(value, weight=ft.FontWeight.BOLD, size=18),
|
||||
ft.Text(title, size=14)
|
||||
],
|
||||
spacing=5,
|
||||
alignment=ft.MainAxisAlignment.CENTER
|
||||
),
|
||||
width=150,
|
||||
height=100,
|
||||
padding=10,
|
||||
alignment=ft.alignment.center,
|
||||
border_radius=10,
|
||||
bgcolor=ft.Colors.BLUE_100
|
||||
)
|
||||
|
||||
def _navigate(self, index):
|
||||
self.placeholder.content.clean()
|
||||
if index == 0:
|
||||
self.placeholder.content = self._build_dashboard_content()
|
||||
elif index == 1:
|
||||
client = ClientsPage(self.page, self)
|
||||
self.placeholder.content = client.build()
|
||||
elif index == 2:
|
||||
transporters = TransportersPage(self.page, self)
|
||||
self.placeholder.content = transporters.build()
|
||||
elif index == 3:
|
||||
destinations = DestinationsPage(self.page, self)
|
||||
self.placeholder.content = destinations.build()
|
||||
elif index == 4:
|
||||
orders = OrdersPage(self.page, self)
|
||||
self.placeholder.content = orders.build()
|
||||
elif index == 5:
|
||||
reports = ReportPage(self.page, self)
|
||||
self.placeholder.content = reports.build()
|
||||
elif index == 6:
|
||||
profile = ProfilePage(self.page, self)
|
||||
self.placeholder.content = profile.build()
|
||||
elif index == 7:
|
||||
subscription = Subscription(self.page, self)
|
||||
self.placeholder.content = subscription.build()
|
||||
self.placeholder.update()
|
||||
|
||||
def got_to_profile(self, e):
|
||||
self.placeholder.content.clean()
|
||||
profile = ProfilePage(self.page, self)
|
||||
self.placeholder.content = profile.build()
|
||||
self.placeholder.update()
|
||||
|
||||
def _navigate_or_logout(self, index):
|
||||
if index == 8: # Index of the "Logout" destination
|
||||
self._logout()
|
||||
else:
|
||||
self._navigate(index)
|
||||
|
||||
def _logout(self):
|
||||
self.page.client_storage.remove("token")
|
||||
self.page.session.clear()
|
||||
self.page.go("/auth")
|
||||
|
||||
def get_subscription(self):
|
||||
try:
|
||||
user_id = self.page.session.get("user_id")
|
||||
token = self.page.client_storage.get("token")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(f"{API_BASE_URL}/admin/subscriptions/{user_id}", headers=headers)
|
||||
return response.json() if response.status_code == 200 else None
|
||||
except Exception as e:
|
||||
print("Error loading clients:", e)
|
||||
|
||||
def get_current_subscription_plan(self):
|
||||
try:
|
||||
token = self.page.client_storage.get("token")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(f"{API_BASE_URL}/subscription/", headers=headers)
|
||||
#print(response.text)
|
||||
return response.json()[-1] if response.status_code == 200 else None
|
||||
except Exception as e:
|
||||
print("Error loading subscription:", e)
|
||||
|
||||
def build(self):
|
||||
self.status = {
|
||||
'active':'Active',
|
||||
'cancelled':'Cancelled',
|
||||
'expired':'Expired',
|
||||
'less_than_5_days':'Less than 5 days'
|
||||
}
|
||||
self.subscription = self.get_current_subscription_plan()
|
||||
|
||||
if self.subscription:
|
||||
self.subscription_status_bottom = ft.Text(
|
||||
f'{self.status[self.subscription['status']] if self.subscription else "None"}',
|
||||
size=12,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
color=ft.Colors.GREEN if self.subscription['status'] != 'expired' else ft.Colors.RED
|
||||
)
|
||||
else:
|
||||
self.subscription_status_bottom = ft.Text(
|
||||
f'{self.status[self.subscription['status']] if self.subscription else "None"}',
|
||||
size=12,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
color=ft.Colors.RED
|
||||
)
|
||||
|
||||
nav_rail = ft.NavigationRail(
|
||||
selected_index=0,
|
||||
label_type=ft.NavigationRailLabelType.ALL,
|
||||
on_change=lambda e: self._navigate_or_logout(e.control.selected_index),
|
||||
destinations=[
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.DASHBOARD, label="Dashboard"
|
||||
),
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.PEOPLE, label="Clients"
|
||||
),
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.LOCAL_SHIPPING, label="Transporters"
|
||||
),
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.LOCATION_ON, label="Address"
|
||||
),
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.RECEIPT_LONG, label="Orders"
|
||||
),
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.ASSESSMENT, label="Reports"
|
||||
),
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.MANAGE_ACCOUNTS, label="Profile"
|
||||
),
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.AUTORENEW, label="Subscription"
|
||||
),
|
||||
ft.NavigationRailDestination(
|
||||
icon=ft.Icons.LOGOUT, label="Logout"
|
||||
)
|
||||
],
|
||||
leading=ft.Container(
|
||||
content=ft.Column(
|
||||
controls=[self.logo],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
spacing=5,
|
||||
),
|
||||
padding=10
|
||||
),
|
||||
#bgcolor = ft.Colors.BLUE,
|
||||
|
||||
trailing = ft.Column(
|
||||
[
|
||||
ft.Text(
|
||||
f'\nSubsctiption:',
|
||||
size=12,
|
||||
),
|
||||
self.subscription_status_bottom
|
||||
],
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER
|
||||
)
|
||||
)
|
||||
|
||||
self.placeholder.content = self._build_dashboard_content()
|
||||
|
||||
return ft.Row(
|
||||
controls=[
|
||||
nav_rail,
|
||||
ft.VerticalDivider(),
|
||||
self.placeholder
|
||||
],
|
||||
expand=True
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user