init commit

This commit is contained in:
2025-08-31 17:55:26 +03:00
commit 876ddec94a
78 changed files with 11999 additions and 0 deletions

BIN
transportmanager/client/.DS_Store vendored Normal file

Binary file not shown.

BIN
transportmanager/client/assets/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,8 @@
import os
from dotenv import load_dotenv
# Load .env from project root even if running from client/
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "..", ".env"))
API_BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:5000")
FLET_SERVER_PORT = int(os.getenv("FLET_SERVER_PORT", "8080"))

View File

@@ -0,0 +1,108 @@
import flet as ft
from pages.auth_page import Auth
from pages.dashboard_page import DashboardPage
from pages.admin_page import Admin
from pages.reset_password_page import ResetPasswordPage
import os
import requests
from config import API_BASE_URL, FLET_SERVER_PORT
os.environ["FLET_SECRET_KEY"] = os.urandom(12).hex()
def main(page: ft.Page):
page.title = "Transport Manager"
page.theme_mode = ft.ThemeMode.LIGHT
page.theme = ft.Theme(color_scheme=ft.ColorScheme(primary=ft.Colors.BLUE))
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.padding = 0
def is_token_valid(token: str) -> bool:
if not token:
return False
try:
resp = requests.get(
f"{API_BASE_URL}/auth/validate_token",
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
return resp.status_code == 200
except Exception:
# If the server can't be reached, treat as invalid to avoid granting access
return False
def route_change(e):
# Current path
route = page.route
# Determine auth state by validating token with backend
token = page.client_storage.get("token")
valid_token = is_token_valid(token) if token else False
is_authenticated = bool(valid_token and page.session.get('user_id'))
# If token is invalid but present, clean it up
if token and not valid_token:
try:
page.client_storage.remove("token")
except Exception:
pass
try:
page.session.pop("user_id", None)
except Exception:
pass
# Clear current UI
page.controls.clear()
# 1) Reset Password allow opening directly from email link
if route and "reset_password" in route:
reset_page = ResetPasswordPage(page)
page.add(reset_page.build())
page.update()
return
# 2) Auth route if already logged in with a valid token, go to dashboard
if route == "/auth":
if is_authenticated:
page.go("/dashboard")
return
login = Auth(page)
page.add(login.build())
page.update()
return
# 3) Admin (protect)
if route == "/admin":
if not is_authenticated:
page.go("/auth")
return
admin = Admin(page)
page.add(admin.build())
page.update()
return
# 4) Dashboard & root
if route in ("/dashboard", "/", None):
if not is_authenticated:
page.go("/auth")
return
dashboard = DashboardPage(page)
page.add(dashboard.build())
page.update()
return
# 5) Fallback 404
page.add(ft.Text("404: Page not found"))
page.update()
page.on_route_change = route_change
page.go(page.route or "/auth")
ft.app(
target=main,
assets_dir="assets",
upload_dir="uploads",
view=ft.WEB_BROWSER,
host="0.0.0.0",
port=FLET_SERVER_PORT,
)

View File

@@ -0,0 +1,147 @@
import flet as ft
from pages.admin_tenants_page import Tenants
from pages.admin_subscriptions_page import Subscriptions
import requests
from config import API_BASE_URL
class Admin:
def __init__(self, page: ft.Page):
self.page = page
self.placeholder = ft.Column(expand=True)
self.total_tenants = ft.Text("Total tenants: 0")
self.revenues = ft.Text("Total revenue: 0")
def get_subscriptions(self):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/admin/subscriptions", headers=headers)
return response.json() if response.status_code == 200 else []
except Exception as e:
print("Error loading clients:", e)
def build_dashboard(self):
all_subscriptions = self.get_subscriptions()
total_tenants = len(all_subscriptions)
self.total_tenants.value = f"Total tenants: {total_tenants}"
sum = 0
for subscription in all_subscriptions:
if subscription['status'] == 'active':
if subscription['plan'] == 'first_2_months':
sum += 0
elif subscription['plan'] == 'monthly':
sum += 100
elif subscription['plan'] == 'yearly':
sum += 1000
self.revenues.value = f"Total revenue: {sum}"
return ft.Row(
[
ft.Container(
content = ft.Column(
[
ft.Icon(name=ft.Icons.PEOPLE, size=50),
self.total_tenants,
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER
),
bgcolor=ft.Colors.BLUE_100,
border_radius=20,
padding=20
),
ft.Container(
content = ft.Column(
[
ft.Icon(name=ft.Icons.AUTORENEW, size=50),
self.revenues,
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER
),
bgcolor=ft.Colors.BLUE_100,
border_radius=20,
padding=20
),
]
)
def on_tenants_btn_click(self, e):
tenents = Tenants(self.page)
self.placeholder.controls = [tenents.build()]
self.placeholder.update()
def on_subscription_btn_click(self, e):
subscription = Subscriptions(self.page)
self.placeholder.controls = [subscription.build()]
self.placeholder.update()
def on_dashboard_btn_click(self, e):
self.placeholder.controls = [
ft.Text("Admin Dashboard", size=24, weight=ft.FontWeight.BOLD),
self.build_dashboard()
]
self.placeholder.update()
def on_logout_btn_click(self, e):
self.page.client_storage.remove("token")
self.page.session.clear()
self.page.go("/auth")
def build(self):
self.placeholder.controls = [
ft.Text("Admin Dashboard", size=24, weight=ft.FontWeight.BOLD),
self.build_dashboard()
]
return ft.Container(
content= ft.Row(
[
ft.Column(
[
ft.Column(
[
ft.Button(
"Dashboard",
on_click=self.on_dashboard_btn_click,
width=150,
icon=ft.Icons.DASHBOARD
),
ft.Button(
"Tenents",
on_click= self.on_tenants_btn_click,
width=150,
icon=ft.Icons.PEOPLE
),
ft.Button(
"Subscriptions",
on_click=self.on_subscription_btn_click,
width=150,
icon=ft.Icons.AUTORENEW
)
]
),
ft.Button(
"Logout",
on_click=self.on_logout_btn_click,
width=150,
icon=ft.Icons.LOGOUT
)
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
#width=180
),
ft.VerticalDivider(),
ft.Container(
content=ft.Column(
[
self.placeholder,
]
),
expand=True
)
],
expand=True
),
expand=True,
padding=20
)

View File

@@ -0,0 +1,160 @@
import flet as ft
import requests
from datetime import datetime
from config import API_BASE_URL
class Subscriptions:
def __init__(self, page: ft.Page):
self.page = page
self.plan = {
'first_2_months':'First Two Months' ,
'monthly':'Monthly',
'yearly':'Yearly'
}
self.status = {
'active':'Active',
'cancelled':'Cancelled',
'expired':'Expired',
'less_than_5_days':'Less than 5 days'
}
self.search_field = ft.TextField(label="Search", on_submit=self.on_search_btn_click, expand=True)
self.all_subscriptions = self.get_subscriptions()
self.subscriptions_list = ft.ListView(
controls=self.create_list(self.all_subscriptions, self.on_subscription_btn_click),
spacing=10,
expand=4
)
self.company_name = ft.TextField(label="Company Name", read_only=True)
self.company_register_number = ft.TextField(label="Register Number", read_only=True)
self.subscription_plan = ft.TextField(label="Subscription Plan", read_only=True)
self.subscription_status = ft.TextField(label="Subscription Status", read_only=True)
self.subscription_start_date = ft.TextField(label="Subscription Start Date", read_only=True)
self.subscription_end_date = ft.TextField(label="Subscription End Date", read_only=True)
self.selected_subscription = None
def create_list(self, items, on_click_handler):
"""Helper to create list items for a column."""
return [
ft.Container(
content=ft.Row(
[
ft.Column(
[
ft.Text(item['register_number'], expand=True, weight=ft.FontWeight.BOLD),
ft.Text(self.plan[item['plan']], size=12)
]
),
ft.Text(self.status[item['status']])
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
),
width=300,
bgcolor=ft.Colors.BLUE_50 if item['status'] != 'expired' else ft.Colors.RED,
padding=10,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
ink=True, # To enable click effect
on_click=lambda e, id=item: on_click_handler(id), # Attach the click handler
)
for item in items
]
def on_subscription_btn_click(self, item):
self.selected_subscription = item
tenant = self.get_tenant(item['user_id'])
self.company_name.value = tenant['name']
self.company_name.update()
self.company_register_number.value = tenant['register_number']
self.company_register_number.update()
self.subscription_plan.value = self.plan[item['plan']]
self.subscription_plan.update()
self.subscription_status.value = self.status[item['status']]
self.subscription_status.update()
self.subscription_start_date.value = str(item['start_date']).split('T')[0]
self.subscription_start_date.update()
self.subscription_end_date.value = str(item['end_date']).split('T')[0]
self.subscription_end_date.update()
def get_tenant(self, id):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/admin/users/{id}", headers=headers)
return response.json() if response.status_code == 200 else None
except Exception as e:
print("Error loading clients:", e)
def get_subscriptions(self):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get("{API_BASE_URL}/admin/subscriptions", headers=headers)
return response.json() if response.status_code == 200 else []
except Exception as e:
print("Error loading clients:", e)
def on_search_btn_click(self, e):
value = self.search_field.value
print(f'Search for {value}')
buffer = []
for subscription in self.all_subscriptions:
if value in subscription['register_number']:
buffer.append(subscription)
self.subscriptions_list.controls.clear()
self.subscriptions_list.controls = self.create_list(buffer, self.on_subscription_btn_click)
self.subscriptions_list.update()
def update_status(self, status):
try:
user_data = {
'subscription_id': self.selected_subscription['id'],
'status': status
}
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.post("{API_BASE_URL}/admin/users/update", headers=headers, json = user_data)
#print(response.text)
except Exception as e:
print(e)
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Text("Subscriptions", size=24, weight=ft.FontWeight.BOLD),
ft.Row(
[
self.search_field,
ft.Button("Search", on_click=self.on_search_btn_click)
]
),
ft.Row(
[
self.subscriptions_list,
ft.Column(
[
self.company_name,
self.company_register_number,
self.subscription_plan,
self.subscription_status,
self.subscription_start_date,
self.subscription_end_date,
ft.Row(
[
ft.Button("Renew", on_click=self.update_status('active')),
ft.Button("Unsubscribe", on_click=self.update_status('cancelled'))
]
)
],
expand=6
)
],
vertical_alignment=ft.CrossAxisAlignment.START
)
],
alignment=ft.MainAxisAlignment.START,
)
)

View File

@@ -0,0 +1,174 @@
import flet as ft
import requests
import json
from config import API_BASE_URL
class Tenants:
def __init__(self, page):
self.page = page
self.search_field = ft.TextField(label="Search", on_submit=self.on_search_btn_click, expand=True)
self.all_tenants = self.get_all_tenants()
self.tenants_list = ft.ListView(
controls=self.create_list(self.all_tenants, self.on_tenant_btn_click),
spacing=10,
expand=4
)
self.name = ft.TextField(label="Company Name")
self.address = ft.TextField(label="Company Address")
self.contact_name = ft.TextField(label="Contact Name")
self.email = ft.TextField(label="Email")
self.phone = ft.TextField(label="Phone")
self.first_order_number = ft.TextField(label="First Order Number")
self.register_number = ft.TextField(label="Register Number")
self.terms = ft.TextField(label="Terms and Conditions", multiline=True, min_lines=5, max_lines=10)
self.logo_filename = ft.TextField(label="Logo")
self.save = ft.Button("Save", on_click=self.on_save_btn_click, width=100)
self.error = ft.Text("")
self.user_id = None
self.user_details = ft.Column(
[
self.name,
self.address,
self.contact_name,
self.email,
self.phone,
self.first_order_number,
self.register_number,
self.logo_filename,
self.terms,
self.save,
self.error
],
expand=6
)
def on_save_btn_click(self, e):
user_data = {
'name': self.name.value,
'contact_name': self.contact_name.value,
'email': self.email.value,
'phone': self.phone.value,
'register_number': self.register_number.value,
'address': self.address.value,
'logo_filename': self.logo_filename.value,
'terms': self.terms.value,
'first_order_number': self.first_order_number.value,
'user_id': self.user_id
}
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{API_BASE_URL}/admin/users/update", headers=headers, json = user_data)
if response.status_code == 200:
self.error.value = 'Tenant data saved!'
self.error.color = ft.Colors.GREEN
self.error.update()
else:
self.error.value = 'Tenant data not saved!'
self.error.color = ft.Colors.RED
self.error.update()
except Exception as e:
print("Error saving tenant data:", e)
def on_search_btn_click(self, e):
value = self.search_field.value
print(f'Search for {value}')
buffer = []
for tenant in self.all_tenants:
if value in tenant['name']:
buffer.append(tenant)
self.tenants_list.controls.clear()
self.tenants_list.controls = self.create_list(buffer, self.on_tenant_btn_click)
self.tenants_list.update()
def on_tenant_btn_click(self, item):
self.error.value = ''
self.error.color = ft.Colors.RED
self.error.update()
self.name.value = item['name']
self.name.update()
self.address.value = item['address']
self.address.update()
self.contact_name.value = item['contact_name']
self.contact_name.update()
self.email.value = item['email']
self.email.update()
self.phone.value = item['phone']
self.phone.update()
self.first_order_number.value = item['first_order_number']
self.first_order_number.update()
self.register_number.value = item['register_number']
self.register_number.update()
self.terms.value = item['terms']
self.terms.update()
self.logo_filename.value = item['logo_filename']
self.logo_filename.update()
self.user_id = item['id']
def get_all_tenants(self):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/admin/users", headers=headers)
return response.json() if response.status_code == 200 else []
except Exception as e:
print("Error loading clients:", e)
def create_list(self, items, on_click_handler):
"""Helper to create list items for a column."""
return [
ft.Container(
content=ft.Row(
[
ft.Text(item['name'], expand=True),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
),
width=300,
bgcolor=ft.Colors.BLUE_50,
padding=10,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
ink=True, # To enable click effect
on_click=lambda e, id=item: on_click_handler(id), # Attach the click handler
)
for item in items
]
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Text("Tenants", size=24, weight=ft.FontWeight.BOLD),
ft.Column(
[
ft.Row(
[
self.search_field,
ft.Button(
"Search",
icon=ft.Icons.SEARCH,
width=100,
on_click=self.on_search_btn_click
)
]
),
]
),
ft.Row(
[
self.tenants_list,
self.user_details
],
vertical_alignment=ft.CrossAxisAlignment.START
)
],
scroll=ft.ScrollMode.ADAPTIVE
),
expand=True
)

View File

@@ -0,0 +1,126 @@
import flet as ft
import requests
from pages.view_orders_in_page import ViewOrdersIn
from config import API_BASE_URL
class ArchiveInPage:
def __init__(self, page: ft.Page, dashboard, orders_in):
self.page = page
self.dashboard = dashboard
self.orders_in = orders_in
self.orders = []
self.orders_list = ft.Column(spacing=10, expand=True, scroll=ft.ScrollMode.ADAPTIVE,)
self.selected_delete_order = None
self.delete_dialog = ft.AlertDialog(
title="Delete order?",
actions=[
ft.Button(
"Yes",
width=100,
on_click=self.on_confirm_delete_order_click
),
ft.FilledButton(
"No",
bgcolor=ft.Colors.GREY,
width = 100,
on_click=self.on_no_delete_order_btn_click
)
]
)
def on_confirm_delete_order_click(self, e):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(f"{API_BASE_URL}/orders_in/{self.selected_delete_order['id']}", headers=headers)
if response.status_code == 200:
print('Order deleted!')
except Exception as e:
print("Error loading orders:", e)
self.page.close(self.delete_dialog)
self.selected_delete_order = None
self.refresh()
def on_no_delete_order_btn_click(self, e):
self.page.close(self.delete_dialog)
self.selected_delete_order = None
def view_order(self, order):
if order:
self.page.session.set("order_in_id", order['id'])
self.view_order_page = ViewOrdersIn(self.page, self, self.dashboard)
self.dashboard.placeholder.content = self.view_order_page.build()
self.dashboard.placeholder.update()
def cancel_order(self, order):
self.selected_delete_order = order
self.page.open(self.delete_dialog)
def load_orders(self):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/orders_in/list", headers=headers)
return response.json() if response.status_code == 200 else []
except Exception as e:
print("Error loading orders:", e)
def on_go_back_btn_click(self, e):
self.dashboard.placeholder.content = self.orders_in.build()
self.dashboard.placeholder.update()
def get_client(self, client_id):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/clients/{client_id}", headers=headers)
if response.status_code == 200:
client = response.json()
return client['name']
else:
return None
except Exception as e:
print("Error loading clients:", e)
return None
def refresh(self):
self.orders = self.load_orders()
self.orders_list.controls.clear()
for order in self.orders:
client = self.get_client(order['client_id'])
self.orders_list.controls.append(
ft.Container(
content=ft.Row([
ft.Column([
ft.Text(f"{client}", size=16, weight=ft.FontWeight.BOLD),
ft.Text(f"Order Number: {order['order_number']}", size=14),
], expand=True),
ft.Button("View",icon=ft.Icons.PICTURE_AS_PDF, on_click=lambda e, o=order: self.view_order(o)),
ft.Button("Delete", icon=ft.Icons.CANCEL, on_click=lambda e, o=order: self.cancel_order(o))
]),
padding=10,
border=ft.border.all(1, ft.Colors.GREY_300),
bgcolor=ft.Colors.BLUE_50,
border_radius=10,
)
)
self.page.update()
def build(self):
self.refresh()
return ft.Container(
content=ft.Column(
[
ft.Row(
[
ft.Text("Archive", size=24, weight=ft.FontWeight.BOLD),
ft.Button("Back", icon=ft.Icons.ARROW_BACK_IOS_NEW, on_click=self.on_go_back_btn_click)
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
),
self.orders_list
]
)
)

View File

@@ -0,0 +1,125 @@
import flet as ft
import requests
from pages.view_page import ViewPage
from pages.orders_edit_page import OrdersEditPage
from config import API_BASE_URL
class ArchivePage:
def __init__(self, page:ft.Page, dashboard, order_page):
self.page = page
self.dashboard = dashboard
self.orders = []
self.orders_list = ft.Column(spacing=10, expand=True, scroll=ft.ScrollMode.ADAPTIVE,)
self.order_page = order_page
self.selected_cancel_order = None
self.cancel_dialog = ft.AlertDialog(
title="Cancel order?",
actions=[
ft.Button(
"Yes",
width=100,
on_click=self.on_confirm_calcel_order_click
),
ft.FilledButton(
"No",
bgcolor=ft.Colors.GREY,
width = 100,
on_click=self.on_no_cancel_order_btn_click
)
]
)
def refresh(self):
self.orders = self.load_orders()
self.orders_list.controls.clear()
for order in self.orders:
self.orders_list.controls.append(
ft.Container(
content=ft.Row([
ft.Column([
ft.Text(f"{order['order_number']}", size=16, weight=ft.FontWeight.BOLD),
], expand=True),
ft.Button("View",icon=ft.Icons.PICTURE_AS_PDF, on_click=lambda e, o=order: self.view_order(o)),
*([
ft.Button("Edit", icon=ft.Icons.EDIT, on_click=lambda e, o=order: self.edit_order(o)),
ft.Button("Cancel", icon=ft.Icons.CANCEL, on_click=lambda e, o=order: self.cancel_order(o))
] if order['status'] == 'active' else [])
]),
padding=10,
border=ft.border.all(1, ft.Colors.GREY_300),
bgcolor=ft.Colors.BLUE_50,
border_radius=10,
)
)
self.page.update()
def load_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 view_order(self, order):
#print(order)
user_id = self.page.session.get("user_id")
pdf_name = f'order_{user_id}_{order['order_number']}.pdf'
#print(pdf_name)
view_page = ViewPage(self.page, pdf_name, self.order_page, self.dashboard, order['id'])
self.dashboard.placeholder.content = view_page.build()
self.dashboard.placeholder.update()
def edit_order(self, order):
self.page.session.set('order_id', order['id'])
edit_order = OrdersEditPage(self.page, self.dashboard, self)
self.dashboard.placeholder.content = edit_order.build()
self.dashboard.placeholder.update()
def cancel_order(self, order):
self.selected_cancel_order = order
self.page.open(self.cancel_dialog)
def on_confirm_calcel_order_click(self, e):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(
f"{API_BASE_URL}/orders/cancel/{self.selected_cancel_order['id']}",
headers=headers
)
if response.status_code != 200:
print("Failed to cancel order:", response.status_code)
except Exception as e:
print("Error cancelling order:", e)
self.page.close(self.cancel_dialog)
self.selected_cancel_order = None
self.refresh() # Optional: Refresh the list
def on_no_cancel_order_btn_click(self, e):
self.selected_cancel_order = None
self.page.close(self.cancel_dialog)
def on_go_back_btn_click(self, e):
self.dashboard.placeholder.content = self.order_page.build()
self.dashboard.placeholder.update()
def build(self):
self.refresh()
return ft.Container(
content=ft.Column(
[
ft.Row(
[
ft.Text("Archive", size=24, weight=ft.FontWeight.BOLD),
ft.Button("Back", icon=ft.Icons.ARROW_BACK_IOS_NEW, on_click=self.on_go_back_btn_click)
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
),
self.orders_list
]
)
)

View File

@@ -0,0 +1,73 @@
# client/pages/login_page.py
import flet as ft
from pages.login_page import Login
class Auth:
def __init__(self, page: ft.Page):
self.page = page
self.email = ft.TextField(label="Email")
self.passwd = ft.TextField(label="Password", password=True, can_reveal_password=True)
self.error_message = ft.Text("")
self.welcome_message = ft.Text(
"",
weight=ft.FontWeight.BOLD,
size=50,
color= ft.Colors.WHITE
)
# if self.page.client_storage.get("logo_filename"):
# logo_path = f'images/{self.page.client_storage.get("logo_filename")}'
# else:
# print(f'filename: {self.page.client_storage.get("logo_filename")}')
logo_path = "images/truck_logo.png"
self.logo = ft.Image(
src=logo_path,
width=400,
border_radius=20
)
self.subtitle = ft.Text(
"From Order to Action, Instantly.",
size=20,
color=ft.Colors.WHITE70
)
login = Login(self.page, self)
self.placeholder = ft.Container(
content=login.build(),
padding=10,
border_radius=10,
alignment=ft.alignment.center,
expand=5
)
def build(self):
return ft.Container(
content=ft.Row(
[
ft.Container(
content = ft.Column(
[
self.logo,
self.subtitle
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER
),
expand=5,
gradient=ft.RadialGradient(
center=ft.Alignment(-0.50, -0.50),
radius=1.0,
colors=[ft.Colors.BLUE_300, ft.Colors.BLUE_900],
tile_mode=ft.GradientTileMode.CLAMP
),
shape=ft.BoxShape.CIRCLE,
margin=ft.margin.only(left=-180, top=-180)
),
self.placeholder,
]
),
alignment=ft.alignment.center,
expand=True,
padding=0,
margin=0,
)

View File

@@ -0,0 +1,294 @@
import flet as ft
import requests
from config import API_BASE_URL
class ClientsPage:
def __init__(self, page: ft.Page, dashboard):
self.page = page
self.dashboard = dashboard
self.clients = []
self.dialog = None
self.name = ft.TextField(
label="Name",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.address = None
self.street_and_number = ft.TextField(
label="Street and number",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.postal_code = ft.TextField(
label="Postal code",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.city = ft.TextField(
label="City",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.region_county = ft.TextField(
label="Region / County",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.country = ft.TextField(
label="Country",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.register_number = ft.TextField(
label="Register Number",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.vat = ft.TextField(
label="VAT",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.contact_person = ft.TextField(
label="Contact Person",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.phone = ft.TextField(
label="Phone",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.email = ft.TextField(
label="Email",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.selected_client_id = None
self.clients_list = ft.ListView(expand=True, spacing=10)
self.dialog = ft.AlertDialog(
modal=True,
title=ft.Text("Client Details"),
content=ft.Column(
controls=[
self.name,
self.register_number,
self.vat,
self.contact_person,
self.phone,
self.email,
self.street_and_number,
self.postal_code,
self.city,
self.region_county,
self.country,
],
width=600
),
actions=[
ft.TextButton("Cancel", on_click=self.on_cancel_btn_click),
ft.ElevatedButton("Save", on_click=self.submit_client)
]
)
self.confirm_dialog = ft.AlertDialog(
modal=True,
title=ft.Text("Delete Client"),
content=ft.Text("Are you sure you want to delete this client?"),
actions=[
ft.TextButton("Cancel", on_click=lambda e: self.page.close(self.confirm_dialog)),
ft.ElevatedButton("Delete", on_click=self.confirm_delete)
]
)
self.client_id_to_delete = None
self.subscription_error = ft.Text("Please subscribe to add new client", color=ft.Colors.RED)
def load_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)
if response.status_code == 200:
self.clients = response.json()
self.refresh_clients_list()
except Exception as e:
print("Error loading clients:", e)
def refresh_clients_list(self):
self.clients_list.controls.clear()
for client in self.clients:
self.clients_list.controls.append(
ft.Container(
content=ft.Row(
[
ft.Column([
ft.Text(client["name"], size=16, weight=ft.FontWeight.BOLD),
ft.Text(client["register_number"], size=12, italic=True)
], expand=True),
ft.Row([
ft.IconButton(icon=ft.Icons.EDIT, on_click=lambda e, c=client: self.open_dialog(c)),
ft.IconButton(icon=ft.Icons.DELETE, on_click=lambda e, cid=client["id"]: self.delete_client(cid)),
])
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
),
padding=10,
border=ft.border.all(1, ft.Colors.GREY_300),
border_radius=10,
bgcolor=ft.Colors.BLUE_50,
expand=True
)
)
self.page.update()
def open_dialog(self, client=None):
if client:
self.selected_client_id = client["id"]
self.name.value = client["name"]
self.street_and_number.value = client["address"].split(" %")[0]
self.postal_code.value = client["address"].split(" %")[1]
self.city.value = client["address"].split(" %")[2]
self.region_county.value = client["address"].split(" %")[3]
self.country.value = client["address"].split(" %")[4]
self.register_number.value = client["register_number"]
self.contact_person.value = client["contact_person"]
self.phone.value = client["phone"]
self.email.value = client["email"]
self.vat.value = client['vat']
else:
self.selected_client_id = None
self.name.value = ""
self.street_and_number.value = ""
self.postal_code.value = ""
self.city.value = ""
self.region_county.value = ""
self.country.value = ""
self.register_number.value = ""
self.contact_person.value = ""
self.phone.value = ""
self.email.value = ""
self.vat.value = ""
self.page.open(self.dialog)
self.page.update()
def submit_client(self, e):
user_id = self.page.session.get("user_id")
address = f'{self.street_and_number.value} %{self.postal_code.value} %{self.city.value} %{self.region_county.value} %{self.country.value}'
client_data = {
"name": self.name.value,
"address": address,
"register_number": self.register_number.value,
"vat": self.vat.value,
"contact_person": self.contact_person.value,
"phone": self.phone.value,
"email": self.email.value,
"user_id": user_id,
}
try:
headers = {"Authorization": f"Bearer {self.page.client_storage.get('token')}"}
if self.selected_client_id:
requests.put(f"{API_BASE_URL}/clients/{self.selected_client_id}", json=client_data, headers=headers)
else:
requests.post(f"{API_BASE_URL}/clients/", json=client_data, headers=headers)
self.page.close(self.dialog)
self.load_clients()
self.page.snack_bar = ft.SnackBar(ft.Text("Client saved successfully."))
self.page.snack_bar.open = True
self.page.update()
except Exception as e:
print("Error submitting client:", e)
def on_cancel_btn_click(self, e):
self.page.close(self.dialog)
def delete_client(self, client_id):
self.client_id_to_delete = client_id
self.page.open(self.confirm_dialog)
self.page.update()
def confirm_delete(self, e):
try:
headers = {"Authorization": f"Bearer {self.page.client_storage.get('token')}"}
requests.delete(f"{API_BASE_URL}/clients/{self.client_id_to_delete}", headers=headers)
self.page.close(self.confirm_dialog)
self.load_clients()
except Exception as ex:
print("Error deleting client:", ex)
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.load_clients()
self.subscription = self.get_current_subscription_plan()
self.add_client_btn = ft.ElevatedButton("New Client", icon=ft.Icons.ADD, on_click=lambda e: self.open_dialog())
self.header = ft.Row(
[
ft.Text("Clients", size=24, weight=ft.FontWeight.BOLD),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
)
if self.subscription:
if self.subscription['status'] != 'expired':
self.header.controls.append(self.add_client_btn)
else:
self.header.controls.append(self.subscription_error)
else:
self.header.controls.append(self.subscription_error)
return ft.Container(
content=ft.Column(
[
self.header,
self.clients_list
],
expand=True
),
expand=True,
)

View 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
)

View File

@@ -0,0 +1,233 @@
import flet as ft
import requests
from config import API_BASE_URL
class DestinationsPage:
def __init__(self, page: ft.Page, dashboard):
self.page = page
self.dashboard = dashboard
self.destinations = []
self.destinations_column = ft.Column(expand=True, spacing=10, scroll=ft.ScrollMode.ADAPTIVE,)
self.dialog = None
self.delete_alert = None
self.current_edit = None
self.subscription_error = ft.Text("Please subscribe to add new destination", color=ft.Colors.RED)
def refresh(self):
self.destinations_column.controls.clear()
token = self.page.client_storage.get("token")
if not token:
print("Missing token.")
return
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/destinations/", headers=headers)
if response.status_code == 200:
self.destinations = response.json()
for destination in self.destinations:
street_and_number = destination["address"].split(" %")[0]
postal_code = destination["address"].split(" %")[1]
city = destination["address"].split(" %")[2]
region_county = destination["address"].split(" %")[3]
country = destination["address"].split(" %")[4]
address = ''
if len(street_and_number) > 0:
address += street_and_number +', '
if len(postal_code) > 0:
address += postal_code +', '
if len(city) > 0:
address += city +', '
if len(region_county) > 0:
address += region_county +', '
if len(country) > 0:
address += country
self.destinations_column.controls.append(
ft.Container(
content=ft.Row([
ft.Column(
[
ft.Text(destination["name"], weight = ft.FontWeight.BOLD),
ft.Text(address)
]
),
ft.Row(
[
ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click= lambda e, d=destination: self.open_location(d)),
ft.IconButton(icon=ft.Icons.EDIT, on_click=lambda e, d=destination: self.open_dialog(d)),
ft.IconButton(icon=ft.Icons.DELETE, on_click=lambda e, d=destination: self.confirm_delete(d)),
]
)
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
padding=10,
bgcolor=ft.Colors.BLUE_50,
border=ft.border.all(1, ft.Colors.GREY_300),
border_radius=10,
)
)
self.page.update()
def open_dialog(self, destination=None):
self.current_edit = destination
name = ft.TextField(
label="Name",
value=destination["name"] if destination else "",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
street_and_number = ft.TextField(
label="Street and number",
value=destination["address"].split("%")[0] if destination else "",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
postal_code = ft.TextField(
label="Postal code",
value=destination["address"].split("%")[1] if destination else "",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
city = ft.TextField(
label="City",
value=destination["address"].split("%")[2] if destination else "",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
region_county = ft.TextField(
label="Region / County",
value=destination["address"].split("%")[3] if destination else "",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
country = ft.TextField(
label="Country",
value=destination["address"].split("%")[4] if destination else "",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
def save_action(e):
address = f'{street_and_number.value} %{postal_code.value} %{city.value} %{region_county.value} %{country.value}'
self.save_destination(name.value, address)
self.dialog = ft.AlertDialog(
modal=True,
title=ft.Text("Edit Address" if destination else "Add Address"),
content=ft.Column(
[
name,
street_and_number,
postal_code,
city,
region_county,
country
],
width=400,
height=400
),
actions=[
ft.TextButton("Cancel", on_click=lambda e: self.page.close(self.dialog)),
ft.ElevatedButton(
"Save",
on_click=save_action
)
],
)
self.page.dialog = self.dialog
self.page.open(self.dialog)
def save_destination(self, name, address):
token = self.page.client_storage.get("token")
user_id = self.page.session.get("user_id")
if not token or not user_id:
print("Missing token or user_id.")
return
headers = {"Authorization": f"Bearer {token}"}
data = {"name": name, "address": address, "user_id": user_id}
if self.current_edit:
url = f"{API_BASE_URL}/destinations/{self.current_edit['id']}"
requests.put(url, json=data, headers=headers)
else:
requests.post(f"{API_BASE_URL}/destinations/", json=data, headers=headers)
self.page.close(self.dialog)
self.refresh()
def confirm_delete(self, destination):
def delete_action(e):
self.delete_destination(destination["id"])
self.delete_alert = ft.AlertDialog(
modal=True,
title=ft.Text("Confirm Deletion"),
content=ft.Text(f"Are you sure you want to delete destination: {destination['name']}?"),
actions=[
ft.TextButton("Cancel", on_click=lambda e: self.page.close(self.delete_alert)),
ft.TextButton("Delete", on_click=delete_action)
]
)
self.page.dialog = self.delete_alert
self.page.open(self.delete_alert)
def delete_destination(self, id):
token = self.page.client_storage.get("token")
if not token:
print("Missing token.")
return
headers = {"Authorization": f"Bearer {token}"}
requests.delete(f"{API_BASE_URL}/destinations/{id}", headers=headers)
self.page.close(self.delete_alert)
self.refresh()
def open_location(self, destination):
query = destination["address"].replace(" ", "+").replace(" %", "").replace(",", "")
maps_url = f"https://www.google.com/maps/search/?api=1&query={query}"
self.page.launch_url(maps_url)
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.refresh()
self.add_destination_btn = ft.ElevatedButton("Add Destination", icon=ft.Icons.ADD, on_click=lambda e: self.open_dialog())
self.headers = ft.Row([
ft.Text("Adderess", size=24, weight=ft.FontWeight.BOLD),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
)
subscription = self.get_current_subscription_plan()
if subscription:
if subscription['status'] != 'expired':
self.headers.controls.append(self.add_destination_btn)
else:
self.headers.controls.append(self.subscription_error)
else:
self.headers.controls.append(self.subscription_error)
return ft.Column(
[
self.headers,
self.destinations_column
],
expand=True,
)

View File

@@ -0,0 +1,62 @@
import flet as ft
import re
import requests
from config import API_BASE_URL
class ForgotPassword:
def __init__(self, page: ft.Page, auth, login):
self.page = page
self.auth = auth
self.login = login
self.email = ft.TextField(label="Email")
self.error = ft.Text("", color=ft.Colors.RED)
self.success = ft.Text("", color=ft.Colors.GREEN)
self.back_button = ft.TextButton(text="Back to Login", on_click=self.on_back_clicked)
def on_submit(self, e):
email_value = self.email.value.strip()
email_regex = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$"
if not re.match(email_regex, email_value):
self.error.value = "Please enter a valid email address."
self.success.value = ""
else:
try:
response = requests.post(
f"{API_BASE_URL}/auth/forgot_password",
json={"email": email_value},
timeout=10
)
if response.status_code == 200:
self.success.value = "If the email exists, you will receive a reset link shortly."
self.error.value = ""
else:
self.error.value = "Server error. Please try again later."
self.success.value = ""
except Exception as ex:
self.error.value = "Connection error. Please check your internet."
self.success.value = ""
print(ex)
self.page.update()
def on_back_clicked(self, e):
self.auth.placeholder.content.clean()
self.auth.placeholder.content = self.login.build()
self.auth.placeholder.update()
def build(self):
return ft.Column(
[
ft.Text("Forgot Password", size=30, weight="bold"),
self.email,
ft.ElevatedButton("Reset Password", on_click=self.on_submit, width=150),
self.error,
self.success,
self.back_button,
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER,
spacing=20,
width=350
)

View File

@@ -0,0 +1,105 @@
import flet as ft
from pages.register_page import Register
from pages.forgot_password_page import ForgotPassword
from pages.two_factor_page import TwoFactorAuth
import requests
from config import API_BASE_URL
class Login:
def __init__(self, page: ft.Page, auth):
self.page = page
self.auth = auth
self.email = ft.TextField(label="Email")
self.passwd = ft.TextField(label="Password", password=True, can_reveal_password=True)
self.error_message = ft.Text("", color=ft.Colors.RED)
def on_login_click(self, e):
self.error_message.value = ""
self.error_message.update()
self.page.update()
email = self.email.value.strip()
password = self.passwd.value.strip()
if not email or not password:
self.error_message.value = "Please enter both email and password."
self.error_message.update()
return
# self.auth.placeholder.content.clean()
# two_factor = TwoFactorAuth(self.page, email, self, self.auth)
# self.auth.placeholder.content = two_factor.build()
# self.auth.placeholder.update()
try:
response = requests.post(
f"{API_BASE_URL}/auth/login",
json={"email": email, "password": password},
timeout=10
)
if response.status_code == 200:
user_info = requests.get(
f"{API_BASE_URL}/auth/me",
headers={"Authorization": f"Bearer {response.json().get('access_token', '')}"}
)
if user_info.status_code == 200:
logo_filename = user_info.json().get("logo_filename", "")
if logo_filename:
self.page.client_storage.set("custom_logo", logo_filename)
self.auth.placeholder.content.clean()
two_factor = TwoFactorAuth(self.page, email, self, self.auth)
self.auth.placeholder.content = two_factor.build()
self.auth.placeholder.update()
else:
self.error_message.value = response.json().get("error", "Login failed")
self.error_message.update()
except Exception as ex:
self.error_message.value = f"Login error: {str(ex)}"
self.error_message.update()
def on_forgot_btn_click(self, e):
self.auth.placeholder.content.clean()
forgot_passwd = ForgotPassword(self.page, self.auth, self)
self.auth.placeholder.content = forgot_passwd.build()
self.auth.placeholder.update()
def on_register_btn_click(self, e):
print('Go to register')
register = Register(self.page, self.auth, self)
self.auth.placeholder.content.clean()
self.auth.placeholder.content = register.build()
self.auth.placeholder.update()
def build(self):
return ft.Column(
[
ft.Text(
"Login",
size=30,
weight="bold"
),
self.email,
self.passwd,
ft.ElevatedButton(
"Login",
width = 150,
on_click=self.on_login_click
),
self.error_message,
ft.Row(
[
ft.TextButton("Forgot Password?", on_click=self.on_forgot_btn_click),
ft.TextButton("Register", on_click=self.on_register_btn_click)
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
)
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER,
spacing=20,
width=350
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
import flet as ft
from pages.orders_in_page import OrdersInPage
from pages.orders_out_page import OrdersOutPage
class OrdersPage:
def __init__(self, page: ft.Page, dashboard):
self.page = page
self.dashboard = dashboard
def on_orders_in_btn_click(self, e):
order_in_page = OrdersInPage(self.page, self.dashboard)
self.dashboard.placeholder.content = order_in_page.build()
self.dashboard.placeholder.update()
def on_orders_out_btn_click(self, e):
orders_out_page = OrdersOutPage(self.page, self.dashboard)
self.dashboard.placeholder.content = orders_out_page.build()
self.dashboard.placeholder.update()
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Text("Orders", size=24, weight=ft.FontWeight.BOLD),
ft.Row(
[
ft.Container(
content=ft.Column(
[
ft.Icon(ft.Icons.ARROW_CIRCLE_DOWN, size=150),
ft.Container(
ft.Row(
[
ft.Text("Incoming orders", size=20)
],
alignment=ft.MainAxisAlignment.CENTER
),
bgcolor=ft.Colors.BLUE_200,
width=250,
height=80
),
ft.FilledButton(
"Orders In",
on_click=self.on_orders_in_btn_click,
width=150
)
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER
),
border=ft.border.all(1, ft.Colors.GREY_300),
bgcolor=ft.Colors.BLUE_50,
padding = ft.padding.symmetric(vertical=20),
width=250,
height=350,
border_radius=20
),
ft.Container(
content=ft.Column(
[
ft.Icon(ft.Icons.ARROW_CIRCLE_UP, size=150),
ft.Container(
ft.Row(
[
ft.Text("Outcoming orders", size=20)
],
alignment=ft.MainAxisAlignment.CENTER
),
bgcolor=ft.Colors.BLUE_200,
width=250,
height=80
),
ft.FilledButton(
"Orders Out",
on_click=self.on_orders_out_btn_click,
width=150
)
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER
),
border=ft.border.all(1, ft.Colors.GREY_300),
bgcolor=ft.Colors.BLUE_50,
padding = ft.padding.symmetric(vertical=20),
width=250,
height=350,
border_radius=20
)
],
alignment=ft.MainAxisAlignment.CENTER,
spacing=50
),
ft.Text(" ")
],
expand=True,
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
),
expand=True
)

View File

@@ -0,0 +1,458 @@
import flet as ft
import requests
import time
from config import API_BASE_URL
class ProfilePage:
def __init__(self, page: ft.Page, dashboard):
self.page = page
self.dashboard = dashboard
self.name_field = ft.TextField(
label="Company Name",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.contact_name_field = ft.TextField(
label="Contact Name",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.email_field = ft.TextField(
label="Email",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.phone_field = ft.TextField(
label="Phone",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.register_number_field = ft.TextField(
label="Register Number",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.vat = ft.TextField(
label="VAT",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.address = None
self.street_and_number = ft.TextField(
label="Street and number",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.postal_code = ft.TextField(
label="Postal code",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.city = ft.TextField(
label="City",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.region_county = ft.TextField(
label="Region / County",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.country = ft.TextField(
label="Country",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.logo_field = ft.TextField(
label="Logo Filename",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.terms_field = ft.TextField(
label="Terms",
multiline=True,
min_lines=5,
max_lines=10,
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.first_order_number_field = ft.TextField(
label="First Order Number",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.created_at_text = ft.Text(value="Created At: TBD") # Set dynamically later
self.edit_button = ft.ElevatedButton(text="Edit Profile", on_click=self.on_edit_click)
self.save_button = ft.ElevatedButton(text="Save Changes", visible=False, on_click=self.on_save_click)
self.cancel_button = ft.TextButton(text="Cancel", visible=False, on_click=self.on_cancel_click)
self.upload_logo_btn = ft.ElevatedButton("Upload Company Logo", icon=ft.Icons.UPLOAD, on_click=self.on_upload_click, disabled=True)
self.message = ft.Text()
self.logo = ft.Image(src="images/image_placeholder.png", width=250)
self.file_picker = ft.FilePicker(on_result=self.on_file_result)
self.page.overlay.append(self.file_picker)
#email credentials
self.email_credentials = None
self.smtp_host = ft.TextField(
label="SMTP HOST",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.smtp_port = ft.TextField(
label="SMTP PORT",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.smtp_user = ft.TextField(
label="Email (SMTP USER)",
disabled=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
def _auth_headers(self):
"""Build Authorization header from client storage robustly (web/desktop)."""
t = self.page.client_storage.get("token")
if not t:
return {}
# Token may be stored as dict or quoted string depending on login flow/runtime
if isinstance(t, dict):
t = t.get("access_token") or t.get("token") or ""
if not isinstance(t, str):
t = str(t)
t = t.strip().strip('"')
return {"Authorization": f"Bearer {t}"} if t else {}
def on_edit_click(self, e):
self.name_field.disabled = False
self.contact_name_field.disabled = False
self.email_field.disabled = False
self.phone_field.disabled = False
self.register_number_field.disabled = False
self.vat.disabled = False
self.street_and_number.disabled = False
self.postal_code.disabled = False
self.city.disabled = False
self.region_county.disabled = False
self.country.disabled = False
self.logo_field.disabled = False
self.terms_field.disabled = False
self.first_order_number_field.disabled = False
self.upload_logo_btn.disabled = False
self.smtp_host.disabled = False
self.smtp_port.disabled = False
self.smtp_user.disabled = False
self.edit_button.visible = False
self.save_button.visible = True
self.cancel_button.visible = True
self.page.update()
def on_save_click(self, e):
# Save logic would be implemented here
headers = self._auth_headers()
if not headers:
self.message.value = "Unauthorized: No token"
self.message.color = ft.Colors.RED
self.page.update()
return
address = f'{self.street_and_number.value} %{self.postal_code.value} %{self.city.value} %{self.region_county.value} %{self.country.value}'
data = {
"name": self.name_field.value,
"contact_name": self.contact_name_field.value,
"email": self.email_field.value,
"phone": self.phone_field.value,
"register_number": self.register_number_field.value,
"vat": self.vat.value,
"address": address,
"logo_filename": self.logo_field.value,
"terms": self.terms_field.value,
"first_order_number": self.first_order_number_field.value
}
try:
response = requests.put(f"{API_BASE_URL}/profile/", json=data, headers=headers)
if response.status_code == 200:
self.message.value = "Profile updated successfully!"
self.message.color = ft.Colors.GREEN
# Save logo filename to client storage
self.page.client_storage.set("logo_filename", self.logo_field.value)
self.page.session.set("first_order_number", self.first_order_number_field.value)
else:
self.message.value = f"Failed to update profile: {response.status_code}"
self.message.color = ft.Colors.RED
except Exception as e:
self.message.value = f"Error updating profile: {e}"
self.message.color = ft.Colors.RED
#email credentials
data = {
'smtp_host' : self.smtp_host.value,
'smtp_port' : self.smtp_port.value,
'smtp_user' : self.smtp_user.value
}
try:
if not self.email_credentials:
response = requests.post(f"{API_BASE_URL}/profile/email", json=data, headers=headers)
else:
response = requests.put(f"{API_BASE_URL}/profile/email", json=data, headers=headers)
if response.status_code == 200:
self.message.value = "Profile updated successfully!"
self.message.color = ft.Colors.GREEN
else:
self.message.value = f"Failed to create / update email credentials: {response.status_code}"
self.message.color = ft.Colors.RED
except Exception as e:
self.message.value = f"Error creating / updating email credentials: {e}"
self.message.color = ft.Colors.RED
self.name_field.disabled = True
self.contact_name_field.disabled = True
self.email_field.disabled = True
self.phone_field.disabled = True
self.register_number_field.disabled = True
self.vat.disabled = True
self.street_and_number.disabled = True
self.postal_code.disabled = True
self.city.disabled = True
self.region_county.disabled = True
self.country.disabled = True
self.logo_field.disabled = True
self.terms_field.disabled = True
self.first_order_number_field.disabled = True
self.upload_logo_btn.disabled = True
self.smtp_host.disabled = True
self.smtp_port.disabled = True
self.smtp_user.disabled = True
self.edit_button.visible = True
self.save_button.visible = False
self.cancel_button.visible = False
self.page.update()
def on_cancel_click(self, e):
# Reset fields or fetch previous values here
self.name_field.disabled = True
self.contact_name_field.disabled = True
self.email_field.disabled = True
self.phone_field.disabled = True
self.register_number_field.disabled = True
self.vat.disabled = True
self.street_and_number.disabled = True
self.postal_code.disabled = True
self.city.disabled = True
self.region_county.disabled = True
self.country.disabled = True
self.logo_field.disabled = True
self.terms_field.disabled = True
self.first_order_number_field.disabled = True
self.upload_logo_btn.disabled = True
self.smtp_host.disabled = True
self.smtp_port.disabled = True
self.smtp_user.disabled = True
self.edit_button.visible = True
self.save_button.visible = False
self.cancel_button.visible = False
self.page.update()
def populate_user_data(self):
user_id = self.page.session.get("user_id")
if not user_id:
self.message.value = "User not authenticated."
self.message.color = ft.Colors.RED
return
headers = self._auth_headers()
if not headers:
self.message.value = "Unauthorized: No token"
self.message.color = ft.Colors.RED
return
response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10)
if response.status_code == 200:
user_data = response.json()
#print(user_data)
self.name_field.value = user_data.get("name", "")
self.contact_name_field.value = user_data.get("contact_name", "")
self.email_field.value = user_data.get("email", "")
self.phone_field.value = user_data.get("phone", "")
self.register_number_field.value = user_data.get("register_number", "")
self.vat.value = user_data.get("vat", "")
self.street_and_number.value = user_data.get("address").split(" %")[0] if user_data.get("address") and len(user_data.get("address")) > 0 else ''
self.postal_code.value = user_data.get("address").split(" %")[1] if user_data.get("address") and len(user_data.get("address")) > 0 else ''
self.city.value = user_data.get("address").split(" %")[2] if user_data.get("address") and len(user_data.get("address")) > 0 else ''
self.region_county.value = user_data.get("address").split(" %")[3] if user_data.get("address") and len(user_data.get("address")) > 0 else ''
self.country.value = user_data.get("address").split(" %")[4] if user_data.get("address") and len(user_data.get("address")) > 0 else ''
logo_filename = user_data.get("logo_filename", "")
self.logo_field.value = logo_filename
self.logo.src = f"images/{logo_filename}" if logo_filename else "images/image_placeholder.png"
if logo_filename:
self.page.client_storage.set("logo_filename", logo_filename)
self.dashboard.logo.src = f"images/{logo_filename}"
self.dashboard.logo.update()
self.terms_field.value = user_data.get("terms", "")
self.first_order_number_field.value = user_data.get("first_order_number", "")
self.created_at_text.value = f"Created At: {user_data.get('created_at', '')}"
if self.register_number_field.value == '':
self.on_edit_click('')
else:
self.message.value = f"Failed to load user data: {response.text}"
self.message.color = ft.Colors.RED
response = requests.get(f"{API_BASE_URL}/profile/email", headers=headers, timeout=10)
if response.status_code == 200:
self.email_credentials = response.json()
self.smtp_host.value = self.email_credentials['smtp_host']
self.smtp_port.value = self.email_credentials['smtp_port']
self.smtp_user.value = self.email_credentials['smtp_user']
else:
print(f"Failed to load email credentials: {response.text}")
#except Exception as e:
# self.message.value = f"Error fetching user data: {e}"
# self.message.color = ft.Colors.RED
def on_upload_click(self, e):
self.file_picker.pick_files(
allow_multiple=False,
allowed_extensions=["png", "jpg", "jpeg"]
)
def on_file_result(self, e: ft.FilePickerResultEvent):
if not e.files:
return
file = e.files[0]
new_filename = f"user_logo_{self.page.session.get('user_id')}_{file.name}"
upload_url = self.page.get_upload_url(new_filename, 1000)
self.file_picker.upload([ft.FilePickerUploadFile(file.name, upload_url=upload_url)])
import os
import shutil
source_path = os.path.join("uploads", new_filename)
destination_path = os.path.join("assets/images", new_filename)
try:
time.sleep(2)
shutil.move(source_path, destination_path)
self.logo.src = f"images/{new_filename}"
self.logo.update()
self.logo_field.value = new_filename
self.page.update()
self.page.client_storage.set("logo_filename", new_filename)
self.dashboard.logo.src = f"images/{new_filename}"
self.dashboard.logo.update()
except Exception as err:
self.message.value = f"Upload error: {err}"
self.message.color = ft.Colors.RED
self.page.update()
def build(self):
self.populate_user_data()
return ft.Container(
content=ft.Column(
[
ft.Text("User Profile", size=24, weight=ft.FontWeight.BOLD),
ft.Row(
[
ft.Column(
[
self.logo,
self.upload_logo_btn,
self.message,
ft.Text(),
self.smtp_user,
self.smtp_host,
self.smtp_port,
ft.Row([self.edit_button, self.save_button, self.cancel_button])
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
width=250
),
ft.Column(
[
self.name_field,
self.contact_name_field,
self.email_field,
self.phone_field,
self.register_number_field,
self.vat,
self.street_and_number,
self.postal_code,
self.city,
self.region_county,
self.country,
self.first_order_number_field,
self.terms_field,
],
spacing=20,
expand=True
)
],
vertical_alignment=ft.CrossAxisAlignment.START
)
],
scroll=ft.ScrollMode.ADAPTIVE
),
)

View File

@@ -0,0 +1,83 @@
import re
import flet as ft
import requests
import time
from config import API_BASE_URL
class Register:
def __init__(self, page: ft.Page, auth, login):
self.page = page
self.auth = auth
self.login = login
self.email = ft.TextField(label="Email", width=300)
self.password = ft.TextField(label="Password", password=True, can_reveal_password=True, width=300)
self.confirm_password = ft.TextField(label="Confirm Password", password=True, can_reveal_password=True, width=300)
self.name = ft.TextField(label="Company Name", width=300)
self.register_button = ft.ElevatedButton(text="Register", on_click=self.on_register_clicked, width=150)
self.back_button = ft.TextButton(text="Back to Login", on_click=self.on_back_clicked)
self.message = ft.Text(value="", color=ft.Colors.RED)
def on_register_clicked(self, e):
# Email validation
email_regex = r"^[^@]+@[^@]+\.[^@\.]+(\.[^@\.]+)*$"
if not re.match(email_regex, self.email.value):
self.message.value = "Invalid email address"
self.page.update()
return
# Password strength validation
if len(self.password.value) < 8 or not re.search(r"[A-Z]", self.password.value) or not re.search(r"[0-9]", self.password.value):
self.message.value = "Password must be at least 8 characters long and include a number and a capital letter"
self.page.update()
return
# Placeholder for register logic
if self.password.value != self.confirm_password.value:
self.message.value = "Passwords do not match"
self.page.update()
return
self.message.value = "Registering..."
self.page.update()
try:
response = requests.post(f"{API_BASE_URL}/auth/register", json={
"name": self.name.value,
"email": self.email.value,
"password": self.password.value
})
if response.status_code == 201:
self.message.color = ft.Colors.GREEN
self.message.value = "Registration successful. You can now log in."
self.page.update()
time.sleep(3)
self.on_back_clicked('')
else:
self.message.color = ft.Colors.RED
self.message.value = response.json().get("message", "Registration failed, the user is already registered.")
self.page.update()
except Exception as err:
self.message.color = ft.Colors.RED
self.message.value = f"Error: {err}"
self.page.update()
def on_back_clicked(self, e):
self.auth.placeholder.content.clean()
self.auth.placeholder.content = self.login.build()
self.auth.placeholder.update()
def build(self):
return ft.Column(
[
ft.Text(
"Register",
size=30,
weight="bold"
),
self.name,
self.email,
self.password,
self.confirm_password,
self.register_button,
self.message,
self.back_button
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER
)

View File

@@ -0,0 +1,389 @@
import flet as ft
import requests
from datetime import datetime
from config import API_BASE_URL
class ReportPage:
def __init__(self, page: ft.Page, dashboard):
self.page = page
self.dashboard = dashboard
self.start_date = ft.Text()
self.end_date = ft.Text()
# self.client_filter = ft.TextField(label="Client", expand=True)
# self.transporter_filter = ft.TextField(label="Transporter", expand=True)
self.status_text = ft.Text("")
self.results_text = ft.Text("")
self.rows = []
self.rows_copy = []
self.total = ft.Text("Total: ", weight=ft.FontWeight.BOLD)
self.data_table = ft.DataTable(
columns=[
ft.DataColumn(label=ft.Text("Order #")),
ft.DataColumn(label=ft.Text("Client")),
ft.DataColumn(label=ft.Text("Transporter")),
ft.DataColumn(label=ft.Text("Date")),
ft.DataColumn(label=ft.Text("Paid (€)")),
ft.DataColumn(label=ft.Text("Received (€)")),
ft.DataColumn(label=ft.Text("Profit (€)")),
],
rows=[],
border=ft.border.all(1, ft.Colors.GREY_300),
expand=True
)
self.all_clients = []
self.all_transporters = []
self.create_table_rows_data()
self.clients_filter = ft.Dropdown(
options=[
ft.dropdown.Option(text = client['name'], key=client['name']) for client in self.all_clients
],
width=250,
label="Clients",
hint_text= "Select client",
on_change= self.filter_by_client
)
self.clients_filter_placeholder = ft.Container(content=self.clients_filter)
self.transporters_filter = ft.Dropdown(
options=[
ft.dropdown.Option(text = transporter['name'], key=transporter['name']) for transporter in self.all_transporters
],
width=250,
label="Transporters",
hint_text= "Select transporter",
on_change= self.filter_by_transporter
)
self.transporters_filter_placeholder = ft.Container(content=self.transporters_filter)
def get_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_client(self, id):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/clients/{id}", headers=headers)
if response.json() not in self.all_clients:
self.all_clients.append(response.json())
return response.json() if response.status_code == 200 else None
except Exception as e:
print("Error loading clients:", e)
def get_transporter(self, id):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/transporters/{id}", headers=headers)
if response.json() not in self.all_transporters:
self.all_transporters.append(response.json())
return response.json() if response.status_code == 200 else None
except Exception as e:
print("Error loading transporters:", e)
def fetch_report(self, e):
total = 0
data = self.search_input.value
self.rows_copy = self.rows
self.data_table.rows.clear()
buffer = []
for r in self.rows_copy:
if data in r[1]:
row = ft.DataRow(
cells=[
ft.DataCell(ft.Text(r[0])),
ft.DataCell(ft.Text(r[1])),
ft.DataCell(ft.Text(r[2])),
ft.DataCell(ft.Text(r[3])),
ft.DataCell(ft.Text(r[4])),
ft.DataCell(ft.Text(r[5])),
ft.DataCell(ft.Text(r[6])),
],
)
self.data_table.rows.append(row)
buffer.append(r)
total += r[6]
self.rows_copy = buffer
self.data_table.update()
self.total.value = f"Total: {total}"
self.total.update()
def create_table_rows_data(self):
all_orders = self.get_orders()
total = 0
for order in all_orders:
# Skip non-active orders from reports
#print(order.get('status'))
if order.get('status') != 'active':
continue
order_number = order['order_number']
client_name = self.get_client(order['client_id'])['name']
transporter_name = self.get_transporter(order['transporter_id'])['name']
order_date = order['created_at'].split("T")[0]
paid = order['paid_price']
received = order['received_price']
try:
profit = round(float(received) - float(paid), 2)
except:
profit = 0.00
row = ft.DataRow(
cells=[
ft.DataCell(ft.Text(order_number)),
ft.DataCell(ft.Text(client_name)),
ft.DataCell(ft.Text(transporter_name)),
ft.DataCell(ft.Text(order_date)),
ft.DataCell(ft.Text(received)),
ft.DataCell(ft.Text(paid)),
ft.DataCell(ft.Text(profit)),
],
)
row_data = [order_number, client_name, transporter_name, order_date, paid, received, profit]
self.rows.append(row_data)
self.data_table.rows.append(row)
total += profit
self.total.value = f"Total: {total}"
self.rows_copy = self.rows
def on_reset_btn_click(self, e):
# --- Recreate Clients dropdown to avoid sticky selection text on Flet 0.28.3 ---
try:
client_options = list(self.clients_filter.options)
except Exception:
client_options = []
new_clients_dd = ft.Dropdown(
options=client_options,
width=250,
label="Clients",
hint_text="Select client",
on_change=self.filter_by_client,
value=None,
)
self.clients_filter = new_clients_dd
self.clients_filter_placeholder.content = self.clients_filter
self.clients_filter_placeholder.update()
# --- Recreate Transporters dropdown ---
try:
transporter_options = list(self.transporters_filter.options)
except Exception:
transporter_options = []
new_transporters_dd = ft.Dropdown(
options=transporter_options,
width=250,
label="Transporters",
hint_text="Select transporter",
on_change=self.filter_by_transporter,
value=None,
)
self.transporters_filter = new_transporters_dd
self.transporters_filter_placeholder.content = self.transporters_filter
self.transporters_filter_placeholder.update()
self.page.update()
self.rows_copy = list(self.rows)
total = 0
self.data_table.rows.clear()
for r in self.rows_copy:
row = ft.DataRow(
cells=[
ft.DataCell(ft.Text(r[0])),
ft.DataCell(ft.Text(r[1])),
ft.DataCell(ft.Text(r[2])),
ft.DataCell(ft.Text(r[3])),
ft.DataCell(ft.Text(r[4])),
ft.DataCell(ft.Text(r[5])),
ft.DataCell(ft.Text(r[6])),
],
)
self.data_table.rows.append(row)
total += r[6]
self.data_table.update()
self.total.value = f"Total: {total}"
self.total.update()
self.start_date.value = ""
self.start_date.update()
self.end_date.value = ""
self.end_date.update()
def on_start_date_click(self, e):
self.start_date.value = e.control.value.strftime('%Y-%m-%d')
self.start_date.update()
total = 0
self.data_table.rows.clear()
buffer = []
data = datetime.strptime(self.start_date.value, '%Y-%m-%d')
for r in self.rows_copy:
obj_date = datetime.strptime(r[3], '%Y-%m-%d')
if data <= obj_date:
row = ft.DataRow(
cells=[
ft.DataCell(ft.Text(r[0])),
ft.DataCell(ft.Text(r[1])),
ft.DataCell(ft.Text(r[2])),
ft.DataCell(ft.Text(r[3])),
ft.DataCell(ft.Text(r[4])),
ft.DataCell(ft.Text(r[5])),
ft.DataCell(ft.Text(r[6])),
],
)
self.data_table.rows.append(row)
buffer.append(r)
total += r[6]
self.rows_copy = buffer
self.data_table.update()
self.total.value = f"Total: {total}"
self.total.update()
def on_end_date_click(self, e):
self.end_date.value = e.control.value.strftime('%Y-%m-%d')
self.end_date.update()
total = 0
self.data_table.rows.clear()
buffer = []
data = datetime.strptime(self.end_date.value, '%Y-%m-%d')
for r in self.rows_copy:
obj_date = datetime.strptime(r[3], '%Y-%m-%d')
if data >= obj_date:
row = ft.DataRow(
cells=[
ft.DataCell(ft.Text(r[0])),
ft.DataCell(ft.Text(r[1])),
ft.DataCell(ft.Text(r[2])),
ft.DataCell(ft.Text(r[3])),
ft.DataCell(ft.Text(r[4])),
ft.DataCell(ft.Text(r[5])),
ft.DataCell(ft.Text(r[6])),
],
)
self.data_table.rows.append(row)
buffer.append(r)
total += r[6]
self.rows_copy = buffer
self.data_table.update()
self.total.value = f"Total: {total}"
self.total.update()
def filter_by_client(self, e):
total = 0
self.data_table.rows.clear()
buffer = []
for r in self.rows_copy:
#print(r[1])
#print(self.clients_filter.value)
if r[1] == self.clients_filter.value:
row = ft.DataRow(
cells=[
ft.DataCell(ft.Text(r[0])),
ft.DataCell(ft.Text(r[1])),
ft.DataCell(ft.Text(r[2])),
ft.DataCell(ft.Text(r[3])),
ft.DataCell(ft.Text(r[4])),
ft.DataCell(ft.Text(r[5])),
ft.DataCell(ft.Text(r[6])),
],
)
self.data_table.rows.append(row)
buffer.append(r)
total += r[6]
self.rows_copy = buffer
self.data_table.update()
self.total.value = f"Total: {total}"
self.total.update()
def filter_by_transporter(self, e):
total = 0
self.data_table.rows.clear()
buffer = []
for r in self.rows_copy:
#print(r[2])
#print(self.transporters_filter.value)
if r[2] == self.transporters_filter.value:
row = ft.DataRow(
cells=[
ft.DataCell(ft.Text(r[0])),
ft.DataCell(ft.Text(r[1])),
ft.DataCell(ft.Text(r[2])),
ft.DataCell(ft.Text(r[3])),
ft.DataCell(ft.Text(r[4])),
ft.DataCell(ft.Text(r[5])),
ft.DataCell(ft.Text(r[6])),
],
)
self.data_table.rows.append(row)
buffer.append(r)
total += r[6]
self.rows_copy = buffer
self.data_table.update()
self.total.value = f"Total: {total}"
self.total.update()
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Row(
[
ft.Text("Reports", size=24, weight=ft.FontWeight.BOLD),
self.total
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
),
ft.Row(
[
self.start_date,
ft.ElevatedButton(
"Start Date",
on_click=lambda _: self.page.open(ft.DatePicker(
first_date=datetime(year=2000, month=10, day=1),
last_date=datetime(year=2025, month=10, day=1),
on_change=self.on_start_date_click,
)),
width=120,
icon=ft.Icons.CALENDAR_MONTH
),
self.end_date,
ft.ElevatedButton(
"End Date",
on_click=lambda _: self.page.open(ft.DatePicker(
first_date=datetime(year=2000, month=10, day=1),
last_date=datetime(year=2025, month=10, day=1),
on_change=self.on_end_date_click,
)),
width=120,
icon=ft.Icons.CALENDAR_MONTH
),
ft.Text(),
self.clients_filter_placeholder,
self.transporters_filter_placeholder,
ft.ElevatedButton("Reset", on_click=self.on_reset_btn_click, width=120),
]
),
ft.Column(
[
ft.Row(
[
self.data_table,
self.status_text,
],
expand=True,
)
],
alignment=ft.MainAxisAlignment.START,
scroll=ft.ScrollMode.ADAPTIVE,
expand=True,
)
],
expand=True,
alignment=ft.MainAxisAlignment.START,
),
expand=True
)

View File

@@ -0,0 +1,68 @@
import flet as ft
import requests
import re
from config import API_BASE_URL
class ResetPasswordPage:
def __init__(self, page: ft.Page):
self.page = page
self.page.update()
self.token = None
if '/reset_password?token=' in self.page.route:
self.token = self.page.route.split('?token=')[1]
self.password = ft.TextField(label="New Password", password=True, can_reveal_password=True)
self.confirm_password = ft.TextField(label="Confirm Password", password=True, can_reveal_password=True)
self.message = ft.Text("")
self.submit_btn = ft.ElevatedButton("Reset Password", on_click=self.on_submit)
def on_submit(self, e):
new_password = self.password.value.strip()
confirm_password = self.confirm_password.value.strip()
# Password strength validation
if len(new_password) < 8 or not re.search(r"[A-Z]", new_password) or not re.search(r"[0-9]", new_password):
self.message.value = "Password must be at least 8 characters long and include a number and a capital letter."
self.page.update()
return
if not new_password or not confirm_password:
self.message.value = "Both fields are required."
elif new_password != confirm_password:
self.message.value = "Passwords do not match."
elif len(new_password) < 6:
self.message.value = "Password must be at least 6 characters."
else:
try:
response = requests.post(
f"{API_BASE_URL}/auth/reset_password",
json={"token": self.token, "new_password": new_password},
timeout=10
)
if response.status_code == 200:
self.message.value = "Password reset successfully. Please login."
else:
self.message.value = "Invalid or expired token."
except Exception as ex:
self.message.value = f"Request failed: {ex}"
self.page.update()
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Text("Reset Your Password", style=ft.TextThemeStyle.HEADLINE_MEDIUM),
self.password,
self.confirm_password,
self.message,
self.submit_btn
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
expand=True,
),
expand=True,
alignment=ft.alignment.center,
padding=20,
width=350
)

View File

@@ -0,0 +1,118 @@
import flet as ft
import requests
import json
from config import API_BASE_URL
class SendEmail:
def __init__(self, page: ft.Page, view_page, dashboard, order_id):
self.page = page
self.view_page = view_page
self.dashboard = dashboard
self.order_id = order_id
self.smtp_password = ft.TextField(label="PASSWORD")
self.message = ft.TextField(label="Message", multiline=True, min_lines=10, max_lines=20, expand=True)
self.send = ft.ElevatedButton("Send", width=150, on_click=self.send_email)
self.order = self.get_order()
if self.order:
transporter = self.get_transporter(self.order['transporter_id'])
self.to_email = transporter['email']
self.smtp_host = ''
self.smtp_port = ''
self.from_email = ''
def on_go_back_btn_click(self, e):
self.dashboard.placeholder.content = self.view_page.build()
self.dashboard.placeholder.update()
def send_email(self, e):
user_id = self.page.session.get("user_id")
user = self.get_user()
transporter_data = {
"to_email":self.to_email,
"subject":self.order['order_number'],
"body":self.message.value,
"filename":f'order_{user_id}_{self.order['order_number']}.pdf',
"smtp_host":self.smtp_host,
"smtp_port":self.smtp_port,
"smtp_user":self.from_email,
"smtp_pass": self.smtp_password.value,
}
self.send_email_custom(transporter_data)
user_data = {
"to_email":user['email'],
"subject":self.order['order_number'],
"body":self.message.value,
"filename":f'order_{user_id}_{self.order['order_number']}.pdf',
"smtp_host":self.smtp_host,
"smtp_port":self.smtp_port,
"smtp_user":self.from_email,
"smtp_pass": self.smtp_password.value,
}
self.send_email_custom(user_data)
def send_email_custom(self, data):
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/orders/send-email/custom", headers=headers, data=json.dumps(data))
return response.json() if response.status_code == 200 else None
def get_transporter(self, id):
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
user_id = self.page.session.get("user_id")
response = requests.get(f"{API_BASE_URL}/transporters/{id}", headers=headers)
return response.json() if response.status_code == 200 else None
def get_order(self):
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/orders/{self.order_id}", headers=headers)
return response.json() if response.status_code == 200 else None
def get_user(self):
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/profile", headers=headers)
return response.json() if response.status_code == 200 else None
def get_email_credentials(self):
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/profile/email", headers=headers)
credentials = response.json()
self.smtp_host = credentials['smtp_host']
self.smtp_port = credentials['smtp_port']
self.from_email = credentials['smtp_user']
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Row(
[
ft.Text("Send E-mail", size=24, weight=ft.FontWeight.BOLD),
ft.Button("Back", icon=ft.Icons.ARROW_BACK_IOS_NEW, on_click=self.on_go_back_btn_click)
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
),
ft.Row(
[
ft.Column(
[
ft.Text("Please insert email password:"),
self.smtp_password,
]
),
self.send
],
vertical_alignment=ft.CrossAxisAlignment.END,
expand=True
),
self.message
],
alignment=ft.MainAxisAlignment.START,
scroll=ft.ScrollMode.ADAPTIVE,
expand=True
),
expand=True
)

View File

@@ -0,0 +1,247 @@
import flet as ft
import requests
from config import API_BASE_URL
class Subscription:
def __init__(self, page: ft.Page, dashboard):
self.page = page
self.dashboard = dashboard
self.subscription = self.get_current_subscription_plan()
self.plan = {
'first_2_months':'First Two Months' ,
'monthly':'Monthly',
'yearly':'Yearly'
}
self.status = {
'active':'Active',
'cancelled':'Cancelled',
'expired':'Expired',
'less_than_5_days':'Less than 5 days'
}
self.current_subscription_plan = ft.Text(self.plan[self.subscription['plan']] if self.subscription else "No subscription")
self.current_subscription_status = ft.Text(self.status[self.subscription['status']] if self.subscription else "None")
self.monthly_subscription_price = ft.Text(
"100 Euro/Month",
weight=ft.FontWeight.BOLD,
size=18,
color=ft.Colors.WHITE
)
self.year_subscription_price = ft.Text(
"1000 Euro/Year",
weight=ft.FontWeight.BOLD,
size=18,
color=ft.Colors.WHITE
)
self.first_subscription_price = ft.Text(
"0 Euro/Month",
weight=ft.FontWeight.BOLD,
size=18,
color=ft.Colors.WHITE
)
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 on_first_two_months_btn_click(self, e):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{API_BASE_URL}/subscription/first_2_months", headers=headers, )
#print(response.text)
self.change_subscription_to_active('first_2_months', 'active')
return response.json() if response.status_code == 200 else None
except Exception as e:
print("Error loading subscription:", e)
def on_month_subscription_btn_click(self, e):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{API_BASE_URL}/subscription/one_month", headers=headers)
#print(response.text)
self.change_subscription_to_active('monthly', 'active')
return response.json() if response.status_code == 200 else None
except Exception as e:
print("Error loading subscription:", e)
def on_year_subscription_btn_click(self, e):
try:
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{API_BASE_URL}/subscription/one_year", headers=headers)
#print(response.text)
self.change_subscription_to_active('yearly', 'active')
return response.json() if response.status_code == 200 else None
except Exception as e:
print("Error loading subscription:", e)
def change_subscription_to_active(self, plan, status):
self.dashboard.subscription_status_bottom.value = "Active"
self.dashboard.subscription_status_bottom.color = ft.Colors.GREEN
self.dashboard.subscription_status_bottom.update()
self.current_subscription_plan.value = self.plan[plan]
self.current_subscription_plan.update()
self.current_subscription_status.value = self.status[status]
self.current_subscription_status.update()
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Row(
[
ft.Text("Subscription", size=24, weight=ft.FontWeight.BOLD),
ft.Column(
[
ft.Row(
[
ft.Text("Current Subscription Plan:", weight=ft.FontWeight.BOLD),
self.current_subscription_plan
]
),
ft.Row(
[
ft.Text("Subscription Status:"),
self.current_subscription_status
]
)
]
)
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
),
ft.Row(
[
ft.Container(
content = ft.Column(
[
ft.Icon(
name = ft.Icons.AUTORENEW,
size=150
),
ft.Container(
content=ft.Column(
[
ft.Text("First Two Months", weight=ft.FontWeight.BOLD),
self.first_subscription_price
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
expand=True
),
bgcolor=ft.Colors.BLUE_200,
padding=20,
width=250
),
ft.Row(
[
ft.FilledButton("Add", width=150, on_click=self.on_first_two_months_btn_click)
],
alignment=ft.MainAxisAlignment.CENTER,
expand=True
)
],
expand = True ,
horizontal_alignment=ft.CrossAxisAlignment.CENTER
),
border_radius=20,
border=ft.border.all(1, ft.Colors.GREY_300),
bgcolor=ft.Colors.BLUE_50,
padding = ft.padding.symmetric(vertical=20),
width=250,
height=350
),
ft.Container(
content = ft.Column(
[
ft.Icon(
name = ft.Icons.AUTORENEW,
size=150
),
ft.Container(
content=ft.Column(
[
ft.Text("One Month Subscription", weight=ft.FontWeight.BOLD),
self.monthly_subscription_price
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
expand=True
),
bgcolor=ft.Colors.BLUE_200,
padding=20,
width=250
),
ft.Row(
[
ft.FilledButton("Add / Renew", width=150, on_click=self.on_month_subscription_btn_click)
],
alignment=ft.MainAxisAlignment.CENTER,
expand=True
)
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER
),
border_radius=20,
border=ft.border.all(1, ft.Colors.GREY_300),
bgcolor=ft.Colors.BLUE_50,
padding = ft.padding.symmetric(vertical=20),
width=250,
height=350
),
ft.Container(
content = ft.Column(
[
ft.Icon(
name = ft.Icons.AUTORENEW,
size=150
),
ft.Container(
content=ft.Column(
[
ft.Text("One Year Subscription", weight=ft.FontWeight.BOLD),
self.year_subscription_price
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
expand=True
),
bgcolor=ft.Colors.BLUE_200,
padding=20,
width=250
),
ft.Row(
[
ft.FilledButton("Add / Renew", width=150, on_click=self.on_year_subscription_btn_click)
],
alignment=ft.MainAxisAlignment.CENTER,
expand=True
)
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER
),
border_radius=20,
border=ft.border.all(1, ft.Colors.GREY_300),
bgcolor=ft.Colors.BLUE_50,
padding = ft.padding.symmetric(vertical=20),
width=250,
height=350
)
],
alignment=ft.MainAxisAlignment.CENTER,
spacing=20
)
],
alignment=ft.MainAxisAlignment.START,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
expand=True,
spacing=50
),
expand=True
)

View File

@@ -0,0 +1,273 @@
import flet as ft
import requests
from config import API_BASE_URL
class TransportersPage:
def __init__(self, page: ft.Page, dashboard):
self.page = page
self.dashboard = dashboard
self.transporters = []
self.dialog = None
self.name = ft.TextField(
label="Name",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.street_and_number = ft.TextField(
label="Street and number",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.postal_code = ft.TextField(
label="Postal code",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.city = ft.TextField(
label="City",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.region_county = ft.TextField(
label="Region / County",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.country = ft.TextField(
label="Country",
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.register_number = ft.TextField(
label="Register Number",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.contact_person = ft.TextField(
label="Contact Person",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.phone = ft.TextField(
label="Phone",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.email = ft.TextField(
label="Email",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.vat = ft.TextField(
label="VAT",
expand=True,
input_filter=ft.InputFilter(
allow=True,
regex_string=r"^[\x20-\x7E]*$",
replacement_string=""
),
)
self.user_id = self.page.session.get("user_id")
self.selected_id = None
self.subscription_error = ft.Text("Please subscribe to add new transporter", color=ft.Colors.RED)
def open_dialog(self, transporter=None):
if transporter:
self.selected_id = transporter["id"]
self.name.value = transporter["name"]
self.street_and_number.value = transporter["address"].split(" %")[0]
self.postal_code.value = transporter["address"].split(" %")[1]
self.city.value = transporter["address"].split(" %")[2]
self.region_county.value = transporter["address"].split(" %")[3]
self.country.value = transporter["address"].split(" %")[4]
self.register_number.value = transporter["register_number"]
self.contact_person.value = transporter["contact_person"]
self.phone.value = transporter["phone"]
self.email.value = transporter["email"]
self.vat.value = transporter["vat"]
else:
self.selected_id = None
self.name.value = ""
self.street_and_number.value = ""
self.postal_code.value = ""
self.city.value = ""
self.region_county.value = ""
self.country.value = ""
self.register_number.value = ""
self.contact_person.value = ""
self.phone.value = ""
self.email.value = ""
self.vat.value = ""
self.dialog = ft.AlertDialog(
modal=True,
title=ft.Text("Transporter"),
content=ft.Column(
controls=[
self.name,
self.register_number,
self.vat,
self.contact_person,
self.phone,
self.email,
self.street_and_number,
self.postal_code,
self.city,
self.region_county,
self.country,
],
width=600
),
actions=[
ft.TextButton("Cancel", on_click=lambda e: self.page.close(self.dialog)),
ft.ElevatedButton("Save", on_click=self.save_transporter)
],
)
self.page.dialog = self.dialog
self.page.open(self.dialog)
def save_transporter(self, e):
address = f'{self.street_and_number.value} %{self.postal_code.value} %{self.city.value} %{self.region_county.value} %{self.country.value}'
data = {
"name": self.name.value,
"address": address,
"register_number": self.register_number.value,
"contact_person": self.contact_person.value,
"phone": self.phone.value,
"email": self.email.value,
"vat": self.vat.value,
"user_id": self.user_id
}
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
if self.selected_id:
requests.put(f"{API_BASE_URL}/transporters/{self.selected_id}", json=data, headers=headers)
else:
requests.post(f"{API_BASE_URL}/transporters/", json=data, headers=headers)
self.page.close(self.dialog)
self.refresh()
self.page.update()
def delete_transporter(self, transporter_id):
def confirm_delete(e):
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
requests.delete(f"{API_BASE_URL}/transporters/{transporter_id}", headers=headers)
self.page.close(self.confirm_dialog)
self.refresh()
self.confirm_dialog = ft.AlertDialog(
title=ft.Text("Confirm"),
content=ft.Text("Are you sure you want to delete this transporter?"),
actions=[
ft.ElevatedButton("Yes", on_click=confirm_delete),
ft.ElevatedButton("No", on_click=lambda e: self.page.close(self.confirm_dialog))
]
)
self.page.dialog = self.confirm_dialog
self.page.open(self.confirm_dialog)
def refresh(self):
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/transporters/", headers=headers)
if response.ok:
self.transporters = response.json()
else:
self.transporters = []
self.transporter_list.controls.clear()
for transporter in self.transporters:
self.transporter_list.controls.append(
ft.Container(
content=ft.Row([
ft.Column([
ft.Text(f"{transporter['name']}", size=16, weight=ft.FontWeight.BOLD),
ft.Text(f"{transporter['email']}{transporter['phone']}", size=12)
], expand=True),
ft.IconButton(icon=ft.Icons.EDIT, on_click=lambda e, t=transporter: self.open_dialog(t)),
ft.IconButton(icon=ft.Icons.DELETE, on_click=lambda e, t=transporter: self.delete_transporter(t["id"])),
]),
padding=10,
border=ft.border.all(1, ft.Colors.GREY_300),
bgcolor=ft.Colors.BLUE_50,
border_radius=10,
)
)
self.page.update()
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.transporter_list = ft.Column(spacing=10, expand=True, scroll=ft.ScrollMode.ADAPTIVE,)
self.refresh()
self.add_transporter_btn = ft.ElevatedButton("Add Transporter", icon=ft.Icons.ADD, on_click=lambda e: self.open_dialog())
self.header = ft.Row(
controls=[
ft.Text("Transporters", size=24, weight=ft.FontWeight.BOLD, expand=True),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
)
subscription = self.get_current_subscription_plan()
if subscription:
if subscription['status'] != 'expired':
self.header.controls.append(self.add_transporter_btn)
else:
self.header.controls.append(self.subscription_error)
else:
self.header.controls.append(self.subscription_error)
return ft.Column(
[
self.header,
self.transporter_list
],
alignment=ft.MainAxisAlignment.START,
)

View File

@@ -0,0 +1,70 @@
import flet as ft
import requests
from config import API_BASE_URL
class TwoFactorAuth:
def __init__(self, page: ft.Page, email: str, login, auth):
self.page = page
self.email = email
self.auth = auth
self.login = login
self.code_field = ft.TextField(label="Verification Code")
self.error_text = ft.Text(value="", color=ft.Colors.RED)
self.success_text = ft.Text(value="", color=ft.Colors.GREEN)
self.verify_button = ft.ElevatedButton(text="Verify", on_click=self.on_verify_click, width=150)
self.back_button = ft.TextButton(text="Back to Login", on_click=self.on_back_clicked)
def on_verify_click(self, e):
code = self.code_field.value
if not code:
self.error_text.value = "Please enter the verification code."
self.page.update()
return
try:
response = requests.post(f"{API_BASE_URL}/auth/verify_code", json={"email": self.email, "code": code})
if response.status_code == 200:
token = response.json().get("access_token")
self.page.client_storage.set("token", token)
user_info = requests.get(f"{API_BASE_URL}/auth/me", headers={"Authorization": f"Bearer {token}"}).json()
self.page.session.set("user_id", user_info.get("id"))
self.page.session.set("first_order_number", user_info['first_order_number'])
#print(user_info.get("user_role"))
if user_info.get("user_role") == 'admin':
print('Admin Logged In')
self.page.go("/admin")
else:
self.success_text.value = "Verification successful. You are now logged in."
self.error_text.value = ""
self.page.update()
self.page.go("/dashboard") # Change this to your main page
else:
self.error_text.value = "Invalid or expired code."
self.success_text.value = ""
self.page.update()
except Exception as err:
self.error_text.value = f"Error: {err}"
self.success_text.value = ""
self.page.update()
def on_back_clicked(self, e):
self.auth.placeholder.content.clean()
self.auth.placeholder.content = self.login.build()
self.auth.placeholder.update()
def build(self):
return ft.Column(
[
ft.Text(value=f"Enter the code sent to {self.email}", size=18),
self.code_field,
self.verify_button,
self.error_text,
self.success_text,
self.back_button
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER,
spacing=20,
width=350
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
import flet as ft
from flet_webview import WebView
from pages.send_email_page import SendEmail
import requests
from config import API_BASE_URL
class ViewPage:
def __init__(self, page: ft.Page, pdf_name, order_page, dashboard, order_id):
self.page = page
self.pdf_name = pdf_name
self.order_page = order_page
self.dashboard = dashboard
self.order_id = order_id
self.send_email = SendEmail(self.page, self, self.dashboard, self.order_id)
self.view_content = WebView(url=f"{API_BASE_URL}/orders/pdfs/{self.pdf_name}", expand=True)
self.send_btn = ft.FilledButton("Send as Email", on_click=self.on_send_email_btn_click)
self.row_btn = ft.Row([])
if self.get_credentials():
self.row_btn.controls.append(self.send_btn)
def on_go_back_btn_click(self, e):
self.dashboard.placeholder.content = self.order_page.build()
self.dashboard.placeholder.update()
def on_send_email_btn_click(self, e):
self.dashboard.placeholder.content = self.send_email.build()
self.dashboard.placeholder.update()
def get_credentials(self):
token = self.page.client_storage.get("token")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{API_BASE_URL}/profile/email", headers=headers)
return response.json() if response.status_code == 200 else None
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Row(
[
ft.Text("Order", size=24, weight=ft.FontWeight.BOLD),
ft.Button("Back", icon=ft.Icons.ARROW_BACK_IOS_NEW, on_click=self.on_go_back_btn_click)
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
),
self.view_content,
self.row_btn,
]
)
)

View File

@@ -0,0 +1,5 @@
flet==0.28.3
requests==2.32.3
python-dotenv==1.0.1
flet-web==0.28.3
flet-webview==0.1.0