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

View File

@@ -0,0 +1,30 @@
#client
flet==0.28.3
requests==2.32.3
python-dotenv==1.0.1
flet-web==0.28.3
flet-webview==0.1.0
# --- Flask API (server) requirements ---
# Web framework
Flask==2.3.3
Flask-Cors==4.0.0
Flask-JWT-Extended==4.6.0
# WSGI server
gunicorn==22.0.0
# Database (Postgres)
psycopg[binary]==3.2.1
# Utilities
python-dotenv==1.0.1
requests==2.32.3
# Features used by your server
geopy==2.4.1
reportlab>=3.6.12
PyPDF2==3.0.1
APScheduler==3.10.4
tzlocal==5.2

BIN
transportmanager/server/.DS_Store vendored Normal file

Binary file not shown.

View File

View File

@@ -0,0 +1,53 @@
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required
from models.subscription import Subscription
admin_subscription_bp = Blueprint("admin_subscription", __name__, url_prefix="/admin/subscriptions")
# List all subscriptions
@admin_subscription_bp.route("", methods=["GET"])
@jwt_required()
def list_subscriptions():
subscription_model = Subscription()
# Optional: if you want, you can add pagination here later!
subscriptions = subscription_model.get_all()
return jsonify(subscriptions), 200
# Update subscription status
@admin_subscription_bp.route("/update_status", methods=["POST"])
@jwt_required()
def update_subscription_status():
data = request.json
subscription_id = data.get("subscription_id")
new_status = data.get("status")
if not subscription_id or not new_status:
return jsonify({"error": "Missing subscription_id or status"}), 400
subscription_model = Subscription()
updated_rows = subscription_model.update_status(subscription_id, new_status)
if updated_rows == 0:
return jsonify({"error": "Subscription not found"}), 404
return jsonify({"message": "Subscription status updated."}), 200
# Cancel subscription
@admin_subscription_bp.route("/cancel", methods=["POST"])
@jwt_required()
def cancel_subscription():
data = request.json
subscription_id = data.get("subscription_id")
if not subscription_id:
return jsonify({"error": "Missing subscription_id"}), 400
subscription_model = Subscription()
updated_rows = subscription_model.update_status(subscription_id, "cancelled")
if updated_rows == 0:
return jsonify({"error": "Subscription not found"}), 404
return jsonify({"message": "Subscription cancelled."}), 200

View File

@@ -0,0 +1,40 @@
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required
from models.user import Users
admin_user_bp = Blueprint("admin_user", __name__, url_prefix="/admin/users")
# Get all users with role "user"
@admin_user_bp.route("", methods=["GET"])
@jwt_required()
def get_all_users():
users_model = Users()
users = users_model.get_all_users_with_role("user")
return jsonify(users), 200
# Get a single user by ID
@admin_user_bp.route("/<int:user_id>", methods=["GET"])
@jwt_required()
def get_user(user_id):
users_model = Users()
user = users_model.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify(user), 200
# Update a user
@admin_user_bp.route("/update", methods=["POST"])
@jwt_required()
def update_user():
if not request.is_json:
print("Content-Type received:", request.content_type)
return jsonify({"error": "Invalid content type, must be application/json"}), 415
data = request.get_json()
if not data.get("user_id"):
return jsonify({"error": "Missing user_id"}), 400
users_model = Users()
users_model.update_user(data)
return jsonify({"message": "User updated successfully."}), 200

View File

@@ -0,0 +1,93 @@
import os
from flask import Flask, jsonify
from routes.auth import auth_bp
from flask_jwt_extended import JWTManager
from routes.profile import profile_bp
from routes.clients import clients_bp
from routes.transporters import transporters_bp
from routes.destinations import destinations_bp
from routes.orders_out import orders_bp
from routes.ouders_in import orders_in_bp
from routes.report import report_bp
from admin.subscription import admin_subscription_bp
from routes.subscription import subscription_bp
from admin.tenants import admin_user_bp
from apscheduler.schedulers.background import BackgroundScheduler
from models.subscription import Subscription
from flask_cors import CORS
app = Flask(__name__)
CORS(
app,
resources={r"/*": {"origins": [os.getenv("WEB_ORIGIN", "*")]}},
allow_headers=["Authorization", "Content-Type"],
expose_headers=["Content-Type"],
)
@app.get("/db/check")
def db_check():
try:
import psycopg # psycopg3 client
except Exception as e:
return {"ok": False, "error": f"psycopg not available: {e}"}, 500
dsn = os.getenv("DATABASE_URL")
if not dsn:
return {"ok": False, "error": "DATABASE_URL not set"}, 500
try:
with psycopg.connect(dsn, connect_timeout=5) as conn:
with conn.cursor() as cur:
cur.execute("SELECT version();")
ver = cur.fetchone()[0]
return {"ok": True, "version": ver}, 200
except Exception as e:
return {"ok": False, "error": str(e)}, 500
@app.get("/health")
def health():
return {"ok": True}, 200
app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY", "your-jwt-secret")
app.config["JWT_TOKEN_LOCATION"] = ["headers", "query_string"]
app.config["JWT_QUERY_STRING_NAME"] = "token"
jwt = JWTManager(app)
env = os.environ.get("FLASK_ENV", "development")
# Register blueprints
app.register_blueprint(auth_bp, url_prefix="/auth")
app.register_blueprint(profile_bp, url_prefix="/profile")
app.register_blueprint(clients_bp)
app.register_blueprint(transporters_bp, url_prefix="/transporters")
app.register_blueprint(destinations_bp, url_prefix="/destinations")
app.register_blueprint(orders_bp, url_prefix="/orders")
app.register_blueprint(orders_in_bp, url_prefix="/orders_in")
app.register_blueprint(report_bp, url_prefix="/report")
app.register_blueprint(admin_subscription_bp)
app.register_blueprint(subscription_bp)
app.register_blueprint(admin_user_bp)
def update_subscription_statuses_job():
print("[Scheduler] Running daily subscription status check...")
subscription_model = Subscription()
subscription_model.update_subscription_statuses()
RUN_SCHEDULER = os.getenv("RUN_SCHEDULER", "1") == "1"
if RUN_SCHEDULER:
scheduler = BackgroundScheduler(daemon=True)
scheduler.add_job(func=update_subscription_statuses_job, trigger="interval", days=1)
scheduler.start()
if __name__ == "__main__":
if env != "production":
# Avoid running the scheduler twice in development mode
import logging
logging.getLogger("apscheduler").setLevel(logging.DEBUG)
app.run(debug=(env == "development"), use_reloader=False)

View File

@@ -0,0 +1,70 @@
import os
import sqlite3
from pathlib import Path
try:
import psycopg
except ImportError:
psycopg = None
DB_TYPE = 'sqlite3'
BASE_DIR = Path(__file__).resolve().parent
SCHEMA_PATH = BASE_DIR / "schema.sql" if DB_TYPE != 'sqlite3' else BASE_DIR / "schema_sqlite.sql"
DATABASE_URL = os.getenv("DATABASE_URL")
SQLITE_PATH = BASE_DIR / "instance" / "dev.db"
def is_postgres():
return DATABASE_URL and DATABASE_URL.lower().startswith("postgres")
def get_connection():
if is_postgres():
if psycopg is None:
raise RuntimeError("psycopg is required for PostgreSQL but not installed.")
return psycopg.connect(DATABASE_URL, autocommit=True)
else:
SQLITE_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(SQLITE_PATH))
conn.execute("PRAGMA foreign_keys = ON;")
return conn
def _iter_sql_statements(script: str):
buffer = []
for line in script.splitlines():
buffer.append(line)
if line.strip().endswith(";"):
stmt = "\n".join(buffer).strip()
buffer = []
if stmt:
yield stmt
tail = "\n".join(buffer).strip()
if tail:
yield tail
def db_init():
if not SCHEMA_PATH.is_file():
raise FileNotFoundError(f"Schema file not found: {SCHEMA_PATH}")
with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
sql_script = f.read()
conn = get_connection()
try:
if is_postgres():
with conn.cursor() as cur:
for stmt in _iter_sql_statements(sql_script):
cur.execute(stmt)
else:
conn.executescript(sql_script)
conn.commit()
print("Database initialized successfully.")
finally:
conn.close()
if __name__ == "__main__":
db_init()

Binary file not shown.

View File

@@ -0,0 +1,79 @@
from datetime import datetime
from database import get_connection, is_postgres
class Clients:
def __init__(self):
# Parameter style: Postgres uses %s, SQLite uses ?
self.ph = "%s" if is_postgres() else "?"
def client_to_dict(self, row):
return {
"id": row[0],
"user_id": row[1],
"name": row[2],
"address": row[3],
"register_number": row[4],
"contact_person": row[5],
"phone": row[6],
"email": row[7],
"vat":row[8],
"created_at": row[9],
}
def create(self, user_id, name, address, register_number, contact_person, phone=None, email=None, vat=None):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
INSERT INTO clients (user_id, name, address, register_number, contact_person, phone, email, vat, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph})
""",
(user_id, name, address, register_number, contact_person, phone, email, vat, created_at),
)
if hasattr(conn, "commit"):
conn.commit()
def get_all_by_user(self, user_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM clients WHERE user_id = {self.ph}",
(user_id,),
)
return [self.client_to_dict(row) for row in cur.fetchall()]
def get_by_id(self, client_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM clients WHERE id = {self.ph}",
(client_id,),
)
row = cur.fetchone()
return self.client_to_dict(row) if row else None
def update(self, client_id, name, address, register_number, contact_person, phone=None, email=None, vat = None):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE clients
SET name={self.ph}, address={self.ph}, register_number={self.ph}, contact_person={self.ph},
phone={self.ph}, email={self.ph}, vat={self.ph}
WHERE id={self.ph}
""",
(name, address, register_number, contact_person, phone, email, vat, client_id),
)
if hasattr(conn, "commit"):
conn.commit()
def delete(self, client_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"DELETE FROM clients WHERE id={self.ph}",
(client_id,),
)
if hasattr(conn, "commit"):
conn.commit()

View File

@@ -0,0 +1,92 @@
from datetime import datetime
from database import get_connection, is_postgres
class Destinations:
def __init__(self):
# Parameter style: Postgres uses %s, SQLite uses ?
self.ph = "%s" if is_postgres() else "?"
def destination_to_dict(self, row):
destination = {
"id": row[0],
"user_id": row[1],
"name": row[2],
"address": row[3],
"latitude": row[4],
"longitude": row[5],
"created_at": row[6]
}
return destination
def create(self, user_id, name, address):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
INSERT INTO destinations (user_id, name, address, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph})
""",
(user_id, name, address, created_at),
)
if hasattr(conn, "commit"):
conn.commit()
return cur.lastrowid if hasattr(cur, "lastrowid") else None
def update(self, id, user_id, name, address):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE destinations
SET user_id = {self.ph}, name = {self.ph}, address = {self.ph}
WHERE id = {self.ph}
""",
(user_id, name, address, id),
)
if hasattr(conn, "commit"):
conn.commit()
def add_gps_coordinates(self, id, latitude, longitude):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE destinations
SET latitude = {self.ph}, longitude = {self.ph}
WHERE id = {self.ph}
""",
(latitude, longitude, id),
)
if hasattr(conn, "commit"):
conn.commit()
def delete(self, id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"DELETE FROM destinations WHERE id = {self.ph}",
(id,),
)
if hasattr(conn, "commit"):
conn.commit()
def get_all_by_user(self, user_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM destinations WHERE user_id = {self.ph} ORDER BY created_at DESC",
(user_id,),
)
rows = cur.fetchall()
return [self.destination_to_dict(row) for row in rows]
def get_by_id(self, id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM destinations WHERE id = {self.ph}",
(id,),
)
row = cur.fetchone()
return self.destination_to_dict(row) if row else None

View File

@@ -0,0 +1,193 @@
from datetime import datetime
from database import get_connection, is_postgres
class OrdersIn:
def __init__(self):
# Parameter placeholder per backend
self.ph = "%s" if is_postgres() else "?"
def order_to_dict(self, row):
return {
"id": row[0],
"order_number": row[1],
"user_id": row[2],
"client_id": row[3],
"products_description": row[4],
"ldb_quantity": row[5],
"kg_quantity": row[6],
"track_reg_number": row[7],
"trailer_reg_number": row[8],
"received_price": row[9],
"created_at": row[10],
}
def order_point_to_dict(self, row):
return {
"id": row[0],
"order_id": row[1],
"destination_id": row[2],
"informatins": row[3],
"point_data": row[4],
"point_hour": row[5],
'point_type': row[6],
}
def create_order(self, data):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cur = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cur.execute(
f"""
INSERT INTO orders_in
(user_id, client_id, products_description, received_price, order_number,
ldb_quantity, kg_quantity, track_reg_number, trailer_reg_number, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(
data["user_id"],
data["client_id"],
data['products_description'],
data["received_price"],
data["order_number"],
data["ldb_quantity"],
data["kg_quantity"],
data["track_reg_number"],
data["trailer_reg_number"],
created_at,
),
)
new_id = cur.fetchone()[0] if is_postgres() else getattr(cur, "lastrowid", None)
if hasattr(conn, "commit"):
conn.commit()
return new_id
def update_order(self, data):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE orders_in SET
user_id = {self.ph}, client_id = {self.ph}, products_description = {self.ph},
received_price = {self.ph}, order_number = {self.ph},
ldb_quantity = {self.ph}, kg_quantity = {self.ph}, track_reg_number = {self.ph},
trailer_reg_number = {self.ph}
WHERE id = {self.ph}
""",
(
data["user_id"],
data["client_id"],
data["products_description"],
data["received_price"],
data["order_number"],
data["ldb_quantity"],
data["kg_quantity"],
data["track_reg_number"],
data["trailer_reg_number"],
data["id"],
),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_order(self, order_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"DELETE FROM orders_in WHERE id = {self.ph}", (order_id,))
if hasattr(conn, "commit"):
conn.commit()
def get_order_by_id(self, order_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"SELECT * FROM orders_in WHERE id = {self.ph}", (order_id,))
row = cur.fetchone()
return self.order_to_dict(row) if row else None
def get_orders_by_user(self, user_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM orders_in WHERE user_id = {self.ph} ORDER BY created_at DESC",
(user_id,),
)
rows = cur.fetchall()
return [self.order_to_dict(row) for row in rows]
def create_order_point(self, data):
with get_connection() as conn:
cur = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cur.execute(
f"""
INSERT INTO order_in_points
(order_id, destination_id, informatins, point_data, point_hour, point_type)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(
data["order_id"],
data["destination_id"],
data["informatins"],
data["point_data"],
data["point_hour"],
data['point_type'],
),
)
new_id = cur.fetchone()[0] if is_postgres() else getattr(cur, "lastrowid", None)
if hasattr(conn, "commit"):
conn.commit()
return new_id
def update_order_point(self, data):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE order_in_points SET
order_id = {self.ph}, destination_id = {self.ph}, informatins = {self.ph},
point_data = {self.ph}, point_hour = {self.ph}, point_type = {self.ph}
WHERE id = {self.ph}
""",
(
data["order_id"],
data["destination_id"],
data["informatins"],
data["point_data"],
data["point_hour"],
data['point_type'],
data["id"],
),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_order_point(self, point_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"DELETE FROM order_in_points WHERE id = {self.ph}", (point_id,))
if hasattr(conn, "commit"):
conn.commit()
def delete_points_by_order_id(self, order_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"DELETE FROM order_in_points WHERE order_id = {self.ph}", (order_id,))
if hasattr(conn, "commit"):
conn.commit()
def get_order_point_by_id(self, point_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"SELECT * FROM order_in_points WHERE id = {self.ph}", (point_id,))
row = cur.fetchone()
return self.order_point_to_dict(row) if row else None
def get_order_points_by_order(self, order_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM order_in_points WHERE order_id = {self.ph} ORDER BY id",
(order_id,),
)
rows = cur.fetchall()
return [self.order_point_to_dict(row) for row in rows]

View File

@@ -0,0 +1,209 @@
from datetime import datetime
from database import get_connection, is_postgres
class OrdersOut:
def __init__(self):
# Parameter placeholder per backend
self.ph = "%s" if is_postgres() else "?"
def order_to_dict(self, row):
return {
"id": row[0],
"order_number": row[1],
"user_id": row[2],
"client_id": row[3],
"transporter_id": row[4],
"products_description": row[5],
"ldb_quantity": row[6],
"kg_quantity": row[7],
"track_reg_number": row[8],
"trailer_reg_number": row[9],
"received_price": row[10],
"paid_price": row[11],
"created_at": row[12],
"status": row[13]
}
def order_point_to_dict(self, row):
return {
"id": row[0],
"order_id": row[1],
"destination_id": row[2],
"informatins": row[3],
"point_data": row[4],
"point_hour": row[5],
'point_type': row[6],
}
def create_order(self, data):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cursor = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cursor.execute(
f"""
INSERT INTO orders_out
(user_id, client_id, transporter_id, products_description, received_price, paid_price, order_number,
ldb_quantity, kg_quantity, track_reg_number, trailer_reg_number, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(
data["user_id"],
data["client_id"],
data["transporter_id"],
data['products_description'],
data["received_price"],
data["paid_price"],
data["order_number"],
data["ldb_quantity"],
data["kg_quantity"],
data["track_reg_number"],
data["trailer_reg_number"],
created_at,
),
)
new_id = cursor.fetchone()[0] if is_postgres() else getattr(cursor, "lastrowid", None)
if hasattr(conn, "commit"):
conn.commit()
return new_id
def update_order(self, data):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE orders_out SET client_id = {self.ph}, transporter_id = {self.ph}, products_description = {self.ph},
received_price = {self.ph}, paid_price = {self.ph}, order_number = {self.ph},
ldb_quantity = {self.ph}, kg_quantity = {self.ph}, track_reg_number = {self.ph},
trailer_reg_number = {self.ph}
WHERE id = {self.ph}
""",
(
data["client_id"],
data["transporter_id"],
data["products_description"],
data["received_price"],
data["paid_price"],
data["order_number"],
data["ldb_quantity"],
data["kg_quantity"],
data["track_reg_number"],
data["trailer_reg_number"],
data["id"],
),
)
if hasattr(conn, "commit"):
conn.commit()
def cancel_order(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"UPDATE orders_out SET order_status = {self.ph} WHERE id = {self.ph}",
('cancelled', order_id,),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_order(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"DELETE FROM orders_out WHERE id = {self.ph}", (order_id,))
if hasattr(conn, "commit"):
conn.commit()
def get_order_by_id(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM orders_out WHERE id = {self.ph}", (order_id,))
row = cursor.fetchone()
return self.order_to_dict(row) if row else None
def get_orders_by_user(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"SELECT * FROM orders_out WHERE user_id = {self.ph} ORDER BY created_at DESC",
(user_id,),
)
rows = cursor.fetchall()
return [self.order_to_dict(row) for row in rows]
def create_order_point(self, data):
with get_connection() as conn:
cursor = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cursor.execute(
f"""
INSERT INTO order_out_points
(order_id, destination_id, informatins, point_data, point_hour, point_type)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(
data["order_id"],
data["destination_id"],
data["informatins"],
data["point_data"],
data["point_hour"],
data['point_type']
),
)
# keep behavior similar: no return expected, but commit consistently
if is_postgres():
_ = cursor.fetchone() # consume RETURNING if present
if hasattr(conn, "commit"):
conn.commit()
def update_order_point(self, data):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE order_out_points SET
order_id = {self.ph}, destination_id = {self.ph}, informatins = {self.ph}, point_data = {self.ph}, point_hour = {self.ph}, point_type = {self.ph}
WHERE id = {self.ph}
""",
(
data["order_id"],
data["destination_id"],
data["informatins"],
data["point_data"],
data["point_hour"],
data['point_type'],
data["id"],
),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_order_point(self, point_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"DELETE FROM order_out_points WHERE id = {self.ph}", (point_id,))
if hasattr(conn, "commit"):
conn.commit()
def delete_points_by_order_id(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"DELETE FROM order_out_points WHERE order_id = {self.ph}", (order_id,))
if hasattr(conn, "commit"):
conn.commit()
def get_order_point_by_id(self, point_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM order_out_points WHERE id = {self.ph}", (point_id,))
row = cursor.fetchone()
return self.order_point_to_dict(row) if row else None
def get_order_points_by_order(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"SELECT * FROM order_out_points WHERE order_id = {self.ph} ORDER BY id",
(order_id,),
)
rows = cursor.fetchall()
return [self.order_point_to_dict(row) for row in rows]

View File

@@ -0,0 +1,130 @@
from datetime import datetime, timedelta
from database import get_connection, is_postgres
class Subscription:
def __init__(self):
# Parameter placeholder per backend
self.ph = "%s" if is_postgres() else "?"
def subscription_to_dict(self, row):
return {
"id": row[0],
"user_id": row[1], #company id
"plan": row[2],
"start_date": row[3],
"end_date": row[4],
"status": row[5],
"register_number": row[6],
"created_at": row[7],
}
def create(self, user_id, plan, start_date, end_date, register_number, status="active"):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cursor = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cursor.execute(
f"""
INSERT INTO subscriptions (user_id, plan, start_date, end_date, status, register_number, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(user_id, plan, start_date, end_date, status, register_number, created_at),
)
new_id = cursor.fetchone()[0] if is_postgres() else getattr(cursor, "lastrowid", None)
if hasattr(conn, "commit"):
conn.commit()
return new_id
def get_by_user_id(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
SELECT * FROM subscriptions
WHERE user_id = {self.ph}
ORDER BY start_date DESC
""",
(user_id,),
)
rows = cursor.fetchall()
return [self.subscription_to_dict(row) for row in rows]
def get_by_id(self, subscription_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
SELECT * FROM subscriptions
WHERE id = {self.ph}
""",
(subscription_id,),
)
row = cursor.fetchone()
return self.subscription_to_dict(row) if row else None
def update_status(self, subscription_id, new_status):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE subscriptions
SET status = {self.ph}
WHERE id = {self.ph}
""",
(new_status, subscription_id),
)
if hasattr(conn, "commit"):
conn.commit()
return cursor.rowcount if hasattr(cursor, "rowcount") else 0
def delete(self, subscription_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
DELETE FROM subscriptions
WHERE id = {self.ph}
""",
(subscription_id,),
)
if hasattr(conn, "commit"):
conn.commit()
return cursor.rowcount if hasattr(cursor, "rowcount") else 0
def get_first_2_months_subscription_for_register_number(self, register_number):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
SELECT * FROM subscriptions
WHERE register_number = {self.ph} AND plan = 'first_2_months' AND status = 'active'
""",
(register_number,),
)
row = cursor.fetchone()
return self.subscription_to_dict(row) if row else None
def get_all(self):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT * FROM subscriptions
ORDER BY created_at DESC
"""
)
rows = cursor.fetchall()
return [self.subscription_to_dict(row) for row in rows]
def update_subscription_statuses(self):
now = datetime.now()
subscriptions = self.get_all()
for sub in subscriptions:
end_date = datetime.fromisoformat(sub["end_date"])
days_left = (end_date - now).days
if days_left < 0 and sub["status"] != "expired":
self.update_status(sub["id"], "expired")
elif 0 <= days_left <= 5 and sub["status"] != "less_than_5_days":
self.update_status(sub["id"], "less_than_5_days")

View File

@@ -0,0 +1,87 @@
from datetime import datetime
from database import get_connection, is_postgres
class Transporters:
def __init__(self):
# Parameter placeholder depending on backend
self.ph = "%s" if is_postgres() else "?"
def transporter_to_dict(self, row):
return {
"id": row[0],
"user_id": row[1],
"name": row[2],
"address": row[3],
"register_number": row[4],
"contact_person": row[5],
"phone": row[6],
"email": row[7],
"vat": row[8],
"created_at": row[9]
}
def create_transporter(self, name, address, register_number, contact_person, phone, email, vat, user_id):
created_at = datetime.now().isoformat()
returning = " RETURNING id" if is_postgres() else ""
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
INSERT INTO transporters
(name, address, register_number, contact_person, phone, email, vat, created_at, user_id)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(name, address, register_number, contact_person, phone, email, vat, created_at, user_id),
)
transporter_id = cursor.fetchone()[0] if is_postgres() else cursor.lastrowid
if hasattr(conn, "commit"):
conn.commit()
return transporter_id
def get_all_transporters_by_user(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"SELECT * FROM transporters WHERE user_id = {self.ph} ORDER BY created_at DESC",
(user_id,),
)
rows = cursor.fetchall()
return [self.transporter_to_dict(row) for row in rows]
def get_transporter_by_id(self, transporter_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"SELECT * FROM transporters WHERE id = {self.ph}",
(transporter_id,),
)
row = cursor.fetchone()
return self.transporter_to_dict(row) if row else None
def update_transporter(self, transporter_id, name, address, register_number, contact_person, phone, email, vat):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE transporters SET
name = {self.ph},
address = {self.ph},
register_number = {self.ph},
contact_person = {self.ph},
phone = {self.ph},
email = {self.ph},
vat = {self.ph}
WHERE id = {self.ph}
""",
(name, address, register_number, contact_person, phone, email, vat, transporter_id),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_transporter(self, transporter_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"DELETE FROM transporters WHERE id = {self.ph}", (transporter_id,))
if hasattr(conn, "commit"):
conn.commit()

View File

@@ -0,0 +1,194 @@
from datetime import datetime
from database import get_connection, is_postgres
class Users:
def __init__(self):
self.ph = "%s" if is_postgres() else "?"
def user_to_dict(self, row):
user = {
'id': row[0],
'name': row[1],
'contact_name': row[2],
'email': row[3],
'password_hash': row[4],
'phone': row[5],
'register_number': row[6],
'vat':row[7],
'address': row[8],
'logo_filename': row[9],
'terms': row[10],
'first_order_number': row[11],
'created_at': row[12],
'otp_code': row[13],
'otp_expiration': row[14],
'user_role': row[15]
}
return user
def email_to_dict(self, row):
email = {
'id': row[0],
'user_id': row[1],
'smtp_host': row[2],
'smtp_port': row[3],
'smtp_user': row[4],
'created_at': row[5]
}
return email
def get_user_by_email(self, email):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE email = {self.ph}", (email,))
row = cursor.fetchone()
return self.user_to_dict(row) if row else None
def get_user_by_id(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE id = {self.ph}", (user_id,))
row = cursor.fetchone()
return self.user_to_dict(row) if row else None
def insert_user(self, name, email, password_hash):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cursor = conn.cursor()
returning = "RETURNING id" if is_postgres() else ""
query = f"""
INSERT INTO users (
name, email, password_hash, created_at
) VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}) {returning}
"""
cursor.execute(query, (name, email, password_hash, created_at))
inserted_id = None
if is_postgres():
inserted_id = cursor.fetchone()[0]
else:
inserted_id = cursor.lastrowid
if hasattr(conn, "commit"):
conn.commit()
return inserted_id
def update_user_otp(self, user_id, otp_code, expiration):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users
SET otp_code = {self.ph}, otp_expiration = {self.ph}
WHERE id = {self.ph}
""",
(otp_code, expiration.isoformat(), user_id)
)
if hasattr(conn, "commit"):
conn.commit()
def clear_user_otp(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users
SET otp_code = NULL, otp_expiration = NULL
WHERE id = {self.ph}
""",
(user_id,)
)
if hasattr(conn, "commit"):
conn.commit()
def update_user_password(self, user_id, new_password_hash):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users
SET password_hash = {self.ph}
WHERE id = {self.ph}
""",
(new_password_hash, user_id)
)
if hasattr(conn, "commit"):
conn.commit()
def update_user(self, data):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users
SET name = {self.ph}, contact_name = {self.ph}, email = {self.ph}, phone = {self.ph}, register_number = {self.ph}, vat = {self.ph}, address = {self.ph}, logo_filename = {self.ph}, terms = {self.ph}, first_order_number = {self.ph}
WHERE id = {self.ph}
""",
(
data['name'],
data['contact_name'],
data['email'],
data['phone'],
data['register_number'],
data['vat'],
data['address'],
data['logo_filename'],
data['terms'],
data['first_order_number'],
data['user_id']
)
)
if hasattr(conn, "commit"):
conn.commit()
def update_user_logo(self, data):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users SET logo_filename = {self.ph} WHERE id = {self.ph}
""",
(data['logo_filename'], data['user_id'])
)
if hasattr(conn, "commit"):
conn.commit()
def get_all_users_with_role(self, role='user'):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE user_role = {self.ph}", (role,))
rows = cursor.fetchall()
return [self.user_to_dict(row) for row in rows]
#--- email credentials ---
def insert_email_credentials(self, user_id, smtp_host, smtp_port, smtp_user):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
INSERT INTO email (
user_id, smtp_host, smtp_port, smtp_user, created_at
) VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph})
""",
(user_id, smtp_host, smtp_port, smtp_user, created_at)
)
if hasattr(conn, "commit"):
conn.commit()
def get_email_credentials(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM email WHERE user_id = {self.ph}", (user_id,))
row = cursor.fetchone()
return self.email_to_dict(row) if row else None
def update_email_credentials(self, user_id, smtp_host, smtp_port, smtp_user):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE email SET smtp_host={self.ph}, smtp_port={self.ph}, smtp_user={self.ph} WHERE id = {self.ph}
""",
(smtp_host, smtp_port, smtp_user, user_id)
)
if hasattr(conn, "commit"):
conn.commit()

View File

@@ -0,0 +1,23 @@
# --- Flask API (server) requirements ---
# Web framework
Flask==2.3.3
Flask-Cors==4.0.0
Flask-JWT-Extended==4.6.0
# WSGI server
gunicorn==22.0.0
# Database (Postgres)
psycopg[binary]==3.2.1
# Utilities
python-dotenv==1.0.1
requests==2.32.3
# Features used by your server
geopy==2.4.1
reportlab>=3.6.12
PyPDF2==3.0.1
APScheduler==3.10.4
tzlocal==5.2

View File

@@ -0,0 +1,3 @@
from flask import Blueprint
# Placeholder for blueprint registration if needed dynamically

View File

@@ -0,0 +1,204 @@
from flask import Blueprint, request, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
from utils.email import send_email, send_gmail
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from flask_jwt_extended import decode_token
import datetime
import random
import os
from datetime import timezone
from models.user import Users
auth_bp = Blueprint("auth", __name__)
@auth_bp.route("/register", methods=["POST"])
def register():
users = Users()
data = request.get_json()
name = data.get("name")
email = data.get("email")
password = data.get("password")
if not name or not email or not password:
return jsonify({"error": "Missing required fields"}), 400
existing_user = users.get_user_by_email(email)
if existing_user:
return jsonify({"error": "User already exists"}), 409
password_hash = generate_password_hash(password)
users.insert_user(name, email, password_hash)
return jsonify({"message": "User registered successfully!"}), 201
@auth_bp.route("/login", methods=["POST"])
def login():
users = Users()
data = request.get_json()
email = data.get("email", "").strip().lower()
password = data.get("password", "")
if not email or not password:
return jsonify({"error": "Missing email or password"}), 400
user = users.get_user_by_email(email)
if not user or not check_password_hash(user["password_hash"], password):
return jsonify({"error": "Invalid credentials"}), 401
otp_code = str(random.randint(100000, 999999))
expiration = datetime.datetime.now(timezone.utc) + datetime.timedelta(minutes=10)
users.update_user_otp(user["id"], otp_code, expiration)
send_gmail(
to_email=user["email"],
subject="Your Login Verification Code",
body=f"Your login verification code is: {otp_code}"
)
return jsonify({"message": "Verification code sent to your email."}), 200
@auth_bp.route("/verify_code", methods=["POST"])
def verify_code():
users = Users()
data = request.get_json()
email = data.get("email", "").strip().lower()
code = data.get("code", "")
if not email or not code:
return jsonify({"error": "Missing email or verification code"}), 400
user = users.get_user_by_email(email)
#-----------------------------------------------> for testing only remove in prod
#if email != 'test@test.com':
#-----------------------------------------------> for testing only remove in prod
if not user or user.get("otp_code") != code:
return jsonify({"error": "Invalid code"}), 401
exp = user.get("otp_expiration")
# Normalize to aware UTC datetime for safe comparison across SQLite (string) and Postgres (datetime)
now_utc = datetime.datetime.now(timezone.utc)
if isinstance(exp, str):
try:
exp_dt = datetime.datetime.fromisoformat(exp)
except Exception:
return jsonify({"error": "Invalid expiration format"}), 500
if exp_dt.tzinfo is None:
exp_dt = exp_dt.replace(tzinfo=timezone.utc)
else:
# Assume a datetime object from DB driver
exp_dt = exp
if exp_dt is None:
return jsonify({"error": "Missing expiration"}), 500
if exp_dt.tzinfo is None:
exp_dt = exp_dt.replace(tzinfo=timezone.utc)
if now_utc > exp_dt:
return jsonify({"error": "Verification code has expired"}), 403
users.clear_user_otp(user["id"])
access_token = create_access_token(
identity=str(user["id"]),
expires_delta=datetime.timedelta(hours=12)
)
return jsonify({
"message": "Login successful",
"access_token": access_token
}), 200
@auth_bp.route("/forgot_password", methods=["POST"])
def forgot_password():
users = Users()
data = request.get_json()
email = data.get("email", "").strip().lower()
if not email:
return jsonify({"error": "Email is required"}), 400
user = users.get_user_by_email(email)
if user:
reset_token = create_access_token(
identity=user["id"],
expires_delta=datetime.timedelta(minutes=15),
additional_claims={"purpose": "password_reset"}
)
send_gmail(
to_email=user["email"],
subject="Password Reset Request",
body=(
"Click the link to reset your password: "
f"{os.getenv('FRONTEND_BASE_URL', 'http://127.0.0.1:5100')}/reset_password?token={reset_token}"
)
)
return jsonify({"message": "If this email is registered, a reset link has been sent."}), 200
@auth_bp.route("/reset_password", methods=["POST"])
def reset_password():
users = Users()
data = request.get_json()
token = data.get("token", "")
new_password = data.get("new_password", "")
if not token or not new_password:
return jsonify({"error": "Missing token or new password"}), 400
try:
decoded_token = decode_token(token)
if decoded_token.get("purpose") != "password_reset":
return jsonify({"error": "Invalid token purpose"}), 403
except Exception:
return jsonify({"error": "Invalid or expired token"}), 403
user_id = decoded_token["sub"]
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
password_hash = generate_password_hash(new_password)
users.update_user_password(user_id, password_hash)
return jsonify({"message": "Password has been reset successfully."}), 200
@auth_bp.route("/me", methods=["GET"])
@jwt_required()
def me():
users = Users()
user_id = get_jwt_identity()
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify({
"id": user["id"],
"name": user["name"],
"contact_name": user['contact_name'],
"email": user["email"],
"phone": user["phone"],
"register_number": user["register_number"],
"vat":user["vat"],
"address": user["address"],
"logo_filename": user["logo_filename"],
"terms": user["terms"],
"first_order_number": user["first_order_number"],
"created_at": user["created_at"],
"user_role": user["user_role"]
}), 200
# Validate token endpoint
@auth_bp.route("/validate_token", methods=["GET"])
@jwt_required()
def validate_token():
users = Users()
user_id = get_jwt_identity()
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify({"message": "Token is valid"}), 200

View File

@@ -0,0 +1,65 @@
from flask import Blueprint, request, jsonify
from models.client import Clients
from flask_jwt_extended import jwt_required, get_jwt_identity
clients_bp = Blueprint("clients", __name__, url_prefix="/clients")
@clients_bp.route("/", methods=["GET"])
@jwt_required()
def list_clients():
clients_db = Clients()
user_id = get_jwt_identity()
clients = clients_db.get_all_by_user(user_id)
return jsonify(clients), 200
@clients_bp.route("/", methods=["POST"])
@jwt_required()
def create_client():
clients_db = Clients()
user_id = get_jwt_identity()
data = request.get_json()
client_id = clients_db.create(
user_id=user_id,
name=data["name"],
address=data["address"],
register_number=data["register_number"],
contact_person=data["contact_person"],
phone=data["phone"],
email=data["email"],
vat = data["vat"]
)
return jsonify({"message": "Client created", "id": client_id}), 201
@clients_bp.route("/<int:client_id>", methods=["PUT"])
@jwt_required()
def update_client(client_id):
clients_db = Clients()
data = request.get_json()
name=data["name"]
address=data["address"]
register_number=data["register_number"]
contact_person=data["contact_person"]
phone=data["phone"]
email=data["email"]
vat = data["vat"]
clients_db.update(client_id, name, address, register_number, contact_person, phone, email, vat)
return jsonify({"message": "Client updated"}), 200
@clients_bp.route("/<int:client_id>", methods=["DELETE"])
@jwt_required()
def delete_client(client_id):
clients_db = Clients()
success = clients_db.delete(client_id)
if not success:
return jsonify({"message": "Client not found or unauthorized"}), 404
return jsonify({"message": "Client deleted"}), 200
@clients_bp.route("/<int:client_id>", methods=["GET"])
@jwt_required()
def get_client(client_id):
clients_db = Clients()
client = clients_db.get_by_id(client_id)
if not client:
return jsonify({"message": "Client not found"}), 404
return jsonify(client), 200

View File

@@ -0,0 +1,73 @@
from flask import Blueprint, request, jsonify
from models.destinations import Destinations
from flask_jwt_extended import jwt_required, get_jwt_identity
from utils.maps import AdressCoordinates
destinations_bp = Blueprint("destinations", __name__, url_prefix="/destinations")
@destinations_bp.route("/", methods=["GET"])
@jwt_required()
def list_destinations():
destinations_db = Destinations()
user_id = get_jwt_identity()
destinations = destinations_db.get_all_by_user(user_id)
return jsonify([dict(d) for d in destinations]), 200
@destinations_bp.route("/", methods=["POST"])
@jwt_required()
def create_destination():
destinations_db = Destinations()
user_id = get_jwt_identity()
data = request.get_json()
destination_id = destinations_db.create(user_id, data.get("name"), data.get("address"))
# coordinates = AdressCoordinates(data.get("address"))
# lat_log = coordinates.open_Maps_by_address()
# if lat_log:
# latitude = lat_log['latitude']
# longitude = lat_log['longitude']
# destinations_db.add_gps_coordinates(destination_id, latitude, longitude)
return jsonify({"id": destination_id, "message": "Destination created"}), 201
@destinations_bp.route("/<int:id>", methods=["PUT"])
@jwt_required()
def update_destination(id):
destinations_db = Destinations()
user_id = get_jwt_identity()
data = request.get_json()
destinations_db.update(id, user_id, data.get("name"), data.get("address"))
coordinates = AdressCoordinates(data.get("address"))
lat_log = coordinates.open_Maps_by_address()
if lat_log:
latitude = lat_log['latitude']
longitude = lat_log['longitude']
destinations_db.add_gps_coordinates(id, latitude, longitude)
return jsonify({"message": "Destination updated"}), 200
@destinations_bp.route("/<int:id>", methods=["DELETE"])
@jwt_required()
def delete_destination(id):
destinations_db = Destinations()
success = destinations_db.delete(id)
if not success:
return jsonify({"message": "Destination not found or unauthorized"}), 404
return "", 204
# New route to update GPS coordinates of a destination
@destinations_bp.route("/<int:id>/coordinates", methods=["PUT"])
@jwt_required()
def update_coordinates(id):
destinations_db = Destinations()
data = request.get_json()
latitude = data.get("latitude")
longitude = data.get("longitude")
if latitude is None or longitude is None:
return jsonify({"message": "Latitude and longitude are required"}), 400
success = destinations_db.add_gps_coordinates(id, latitude, longitude)
if not success:
return jsonify({"message": "Failed to update coordinates"}), 404
return jsonify({"message": "Coordinates updated"}), 200

View File

@@ -0,0 +1,259 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.order_out import OrdersOut
from models.user import Users
from models.transporters import Transporters
from datetime import datetime
from utils.pdf import generate_order_pdf
from utils.cancel_order import cancel_order_pdf
import os
from flask import send_from_directory
from utils.email import send_gmail_with_attachment, send_custom_email_with_attachment
orders_bp = Blueprint("orders", __name__, url_prefix="/orders")
@orders_bp.route("/", methods=["POST"])
@jwt_required()
def create_order_route():
user_id = get_jwt_identity()
orders = OrdersOut()
incoming_data = request.json
#here we need to first implement the order pdf
users = Users()
user = users.get_user_by_id(user_id)
logo_filename = user.get('logo_filename')
logo_path = None
if logo_filename:
logo_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "client", "assets", "images", logo_filename)
)
transporters = Transporters()
transporter = transporters.get_transporter_by_id(incoming_data["transporter_id"])
generate_order_pdf(order=incoming_data, user_data=user, transporter_data=transporter, logo_path=logo_path)
#
#try:
order_data = {
'user_id': user_id,
'client_id': incoming_data["client_id"],
'transporter_id': incoming_data["transporter_id"],
'received_price': incoming_data["received_price"],
'paid_price': incoming_data["paid_price"],
'order_number': incoming_data["order_number"],
'created_at': datetime.now(),
'ldb_quantity': incoming_data["ldb_quantity"],
'kg_quantity': incoming_data["kg_quantity"],
'track_reg_number': incoming_data["track_reg_number"],
'trailer_reg_number': incoming_data["trailer_reg_number"],
'products_description': incoming_data["products_description"],
}
order_id = orders.create_order(order_data)
for address in incoming_data["loading_addresses"]:
data = {
"order_id": order_id,
"destination_id": address['loading_address_id'],
"informatins": address['loading_informatins'],
"point_data": address['loading_date'],
"point_hour": address['loading_hour'],
"point_type": "loading"
}
orders.create_order_point(data)
for address in incoming_data["unloading_addresses"]:
data = {
"order_id": order_id,
"destination_id": address['unloading_address_id'],
"informatins": address['unloading_informatins'],
"point_data": address['unloading_date'],
"point_hour": address['unloading_hour'],
"point_type": "unloading"
}
orders.create_order_point(data)
return jsonify({"message": "Order created", "order_id": order_id}), 201
#except Exception as e:
# return jsonify({"error": str(e)}), 400
@orders_bp.route("/<int:order_id>", methods=["PUT"])
@jwt_required()
def update_order_route(order_id):
orders = OrdersOut()
data = request.json
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
if not order:
return jsonify({"error": "Order not found"}), 404
if str(order["user_id"]) != str(user_id):
return jsonify({"error": "Unauthorized"}), 403
try:
orders.update_order({
"id":data.get("id", order['id']),
"client_id": data.get("client_id", order["client_id"]),
"transporter_id": data.get("transporter_id", order["transporter_id"]),
"received_price": data.get("received_price", order["received_price"]),
"paid_price": data.get("paid_price", order["paid_price"]),
"order_number": data.get("order_number", order["order_number"]),
"ldb_quantity": data.get("ldb_quantity", order["ldb_quantity"]),
"kg_quantity": data.get("kg_quantity", order["kg_quantity"]),
"track_reg_number": data.get("track_reg_number", order["track_reg_number"]),
"trailer_reg_number": data.get("trailer_reg_number", order["trailer_reg_number"]),
"products_description": data.get("products_description", order["products_description"]),
})
orders.delete_points_by_order_id(order_id)
for address in data["loading_addresses"]:
loading_data = {
"order_id": order_id,
"destination_id": address['loading_address_id'],
"informatins": address['loading_informatins'],
"point_data": address['loading_date'],
"point_hour": address['loading_hour'],
"point_type": "loading"
}
orders.create_order_point(loading_data)
for address in data["unloading_addresses"]:
unloading_data = {
"order_id": order_id,
"destination_id": address['unloading_address_id'],
"informatins": address['unloading_informatins'],
"point_data": address['unloading_date'],
"point_hour": address['unloading_hour'],
"point_type": "unloading"
}
orders.create_order_point(unloading_data)
#regenerate pdf:
incoming_data = data
users = Users()
user = users.get_user_by_id(user_id)
transporters = Transporters()
transporter = transporters.get_transporter_by_id(incoming_data["transporter_id"])
logo_filename = user.get('logo_filename')
logo_path = None
if logo_filename:
logo_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "client", "assets", "images", logo_filename)
)
generate_order_pdf(order=incoming_data, user_data=user, transporter_data=transporter, logo_path=logo_path)
return jsonify({"message": "Order updated", "order_id": order_id}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/<int:order_id>", methods=["DELETE"])
@jwt_required()
def delete_order_route(order_id):
orders = OrdersOut()
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
if not order:
return jsonify({"error": "Order not found"}), 404
if order["user_id"] != user_id:
return jsonify({"error": "Unauthorized"}), 403
try:
orders.delete_points_by_order_id(order_id)
orders.delete_order(order_id)
return jsonify({"message": "Order deleted"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/list", methods=["GET"])
@jwt_required()
def list_orders():
orders = OrdersOut()
user_id = get_jwt_identity()
try:
user_orders = orders.get_orders_by_user(user_id)
#result = [{"id": order["id"], "order_number": order["order_number"]} for order in user_orders]
return jsonify(user_orders), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/<int:order_id>", methods=["GET"])
@jwt_required()
def get_order(order_id):
orders = OrdersOut()
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
points = orders.get_order_points_by_order(order['id'])
loading_points = []
unloading_points = []
for point in points:
if point['point_type'] == 'loading':
loading_points.append(point)
else:
unloading_points.append(point)
order['loading_points'] = loading_points
order['unloading_points'] = unloading_points
if not order:
return jsonify({"error": "Order not found"}), 404
print(f'{order["user_id"]} {user_id}')
print(f'{type(order["user_id"])} {type(user_id)}')
if order["user_id"] != int(user_id):
return jsonify({"error": "Unauthorized"}), 403
return jsonify(order), 200
@orders_bp.route("/pdfs/<path:filename>", methods=["GET"])
#@jwt_required()
def serve_order_pdf(filename):
pdf_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "generated_pdfs"))
print(pdf_folder)
print(filename)
return send_from_directory(pdf_folder, filename, mimetype="application/pdf")
@orders_bp.route("/send-email/gmail", methods=["POST"])
@jwt_required()
def send_email_with_gmail():
data = request.json
try:
to_email = data["to_email"]
subject = data["subject"]
body = data["body"]
filename = data["filename"]
attachment_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "uploads", filename))
send_gmail_with_attachment(to_email, subject, body, attachment_path)
return jsonify({"message": "Email sent successfully using Gmail"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/send-email/custom", methods=["POST"])
@jwt_required()
def send_email_with_custom_smtp():
data = request.json
try:
to_email = data["to_email"]
subject = data["subject"]
body = data["body"]
filename = data["filename"]
smtp_host = data["smtp_host"]
smtp_port = data["smtp_port"]
smtp_user = data["smtp_user"]
smtp_pass = data["smtp_pass"]
attachment_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "uploads", filename))
send_custom_email_with_attachment(to_email, subject, body, attachment_path, smtp_host, smtp_port, smtp_user, smtp_pass)
return jsonify({"message": "Email sent successfully using custom SMTP"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/cancel/<int:order_id>", methods=["DELETE"])
@jwt_required()
def cancel_order(order_id):
try:
orders = OrdersOut()
order = orders.get_order_by_id(order_id)
user_id = get_jwt_identity()
pdf_name = f'order_{user_id}_{order['order_number']}.pdf'
cancel_order_pdf(pdf_name)
orders.cancel_order(order_id)
return jsonify({"message": "The order was successfully canceled!"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -0,0 +1,163 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.order_in import OrdersIn
from models.transporters import Transporters
from models.user import Users
from datetime import datetime
orders_in_bp = Blueprint("orders_in", __name__, url_prefix="/orders_in")
@orders_in_bp.route("/", methods=["POST"])
@jwt_required()
def create_order_in_route():
user_id = get_jwt_identity()
orders = OrdersIn()
incoming_data = request.json
try:
order_data = {
'user_id': user_id,
'client_id': incoming_data["client_id"],
'received_price': incoming_data["received_price"],
'order_number': incoming_data["order_number"],
'created_at': datetime.now(),
'ldb_quantity': incoming_data["ldb_quantity"],
'kg_quantity': incoming_data["kg_quantity"],
'track_reg_number': incoming_data["track_reg_number"],
'trailer_reg_number': incoming_data["trailer_reg_number"],
'products_description': incoming_data["products_description"],
}
order_id = orders.create_order(order_data)
for address in incoming_data["loading_addresses"]:
data = {
"order_id": order_id,
"destination_id": address['loading_address_id'],
"informatins": address['loading_informatins'],
"point_data": address['loading_date'],
"point_hour": address['loading_hour'],
"point_type": "loading"
}
orders.create_order_point(data)
for address in incoming_data["unloading_addresses"]:
data = {
"order_id": order_id,
"destination_id": address['unloading_address_id'],
"informatins": address['unloading_informatins'],
"point_data": address['unloading_date'],
"point_hour": address['unloading_hour'],
"point_type": "unloading"
}
orders.create_order_point(data)
return jsonify({"message": "Order in created", "order_id": order_id}), 201
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_in_bp.route("/<int:order_id>", methods=["PUT"])
@jwt_required()
def update_order_route(order_id):
orders = OrdersIn()
data = request.json
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
if not order:
return jsonify({"error": "Order in not found"}), 404
if str(order["user_id"]) != str(user_id):
return jsonify({"error": "Unauthorized"}), 403
try:
orders.update_order({
"id":data.get("id", order['id']),
"client_id": data.get("client_id", order["client_id"]),
"received_price": data.get("received_price", order["received_price"]),
"order_number": data.get("order_number", order["order_number"]),
"ldb_quantity": data.get("ldb_quantity", order["ldb_quantity"]),
"kg_quantity": data.get("kg_quantity", order["kg_quantity"]),
"track_reg_number": data.get("track_reg_number", order["track_reg_number"]),
"trailer_reg_number": data.get("trailer_reg_number", order["trailer_reg_number"]),
"products_description": data.get("products_description", order["products_description"]),
"user_id":user_id
})
orders.delete_points_by_order_id(order_id)
for address in data["loading_addresses"]:
loading_data = {
"order_id": order_id,
"destination_id": address['loading_address_id'],
"informatins": address['loading_informatins'],
"point_data": address['loading_date'],
"point_hour": address['loading_hour'],
"point_type": "loading"
}
orders.create_order_point(loading_data)
for address in data["unloading_addresses"]:
unloading_data = {
"order_id": order_id,
"destination_id": address['unloading_address_id'],
"informatins": address['unloading_informatins'],
"point_data": address['unloading_date'],
"point_hour": address['unloading_hour'],
"point_type": "unloading"
}
orders.create_order_point(unloading_data)
return jsonify({"message": "Order updated"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_in_bp.route("/<int:order_id>", methods=["DELETE"])
@jwt_required()
def delete_order_route(order_id):
orders = OrdersIn()
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
if not order:
return jsonify({"error": "Order in not found"}), 404
if str(order["user_id"]) != str(user_id):
return jsonify({"error": "Unauthorized"}), 403
try:
orders.delete_points_by_order_id(order_id)
orders.delete_order(order_id)
return jsonify({"message": "Order in deleted"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_in_bp.route("/list", methods=["GET"])
@jwt_required()
def list_orders():
orders = OrdersIn()
user_id = get_jwt_identity()
try:
user_orders = orders.get_orders_by_user(user_id)
#result = [{"id": order["id"], "order_number": order["order_number"]} for order in user_orders]
return jsonify(user_orders), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_in_bp.route("/<int:order_id>", methods=["GET"])
@jwt_required()
def get_order(order_id):
orders = OrdersIn()
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
points = orders.get_order_points_by_order(order['id'])
loading_points = []
unloading_points = []
for point in points:
if point['point_type'] == 'loading':
loading_points.append(point)
else:
unloading_points.append(point)
order['loading_points'] = loading_points
order['unloading_points'] = unloading_points
if not order:
return jsonify({"error": "Order not found"}), 404
print(f'{order["user_id"]} {user_id}')
print(f'{type(order["user_id"])} {type(user_id)}')
if order["user_id"] != int(user_id):
return jsonify({"error": "Unauthorized"}), 403
return jsonify(order), 200

View File

@@ -0,0 +1,144 @@
import os
from werkzeug.utils import secure_filename
from flask import current_app
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.user import Users
profile_bp = Blueprint("profile", __name__)
@profile_bp.route("/", methods=["GET"])
@jwt_required()
def get_profile():
user_id = get_jwt_identity()
users = Users()
user = users.get_user_by_id(user_id) # Plain SQL method returning dict or None
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify({
"id": user["id"],
"name": user["name"],
"contact_name": user["contact_name"],
"email": user["email"],
"address": user["address"],
"register_number": user["register_number"],
"phone": user["phone"],
"logo_filename": user["logo_filename"],
"terms": user["terms"],
"first_order_number": user["first_order_number"],
"user_role": user["user_role"],
"vat":user["vat"]
})
@profile_bp.route("/", methods=["PUT"])
@jwt_required()
def update_profile():
users = Users()
user_id = get_jwt_identity()
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
data = request.get_json()
update_data = {
"name": data.get("name", user["name"]),
"contact_name": data.get("contact_name", user["contact_name"]),
"email": data.get("email", user["email"]),
"address": data.get("address", user["address"]),
"register_number": data.get("register_number", user["register_number"]),
"phone": data.get("phone", user["phone"]),
"logo_filename": data.get("logo_filename", user["logo_filename"]),
"terms": data.get("terms", user["terms"]),
"first_order_number": data.get("first_order_number", user["first_order_number"]),
"user_id": user_id,
"vat":data.get("vat", user["vat"]),
}
users.update_user(update_data)
return jsonify({"message": "Profile updated successfully"})
@profile_bp.route("/logo", methods=["POST"])
@jwt_required()
def upload_logo():
users = Users()
if 'logo' not in request.files:
return jsonify({"error": "Logo file is required"}), 400
file = request.files['logo']
if file.filename == '':
return jsonify({"error": "No selected file"}), 400
filename = secure_filename(file.filename)
upload_dir = os.path.join(current_app.root_path, '..', 'instance', 'logos')
os.makedirs(upload_dir, exist_ok=True)
filepath = os.path.join(upload_dir, filename)
file.save(filepath)
user_id = get_jwt_identity()
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
# Update the logo filename in DB
users.update_user_logo(user_id, {"logo_filename": filename})
return jsonify({"message": "Logo uploaded", "filename": filename}), 200
@profile_bp.route('/email')
@jwt_required()
def get_email_credentials():
user_id = get_jwt_identity()
users = Users()
credentials = users.get_email_credentials(user_id)
if not credentials:
return jsonify({"error": "Credentials not found"}), 404
return jsonify({
'id': credentials['id'],
'user_id': credentials['user_id'],
'smtp_host': credentials['smtp_host'],
'smtp_port': credentials['smtp_port'],
'smtp_user': credentials['smtp_user'],
'created_at': credentials['created_at']
}), 200
@profile_bp.route('/email', methods=["POST"])
@jwt_required()
def insert_email_credentials():
users = Users()
user_id = get_jwt_identity()
data = request.get_json()
if not data:
return jsonify({"error": "Credentials not found"}), 404
smtp_host = data['smtp_host']
smtp_port = data['smtp_port']
smtp_user = data['smtp_user']
users.insert_email_credentials(user_id, smtp_host, smtp_port, smtp_user)
return jsonify({"message": "Credentials inserted successfully"}), 200
@profile_bp.route('/email', methods=["PUT"])
@jwt_required()
def update_email_credentials():
users = Users()
user_id = get_jwt_identity()
data = request.get_json()
if not data:
return jsonify({"error": "Credentials not found"}), 404
smtp_host = data['smtp_host']
smtp_port = data['smtp_port']
smtp_user = data['smtp_user']
users.update_email_credentials(user_id, smtp_host, smtp_port, smtp_user)
return jsonify({"message": "Credentials updated successfully"}), 200

View File

@@ -0,0 +1,42 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.order_out import OrdersOut # Your plain SQL model
from datetime import datetime
report_bp = Blueprint("report", __name__, url_prefix="/report")
@report_bp.route("/profit", methods=["GET"])
@jwt_required()
def get_profit_report():
try:
user_id = get_jwt_identity()
# Get filters from query params
date_from = request.args.get("date_from")
date_to = request.args.get("date_to")
client_id = request.args.get("client_id")
transporter_id = request.args.get("transporter_id")
# Use the plain SQL method that returns filtered orders list
filters = {
"user_id": user_id,
"date_from": date_from,
"date_to": date_to,
"client_id": client_id,
"transporter_id": transporter_id
}
orders = OrdersOut.get_filtered_orders(filters) # Implement this method in your model
total_received = sum(float(o.get("price_received", 0) or 0) for o in orders)
total_paid = sum(float(o.get("price_paid", 0) or 0) for o in orders)
profit = total_received - total_paid
return jsonify({
"total_received": total_received,
"total_paid": total_paid,
"profit": profit,
"orders_count": len(orders)
})
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -0,0 +1,65 @@
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required, get_jwt_identity
from datetime import datetime, timedelta
from models.subscription import Subscription
from models.user import Users
from datetime import datetime, timedelta
subscription_bp = Blueprint("subscription", __name__, url_prefix="/subscription")
@subscription_bp.route("/", methods=["GET"])
@jwt_required()
def get_subscription():
user_id = get_jwt_identity()
subscription_model = Subscription()
subscriptions = subscription_model.get_by_user_id(user_id)
return jsonify(subscriptions), 200
@subscription_bp.route("/first_2_months", methods=["POST"])
@jwt_required()
def first_2_months_subscription():
user_id = get_jwt_identity()
users_model = Users()
user = users_model.get_user_by_id(user_id)
subscription_model = Subscription()
existing_sub = subscription_model.get_first_2_months_subscription_for_register_number(user["register_number"])
if existing_sub:
return jsonify({"error": "First 2 months subscription already used for this company."}), 400
start_date = datetime.now()
end_date = start_date + timedelta(days=60)
subscription_model.create(user_id, "first_2_months", start_date.isoformat(), end_date.isoformat(), user["register_number"])
return jsonify({"message": "First 2 months subscription created."}), 201
@subscription_bp.route("/one_month", methods=["POST"])
@jwt_required()
def one_month_subscription():
user_id = get_jwt_identity()
start_date = datetime.now()
end_date = start_date + timedelta(days=30)
users_model = Users()
user = users_model.get_user_by_id(user_id)
subscription_model = Subscription()
subscription_model.create(user_id, "monthly", start_date.isoformat(), end_date.isoformat(), user["register_number"])
return jsonify({"message": "1 month subscription created."}), 201
@subscription_bp.route("/one_year", methods=["POST"])
@jwt_required()
def one_year_subscription():
user_id = get_jwt_identity()
start_date = datetime.now()
end_date = start_date + timedelta(days=365)
users_model = Users()
user = users_model.get_user_by_id(user_id)
subscription_model = Subscription()
subscription_model.create(user_id, "yearly", start_date.isoformat(), end_date.isoformat(), user["register_number"])
return jsonify({"message": "1 year subscription created."}), 201

View File

@@ -0,0 +1,74 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.transporters import Transporters
transporters_bp = Blueprint("transporters", __name__, url_prefix="/transporters")
@transporters_bp.route("/", methods=["GET"])
@jwt_required()
def list_transporters():
user_id = get_jwt_identity()
transporters_db = Transporters()
transporters = transporters_db.get_all_transporters_by_user(user_id)
return jsonify(transporters), 200
@transporters_bp.route("/", methods=["POST"])
@jwt_required()
def create_transporter():
transporters_db = Transporters()
data = request.get_json()
user_id = get_jwt_identity()
transporter_id = transporters_db.create_transporter(
user_id=user_id,
name=data.get("name"),
address=data.get("address"),
register_number=data.get("register_number"),
contact_person=data.get("contact_person"),
phone=data.get("phone"),
email=data.get("email"),
vat = data.get("vat")
)
transporter = transporters_db.get_transporter_by_id(transporter_id)
return jsonify(transporter), 201
@transporters_bp.route("/<int:transporter_id>", methods=["PUT"])
@jwt_required()
def update_transporter(transporter_id):
transporters_db = Transporters()
user_id = get_jwt_identity()
data = request.get_json()
transporter = transporters_db.get_transporter_by_id(transporter_id)
if not transporter:
return jsonify({"error": "Transporter not found"}), 404
transporters_db.update_transporter(
transporter_id=transporter_id,
name=data.get("name"),
address=data.get("address"),
register_number=data.get("register_number"),
contact_person=data.get("contact_person"),
phone=data.get("phone"),
email=data.get("email"),
vat=data.get("vat")
)
updated_transporter = transporters_db.get_transporter_by_id(transporter_id)
return jsonify(updated_transporter), 200
@transporters_bp.route("/<int:transporter_id>", methods=["DELETE"])
@jwt_required()
def delete_transporter(transporter_id):
transporters_db = Transporters()
user_id = get_jwt_identity()
transporter = transporters_db.get_transporter_by_id(transporter_id)
if not transporter:
return jsonify({"error": "Transporter not found"}), 404
transporters_db.delete_transporter(transporter_id)
return jsonify({"message": "Transporter deleted"}), 200
@transporters_bp.route("/<int:transporter_id>", methods=["GET"])
@jwt_required()
def get_transporter(transporter_id):
transporters_db = Transporters()
transporter = transporters_db.get_transporter_by_id(transporter_id)
if not transporter:
return jsonify({"error": "Transporter not found"}), 404
return jsonify(transporter), 200

View File

@@ -0,0 +1,146 @@
-- Users table
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
contact_name TEXT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
phone TEXT,
register_number TEXT,
vat TEXT,
address TEXT,
logo_filename TEXT,
terms TEXT,
first_order_number INTEGER DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
otp_code TEXT,
otp_expiration TIMESTAMPTZ,
user_role TEXT NOT NULL DEFAULT 'user' CHECK (user_role IN ('user', 'admin'))
);
-- Clients table
CREATE TABLE IF NOT EXISTS clients (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
register_number TEXT,
contact_person TEXT,
phone TEXT,
email TEXT,
vat TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Transporters table
CREATE TABLE IF NOT EXISTS transporters (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
register_number TEXT,
contact_person TEXT,
phone TEXT,
email TEXT,
vat TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Destinations table
CREATE TABLE IF NOT EXISTS destinations (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
latitude TEXT,
longitude TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Orders out table
CREATE TABLE IF NOT EXISTS orders_out (
id SERIAL PRIMARY KEY,
order_number TEXT NOT NULL,
user_id INTEGER NOT NULL,
client_id INTEGER NOT NULL,
transporter_id INTEGER NOT NULL,
products_description TEXT,
ldb_quantity DOUBLE PRECISION,
kg_quantity DOUBLE PRECISION,
track_reg_number TEXT,
trailer_reg_number TEXT,
received_price DOUBLE PRECISION,
paid_price DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
order_status TEXT NOT NULL DEFAULT 'active' CHECK (order_status IN ('active', 'inactive', 'cancelled')),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE,
FOREIGN KEY(transporter_id) REFERENCES transporters(id) ON DELETE CASCADE
);
-- Orders in table
CREATE TABLE IF NOT EXISTS orders_in (
id SERIAL PRIMARY KEY,
order_number TEXT NOT NULL,
user_id INTEGER NOT NULL,
client_id INTEGER NOT NULL,
products_description TEXT,
ldb_quantity DOUBLE PRECISION,
kg_quantity DOUBLE PRECISION,
track_reg_number TEXT,
trailer_reg_number TEXT,
received_price DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE
);
-- Order In Points (loading/unloading) table
CREATE TABLE IF NOT EXISTS order_in_points (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
informatins TEXT,
point_data TEXT,
point_hour TEXT,
point_type TEXT NOT NULL CHECK (point_type IN ('loading', 'unloading')),
FOREIGN KEY(order_id) REFERENCES orders_in(id) ON DELETE CASCADE
);
-- Order In Points (loading/unloading) table
CREATE TABLE IF NOT EXISTS order_out_points (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
informatins TEXT,
point_data TEXT,
point_hour TEXT,
point_type TEXT NOT NULL CHECK (point_type IN ('loading', 'unloading')),
FOREIGN KEY(order_id) REFERENCES orders_out(id) ON DELETE CASCADE
);
-- Subscriptions table
CREATE TABLE IF NOT EXISTS subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
plan TEXT NOT NULL CHECK (plan IN ('first_2_months', 'monthly', 'yearly')),
start_date TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_date TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('active', 'expired', 'cancelled')),
register_number TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS email (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
smtp_host TEXT,
smtp_port TEXT,
smtp_user TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,146 @@
-- Users table
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
contact_name TEXT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
phone TEXT,
register_number TEXT,
vat TEXT,
address TEXT,
logo_filename TEXT,
terms TEXT,
first_order_number INTEGER DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
otp_code TEXT,
otp_expiration TIMESTAMPTZ,
user_role TEXT NOT NULL DEFAULT 'user' CHECK (user_role IN ('user', 'admin'))
);
-- Clients table
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
register_number TEXT,
contact_person TEXT,
phone TEXT,
email TEXT,
vat TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Transporters table
CREATE TABLE IF NOT EXISTS transporters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
register_number TEXT,
contact_person TEXT,
phone TEXT,
email TEXT,
vat TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Destinations table
CREATE TABLE IF NOT EXISTS destinations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
latitude TEXT,
longitude TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Orders out table
CREATE TABLE IF NOT EXISTS orders_out (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_number TEXT NOT NULL,
user_id INTEGER NOT NULL,
client_id INTEGER NOT NULL,
transporter_id INTEGER NOT NULL,
products_description TEXT,
ldb_quantity DOUBLE PRECISION,
kg_quantity DOUBLE PRECISION,
track_reg_number TEXT,
trailer_reg_number TEXT,
received_price DOUBLE PRECISION,
paid_price DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
order_status TEXT NOT NULL DEFAULT 'active' CHECK (order_status IN ('active', 'inactive', 'cancelled')),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE,
FOREIGN KEY(transporter_id) REFERENCES transporters(id) ON DELETE CASCADE
);
-- Orders in table
CREATE TABLE IF NOT EXISTS orders_in (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_number TEXT NOT NULL,
user_id INTEGER NOT NULL,
client_id INTEGER NOT NULL,
products_description TEXT,
ldb_quantity DOUBLE PRECISION,
kg_quantity DOUBLE PRECISION,
track_reg_number TEXT,
trailer_reg_number TEXT,
received_price DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE
);
-- Order In Points (loading/unloading) table
CREATE TABLE IF NOT EXISTS order_in_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
informatins TEXT,
point_data TEXT,
point_hour TEXT,
point_type TEXT NOT NULL CHECK (point_type IN ('loading', 'unloading')),
FOREIGN KEY(order_id) REFERENCES orders_in(id) ON DELETE CASCADE
);
-- Order In Points (loading/unloading) table
CREATE TABLE IF NOT EXISTS order_out_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
informatins TEXT,
point_data TEXT,
point_hour TEXT,
point_type TEXT NOT NULL CHECK (point_type IN ('loading', 'unloading')),
FOREIGN KEY(order_id) REFERENCES orders_out(id) ON DELETE CASCADE
);
-- Subscriptions table
CREATE TABLE IF NOT EXISTS subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
plan TEXT NOT NULL CHECK (plan IN ('first_2_months', 'monthly', 'yearly')),
start_date TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_date TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('active', 'expired', 'cancelled')),
register_number TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS email (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
smtp_host TEXT,
smtp_port TEXT,
smtp_user TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,62 @@
import os
from PyPDF2 import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
GENERATED_FOLDER = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "generated_pdfs")
)
def create_watermark_pdf(watermark_path, text="CANCELLED"):
c = canvas.Canvas(watermark_path, pagesize=A4)
c.setFont("Helvetica-Bold", 80)
c.setFillGray(0.6) # Light gray text
c.saveState()
c.translate(300, 400)
c.rotate(45)
c.drawCentredString(0, 0, text)
c.restoreState()
c.save()
def apply_watermark(input_pdf_path, output_pdf_path, watermark_pdf_path):
reader = PdfReader(input_pdf_path)
watermark = PdfReader(watermark_pdf_path).pages[0]
writer = PdfWriter()
for page in reader.pages:
page.merge_page(watermark)
writer.add_page(page)
with open(output_pdf_path, "wb") as f:
writer.write(f)
def cancel_order_pdf(order_filename):
# File paths
input_pdf_path = os.path.join(GENERATED_FOLDER, order_filename)
output_pdf_path = input_pdf_path
watermark_pdf_path = os.path.join(GENERATED_FOLDER, "temp_watermark.pdf")
print(watermark_pdf_path)
# Check if file exists
if not os.path.isfile(input_pdf_path):
raise FileNotFoundError(f"Original order PDF not found: {input_pdf_path}")
# Create watermark and apply it
create_watermark_pdf(watermark_pdf_path, text="CANCELLED")
apply_watermark(input_pdf_path, output_pdf_path, watermark_pdf_path)
# Optionally remove temp watermark
os.remove(watermark_pdf_path)
return output_pdf_path
# Example usage:
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python cancel_order.py <order_filename>")
else:
try:
result_path = cancel_order_pdf(sys.argv[1])
print(f"Cancelled PDF created: {result_path}")
except Exception as e:
print(f"Error: {e}")

View File

@@ -0,0 +1,139 @@
import smtplib
from email.message import EmailMessage
import os
def send_email(to_email, subject, body):
smtp_host = os.environ.get("SMTP_HOST")
smtp_port = int(os.environ.get("SMTP_PORT", 587))
smtp_user = os.environ.get("SMTP_USER")
smtp_pass = os.environ.get("SMTP_PASS")
sender_email = os.environ.get("SMTP_FROM", smtp_user)
if not all([smtp_host, smtp_port, smtp_user, smtp_pass]):
raise ValueError("SMTP config incomplete in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
# Send email with attachment
def send_email_with_attachment(to_email, subject, body, attachment_path):
smtp_host = os.environ.get("SMTP_HOST")
smtp_port = int(os.environ.get("SMTP_PORT", 587))
smtp_user = os.environ.get("SMTP_USER")
smtp_pass = os.environ.get("SMTP_PASS")
sender_email = os.environ.get("SMTP_FROM", smtp_user)
if not all([smtp_host, smtp_port, smtp_user, smtp_pass]):
raise ValueError("SMTP config incomplete in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
if attachment_path and os.path.isfile(attachment_path):
with open(attachment_path, "rb") as f:
file_data = f.read()
file_name = os.path.basename(attachment_path)
msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name)
else:
raise FileNotFoundError(f"Attachment file not found: {attachment_path}")
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
# Send email using Gmail directly
def send_gmail(to_email, subject, body):
smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_user = 'macamete.robert@gmail.com'
smtp_pass = 'advx yqlv jkaa czvr'
sender_email = 'macamete.robert@gmail.com'
if not all([smtp_user, smtp_pass]):
raise ValueError("GMAIL_USER and GMAIL_PASS must be set in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
# Send email with attachment using Gmail directly
def send_gmail_with_attachment(to_email, subject, body, attachment_path):
smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_user = os.environ.get("GMAIL_USER")
smtp_pass = os.environ.get("GMAIL_PASS")
sender_email = smtp_user
if not all([smtp_user, smtp_pass]):
raise ValueError("GMAIL_USER and GMAIL_PASS must be set in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
if attachment_path and os.path.isfile(attachment_path):
with open(attachment_path, "rb") as f:
file_data = f.read()
file_name = os.path.basename(attachment_path)
msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name)
else:
raise FileNotFoundError(f"Attachment file not found: {attachment_path}")
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
# Send email with attachment
def send_custom_email_with_attachment(to_email, subject, body, attachment_path, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS):
smtp_host = SMTP_HOST
smtp_port = int(SMTP_PORT)
smtp_user = SMTP_USER
smtp_pass = SMTP_PASS
sender_email = smtp_user
if not all([smtp_host, smtp_port, smtp_user, smtp_pass]):
raise ValueError("SMTP config incomplete in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
if attachment_path and os.path.isfile(attachment_path):
with open(attachment_path, "rb") as f:
file_data = f.read()
file_name = os.path.basename(attachment_path)
msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name)
else:
raise FileNotFoundError(f"Attachment file not found: {attachment_path}")
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)

View File

@@ -0,0 +1,28 @@
from geopy.geocoders import Nominatim
class AdressCoordinates:
def __init__(self, address):
self.addess = address
def open_Maps_by_address(self):
address = self.addess
if not address:
return
#try:
geolocator = Nominatim(user_agent="flet_Maps_app")
location = geolocator.geocode(address)
print(location)
if location:
latitude = location.latitude
longitude = location.longitude
Maps_url = f"https://www.google.com/maps/search/?api=1&query={latitude},{longitude}"
return {
'latitude' : latitude,
'longitude' : longitude,
'Maps_url': Maps_url
}
#except Exception as ex:
# print(ex)

View File

@@ -0,0 +1,199 @@
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.platypus import Table, TableStyle, Paragraph, SimpleDocTemplate, Spacer, Image
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
import io
import os
import logging
# --- helpers ---------------------------------------------------------------
def _resolve_logo_path(logo_path: str | None) -> str | None:
"""Return a readable logo path or None. Tries multiple fallbacks.
This avoids crashes when a caller passes a path that exists only in the
client container (e.g., '/client/assets/...') while we're running in the
server container on Fly.
"""
here = os.path.dirname(__file__)
project_root = os.path.abspath(os.path.join(here, "..", ".."))
candidates = []
# 1) if caller provided a path, try it as-is
if logo_path:
candidates.append(logo_path)
# also try without a leading slash inside our image tree
stripped = logo_path.lstrip("/\\")
candidates.append(os.path.join(project_root, stripped))
# 2) Known locations in this repo layout
candidates.append(os.path.join(project_root, "client", "assets", "images", "truck_logo_black.png"))
candidates.append(os.path.join(project_root, "assets", "images", "truck_logo_black.png"))
# 3) Allow override via env
env_path = os.getenv("DEFAULT_LOGO_PATH")
if env_path:
candidates.insert(0, env_path)
for p in candidates:
try:
if p and os.path.isfile(p):
return p
except Exception:
# some paths may be malformed on certain platforms
continue
return None
def generate_order_pdf(order, user_data, transporter_data, logo_path, save_to_disk=True):
#print(order)
#print(f'user data: {user_data}')
if 'address' in user_data:
address = user_data.get("address")
if address:
address = address.replace(" %",", ")
else:
address = ''
else:
address=''
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=A4)
elements = []
styles = getSampleStyleSheet()
# Resolve logo path robustly across local/dev and Fly
logo_path = _resolve_logo_path(logo_path)
# Prepare texts
user_text = f"""<b>{user_data.get("name", "")}</b><br/>
{user_data.get("register_number", "")}<br/>
{user_data.get("vat", "")}<br/>
{address}<br/>
{""}<br/>
{user_data.get("contact_name", "")}<br/>
{user_data.get("phone", "")}<br/>
{user_data.get("email", "")}
"""
transporter_text = f"""<b>{transporter_data.get("name", "")}</b><br/>
{transporter_data.get("contact_person", "")}<br/>
{transporter_data.get("phone", "")}<br/>
{transporter_data.get("email", "")}
"""
# Logo (centered), tolerate missing file
logo = None
if logo_path:
try:
logo = Image(logo_path, width=120, mask='auto', height=60)
logo.hAlign = 'CENTER'
except Exception as e:
logging.warning("PDF: failed to load logo at %s: %s", logo_path, e)
logo = None
# Top section: transporter - logo - user
top_section = Table([
[
Paragraph(transporter_text, styles['Normal']),
logo,
Paragraph(user_text, styles['Normal'])
]
], colWidths=[200, 150, 200])
top_section.setStyle(TableStyle([
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("ALIGN", (0, 0), (0, 0), "LEFT"),
("ALIGN", (1, 0), (1, 0), "CENTER"),
("ALIGN", (2, 0), (2, 0), "RIGHT"),
("LEFTPADDING", (0, 0), (0, 0), 20),
]))
elements.append(top_section)
elements.append(Spacer(1, 12))
# Order number and current date (centered, vertically stacked)
from datetime import datetime
header_info = Table([
[Paragraph(f"<b>Loading Order</b>: {order['order_number']}", styles["Normal"])],
[Paragraph(f"Date: {datetime.now().strftime('%d/%m/%Y')}", styles["Normal"])]
], colWidths=[450])
header_info.setStyle(TableStyle([
("ALIGN", (0, 0), (-1, -1), "CENTER")
]))
elements.append(header_info)
elements.append(Spacer(1, 12))
# Order Summary Table
elements.append(Paragraph("Summary", styles['Heading3']))
summary_data = [
["Details", Paragraph("Values", styles["Normal"])],
["Truck Reg. No.", Paragraph(str(order["track_reg_number"]), styles["Normal"])],
["Trailer Reg. No.", Paragraph(str(order["trailer_reg_number"]), styles["Normal"])],
["Product Description", Paragraph(str(order["products_description"]), styles["Normal"])],
["LDM", Paragraph(str(order["ldb_quantity"]), styles["Normal"])],
["KG", Paragraph(str(order["kg_quantity"]), styles["Normal"])],
["Price", Paragraph(str(order["paid_price"]), styles["Normal"])],
]
summary_table = Table(summary_data, colWidths=[150, 350])
summary_table.setStyle(TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("WORDWRAP", (0, 0), (-1, -1), "CJK"),
("ALIGN", (0, 0), (-1, -1), "LEFT"),
]))
elements.append(summary_table)
elements.append(Spacer(1, 24))
# Loading Points
elements.append(Paragraph("Loading Sequence", styles['Heading3']))
loading_data = [[Paragraph("Address", styles["Normal"]), Paragraph("Date & Hour", styles["Normal"]), Paragraph("Instructions", styles["Normal"])]]
for l in order["loading_addresses"]:
loading_data.append([
Paragraph(f"{l["loading_address_name"]}: {l["loading_address"]}", styles["Normal"]),
Paragraph(f"{l['loading_date']} {l['loading_hour']}", styles["Normal"]),
Paragraph(str(l["loading_informatins"]), styles["Normal"])
])
loading_table = Table(loading_data, colWidths=[200, 100, 200])
loading_table.setStyle(TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("WORDWRAP", (0, 0), (-1, -1), "CJK"),
("ALIGN", (0, 0), (-1, -1), "LEFT"),
]))
elements.append(loading_table)
elements.append(Spacer(1, 24))
# Unloading Points
elements.append(Paragraph("Unloading Sequence", styles['Heading3']))
unloading_data = [[Paragraph("Address", styles["Normal"]), Paragraph("Date & Hour", styles["Normal"]), Paragraph("Instructions", styles["Normal"])]]
for u in order["unloading_addresses"]:
unloading_data.append([
Paragraph(f"{u["unloading_address_name"]}: {u["unloading_address"]}", styles["Normal"]),
Paragraph(f"{u['unloading_date']} {u['unloading_hour']}", styles["Normal"]),
Paragraph(str(u["unloading_informatins"]), styles["Normal"])
])
unloading_table = Table(unloading_data, colWidths=[200, 100, 200])
unloading_table.setStyle(TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("WORDWRAP", (0, 0), (-1, -1), "CJK"),
("ALIGN", (0, 0), (-1, -1), "LEFT"),
]))
elements.append(unloading_table)
elements.append(Spacer(1, 24))
elements.append(Paragraph("Terms and Conditions", styles["Heading3"]),)
elements.append(
Paragraph(str(order["terms"]), styles["Normal"])
)
doc.build(elements)
buffer.seek(0)
if save_to_disk:
save_path=f"generated_pdfs/order_{order['user_id']}_{order['order_number']}.pdf"
with open(save_path, "wb") as f:
f.write(buffer.getvalue())
return buffer