commit 876ddec94a31712b178f1a27112e0408fb4b2a1f Author: Marius Robert Macamete Date: Sun Aug 31 17:55:26 2025 +0300 init commit diff --git a/transportmanager/client/.DS_Store b/transportmanager/client/.DS_Store new file mode 100644 index 0000000..f8f2f8f Binary files /dev/null and b/transportmanager/client/.DS_Store differ diff --git a/transportmanager/client/assets/.DS_Store b/transportmanager/client/assets/.DS_Store new file mode 100644 index 0000000..a99a69a Binary files /dev/null and b/transportmanager/client/assets/.DS_Store differ diff --git a/transportmanager/client/assets/favicon.png b/transportmanager/client/assets/favicon.png new file mode 100644 index 0000000..d1c097e Binary files /dev/null and b/transportmanager/client/assets/favicon.png differ diff --git a/transportmanager/client/assets/icons/.DS_Store b/transportmanager/client/assets/icons/.DS_Store new file mode 100644 index 0000000..439298f Binary files /dev/null and b/transportmanager/client/assets/icons/.DS_Store differ diff --git a/transportmanager/client/assets/icons/loading-animation.png b/transportmanager/client/assets/icons/loading-animation.png new file mode 100644 index 0000000..d5c47db Binary files /dev/null and b/transportmanager/client/assets/icons/loading-animation.png differ diff --git a/transportmanager/client/assets/images/.DS_Store b/transportmanager/client/assets/images/.DS_Store new file mode 100644 index 0000000..22b119b Binary files /dev/null and b/transportmanager/client/assets/images/.DS_Store differ diff --git a/transportmanager/client/assets/images/image_placeholder.png b/transportmanager/client/assets/images/image_placeholder.png new file mode 100644 index 0000000..b1fdd31 Binary files /dev/null and b/transportmanager/client/assets/images/image_placeholder.png differ diff --git a/transportmanager/client/assets/images/truck_logo copy.png b/transportmanager/client/assets/images/truck_logo copy.png new file mode 100644 index 0000000..bca86cb Binary files /dev/null and b/transportmanager/client/assets/images/truck_logo copy.png differ diff --git a/transportmanager/client/assets/images/truck_logo.png b/transportmanager/client/assets/images/truck_logo.png new file mode 100644 index 0000000..cfa984d Binary files /dev/null and b/transportmanager/client/assets/images/truck_logo.png differ diff --git a/transportmanager/client/assets/images/truck_logo_black.png b/transportmanager/client/assets/images/truck_logo_black.png new file mode 100644 index 0000000..d5c47db Binary files /dev/null and b/transportmanager/client/assets/images/truck_logo_black.png differ diff --git a/transportmanager/client/assets/images/user_logo_1_beef cow_0.jpg b/transportmanager/client/assets/images/user_logo_1_beef cow_0.jpg new file mode 100644 index 0000000..3b6f918 Binary files /dev/null and b/transportmanager/client/assets/images/user_logo_1_beef cow_0.jpg differ diff --git a/transportmanager/client/assets/images/user_logo_1_bell-icon-with-one-notification-1.png b/transportmanager/client/assets/images/user_logo_1_bell-icon-with-one-notification-1.png new file mode 100644 index 0000000..85aa02e Binary files /dev/null and b/transportmanager/client/assets/images/user_logo_1_bell-icon-with-one-notification-1.png differ diff --git a/transportmanager/client/assets/images/user_logo_1_gold-medal_5611108.png b/transportmanager/client/assets/images/user_logo_1_gold-medal_5611108.png new file mode 100644 index 0000000..7fdba3c Binary files /dev/null and b/transportmanager/client/assets/images/user_logo_1_gold-medal_5611108.png differ diff --git a/transportmanager/client/assets/images/user_logo_1_logo.png b/transportmanager/client/assets/images/user_logo_1_logo.png new file mode 100644 index 0000000..a82b6e0 Binary files /dev/null and b/transportmanager/client/assets/images/user_logo_1_logo.png differ diff --git a/transportmanager/client/assets/images/user_logo_1_panel.png b/transportmanager/client/assets/images/user_logo_1_panel.png new file mode 100644 index 0000000..102c005 Binary files /dev/null and b/transportmanager/client/assets/images/user_logo_1_panel.png differ diff --git a/transportmanager/client/assets/images/user_logo_2_bell-icon-with-one-notification-1.png b/transportmanager/client/assets/images/user_logo_2_bell-icon-with-one-notification-1.png new file mode 100644 index 0000000..85aa02e Binary files /dev/null and b/transportmanager/client/assets/images/user_logo_2_bell-icon-with-one-notification-1.png differ diff --git a/transportmanager/client/assets/images/user_logo_2_png-clipart-computer-icons-task-action-item-others-miscellaneous-angle.png b/transportmanager/client/assets/images/user_logo_2_png-clipart-computer-icons-task-action-item-others-miscellaneous-angle.png new file mode 100644 index 0000000..7ceacfd Binary files /dev/null and b/transportmanager/client/assets/images/user_logo_2_png-clipart-computer-icons-task-action-item-others-miscellaneous-angle.png differ diff --git a/transportmanager/client/config.py b/transportmanager/client/config.py new file mode 100644 index 0000000..588e6d9 --- /dev/null +++ b/transportmanager/client/config.py @@ -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")) \ No newline at end of file diff --git a/transportmanager/client/main.py b/transportmanager/client/main.py new file mode 100644 index 0000000..998778a --- /dev/null +++ b/transportmanager/client/main.py @@ -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, +) \ No newline at end of file diff --git a/transportmanager/client/pages/admin_page.py b/transportmanager/client/pages/admin_page.py new file mode 100644 index 0000000..4e96ef9 --- /dev/null +++ b/transportmanager/client/pages/admin_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/admin_subscriptions_page.py b/transportmanager/client/pages/admin_subscriptions_page.py new file mode 100644 index 0000000..6b58474 --- /dev/null +++ b/transportmanager/client/pages/admin_subscriptions_page.py @@ -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, + ) + ) \ No newline at end of file diff --git a/transportmanager/client/pages/admin_tenants_page.py b/transportmanager/client/pages/admin_tenants_page.py new file mode 100644 index 0000000..20b6b7b --- /dev/null +++ b/transportmanager/client/pages/admin_tenants_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/archive_in_page.py b/transportmanager/client/pages/archive_in_page.py new file mode 100644 index 0000000..ee5eaae --- /dev/null +++ b/transportmanager/client/pages/archive_in_page.py @@ -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 + ] + ) + ) \ No newline at end of file diff --git a/transportmanager/client/pages/archive_page.py b/transportmanager/client/pages/archive_page.py new file mode 100644 index 0000000..f8aeaab --- /dev/null +++ b/transportmanager/client/pages/archive_page.py @@ -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 + ] + ) + ) \ No newline at end of file diff --git a/transportmanager/client/pages/auth_page.py b/transportmanager/client/pages/auth_page.py new file mode 100644 index 0000000..a6243fa --- /dev/null +++ b/transportmanager/client/pages/auth_page.py @@ -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, + ) \ No newline at end of file diff --git a/transportmanager/client/pages/clients_page.py b/transportmanager/client/pages/clients_page.py new file mode 100644 index 0000000..351e77d --- /dev/null +++ b/transportmanager/client/pages/clients_page.py @@ -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, + ) \ No newline at end of file diff --git a/transportmanager/client/pages/dashboard_page.py b/transportmanager/client/pages/dashboard_page.py new file mode 100644 index 0000000..1eca617 --- /dev/null +++ b/transportmanager/client/pages/dashboard_page.py @@ -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 + ) + + \ No newline at end of file diff --git a/transportmanager/client/pages/destinations_page.py b/transportmanager/client/pages/destinations_page.py new file mode 100644 index 0000000..4292a3c --- /dev/null +++ b/transportmanager/client/pages/destinations_page.py @@ -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, + ) \ No newline at end of file diff --git a/transportmanager/client/pages/forgot_password_page.py b/transportmanager/client/pages/forgot_password_page.py new file mode 100644 index 0000000..ee57b0f --- /dev/null +++ b/transportmanager/client/pages/forgot_password_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/login_page.py b/transportmanager/client/pages/login_page.py new file mode 100644 index 0000000..7efb82f --- /dev/null +++ b/transportmanager/client/pages/login_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/orders_edit_page.py b/transportmanager/client/pages/orders_edit_page.py new file mode 100644 index 0000000..6cc36a4 --- /dev/null +++ b/transportmanager/client/pages/orders_edit_page.py @@ -0,0 +1,1375 @@ +import flet as ft +import datetime +import requests +import time +from pages.view_page import ViewPage +#from pages.archive_page import ArchivePage +from config import API_BASE_URL + +class OrdersEditPage: + def __init__(self, page: ft.Page, dashbaord, archive_page): + self.page = page + self.dashboard = dashbaord + self.archive_page = archive_page + self.order_id = self.page.session.get('order_id') + self.order = self.get_order() + print('Edit order') + #print(self.order_id) + #print(self.order) + #print(type(self.order_id)) + + + self.selected_client_id = self.order['client_id'] + + self.all_clients = self.get_all_clients() + self.filtered_clients = self.all_clients.copy() + self.client_search_field = ft.TextField(label="Search Clients...", on_change=self.on_searching_client) + self.client_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for client in self.all_clients + ] + ) + + for client in self.all_clients: + if client["id"] == self.selected_client_id: + self.client_search_field.value = client['name'] + + self.all_transporters = self.get_all_transporters() + self.filtered_transporters = self.all_transporters.copy() + self.selected_transporter_id = self.order['transporter_id'] + self.transporter_search_field = ft.TextField(label="Search Transporters...", on_change=self.on_searching_transporter) + self.transporter_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ft.Text(transporter["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if transporter["id"] == self.selected_transporter_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=transporter: self.on_transporter_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for transporter in self.all_transporters + ] + ) + + for transporter in self.all_transporters: + if transporter["id"] == self.selected_transporter_id: + self.transporter_search_field.value = transporter['name'] + + self.all_addresses = self.get_all_addresses() + self.selected_loading_address_id = None + self.filtered_addresses = self.all_addresses.copy() + self.loading_address_search_field = ft.TextField(label="Search Loading Address...", on_change=self.on_searching_loading_address) + self.loading_address_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ + ft.Text(address["name"], expand=True), + ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click=lambda e, t=address: self.on_location_btn_click(t)) + ], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=address: self.on_loading_address_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for address in self.all_addresses + ] + ) + + self.loading_informations = ft.TextField( + label="Loading Instructions", + min_lines=3, + max_lines=7, + multiline=True, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.loading_date = ft.TextField(label="Date", expand=True, read_only=True) + + self.loading_hour = ft.TextField(label="Hour", expand=True, read_only=True) + + self.loading = ft.ListView( + spacing=10, + ) + + self.selected_unloading_address_id = None + self.filtered_addresses_ul = self.all_addresses.copy() + self.unloading_address_search_field = ft.TextField(label="Search Unloading Address...", on_change=self.on_searching_unloading_address) + self.unloading_address_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ + ft.Text(address["name"], expand=True), + ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click=lambda e, t=address: self.on_location_btn_click(t)) + ], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=address: self.on_unloading_address_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for address in self.all_addresses + ] + ) + + self.unloading_informations = ft.TextField( + label="Unloading Instructions", + min_lines=3, + max_lines=7, + multiline=True, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.unloading_date = ft.TextField(label="Date", expand=True, read_only=True) + + self.unloading_hour = ft.TextField(label="Hour", expand=True, read_only=True) + + self.unloading = ft.ListView( + spacing=10, + ) + + self.ldm_quantity = ft.TextField( + expand=True, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value=self.order['ldb_quantity'] + ) + + self.kg_quantity = ft.TextField( + expand=True, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value=self.order['kg_quantity'] + ) + + self.track_reg_number = ft.TextField( + label="Track Reg. Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value=self.order['track_reg_number'] + ) + + self.trailer_reg_number = ft.TextField( + label="Trailer Reg. Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value=self.order['trailer_reg_number'] + ) + + self.received_price = ft.TextField( + label="Price Received - visible only to you!", + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value=self.order['received_price'] + ) + + self.paid_price = ft.TextField( + label="Price Paid", + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value = self.order['paid_price'] + ) + + self.first_order_number = self.page.session.get("first_order_number") + last_orders = self.load_orders() + if len(last_orders) == 0: + number = f'First Order: {self.first_order_number}' + else: + ln = last_orders[0]['order_number'] + #print(last_orders) + number = f'Last used: {ln}' + self.order_number = ft.TextField( + label=f"{number}", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value = self.order['order_number'] + ) + + self.error_message = ft.Text(color = ft.Colors.RED) + + self.loading_query = [] + self.loading_error_message = ft.Text(color= ft.Colors.RED) + + self.unloading_query = [] + self.unloading_error_message = ft.Text(color= ft.Colors.RED) + + self.product_description= ft.TextField( + label="Description", + multiline=True, + min_lines=3, + max_lines=5, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value=self.order['products_description'] + ) + + #add loading points + self.init_loading_query = self.order['loading_points'] + addresses = [] + for init_addr in self.init_loading_query: + for all_addr in self.all_addresses: + if init_addr['destination_id'] == all_addr['id']: + adr = '' + street_and_number = all_addr["address"].split(" %")[0] + postal_code = all_addr["address"].split(" %")[1] + city = all_addr["address"].split(" %")[2] + region_county = all_addr["address"].split(" %")[3] + country = all_addr["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + address = { + 'loading_address_id': init_addr['destination_id'], + 'loading_address_name': all_addr['name'], + 'loading_address': adr, + 'loading_date': init_addr['point_data'], + 'loading_hour': init_addr['point_hour'], + 'loading_informatins': init_addr['informatins'] + } + addresses.append(address) + break + self.loading_query = addresses + self.loading.controls = self.create_loading_list(addresses, self.on_delete_loading_address_btn_click) + + #add unloading points + self.init_unloading_query = self.order['unloading_points'] + addresses = [] + for init_addr in self.init_unloading_query: + for all_addr in self.all_addresses: + if init_addr['destination_id'] == all_addr['id']: + adr = '' + street_and_number = all_addr["address"].split(" %")[0] + postal_code = all_addr["address"].split(" %")[1] + city = all_addr["address"].split(" %")[2] + region_county = all_addr["address"].split(" %")[3] + country = all_addr["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + address = { + 'unloading_address_id': init_addr['destination_id'], + 'unloading_address_name': all_addr['name'], + 'unloading_address': adr, + 'unloading_date': init_addr['point_data'], + 'unloading_hour': init_addr['point_hour'], + 'unloading_informatins': init_addr['informatins'] + } + addresses.append(address) + break + self.unloading_query = addresses + self.unloading.controls = self.create_unloading_list(addresses, self.on_delete_unloading_address_btn_click) + + def get_order(self): + try: + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + type(self.order_id) + response = requests.get(f"{API_BASE_URL}/orders/{self.order_id}", headers=headers) + return response.json() if response.status_code == 200 else [] + except Exception as e: + print("Error loading orders:", e) + + 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 on_location_btn_click(self, destination): + query = destination["address"].replace(", ", "+") + maps_url = f"https://www.google.com/maps/search/?api=1&query={query}" + self.page.launch_url(maps_url) + + def on_searching_client(self, e): + query = e.control.value.lower() + self.filtered_clients = [client for client in self.all_clients if query in client["name"].lower()] + self.update_client_list(self.filtered_clients) + + def init_search_client(self): + query = self.client_search_field.value.lower() + self.filtered_clients = [client for client in self.all_clients if query in client["name"].lower()] + self.client_results.controls.clear() + for client in self.filtered_clients: + self.client_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + + def update_client_list(self, clients): + self.client_results.controls.clear() + for client in clients: + self.client_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.client_results.update() + + def on_client_selected(self, e, client): + self.selected_client_id = client["id"] + self.client_search_field.value = client["name"] + self.client_search_field.update() + self.update_client_list(self.filtered_clients) + + def on_searching_transporter(self, e): + query = e.control.value.lower() + self.filtered_transporters = [t for t in self.all_transporters if query in t["name"].lower()] + self.update_transporter_list(self.filtered_transporters) + + def init_search_transporter(self): + query = self.transporter_search_field.value.lower() + self.filtered_transporters = [transporter for transporter in self.all_transporters if query in transporter["name"].lower()] + self.transporter_results.controls.clear() + for transporter in self.filtered_transporters: + self.transporter_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(transporter["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if transporter["id"] == self.selected_transporter_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=transporter: self.on_transporter_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + + def update_transporter_list(self, transporters): + self.transporter_results.controls.clear() + for transporter in transporters: + self.transporter_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(transporter["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if transporter["id"] == self.selected_transporter_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=transporter: self.on_transporter_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.transporter_results.update() + + def on_transporter_selected(self, e, transporter): + self.selected_transporter_id = transporter["id"] + self.transporter_search_field.value = transporter["name"] + self.transporter_search_field.update() + self.update_transporter_list(self.filtered_transporters) + + def on_searching_loading_address(self, e): + query = e.control.value.lower() + self.filtered_addresses = [a for a in self.all_addresses if query in a["name"].lower()] + self.update_loading_addresses_list(self.filtered_addresses) + + def update_loading_addresses_list(self, addresses): + self.loading_address_results.controls.clear() + for address in addresses: + self.loading_address_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(address["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if address["id"] == getattr(self, "selected_loading_address_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e,a=address: self.on_loading_address_selected(e, a), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.loading_address_results.update() + + def on_loading_address_selected(self, e, address): + self.selected_loading_address_id = address["id"] + self.loading_address_search_field.value = address["name"] + self.loading_address_search_field.update() + self.update_loading_addresses_list(self.filtered_addresses) + + def on_loading_date_click(self, e): + self.loading_date.value = e.control.value.strftime('%m/%d/%Y') + self.loading_date.update() + #print(self.loading_date.value) + + def on_loading_hour_click(self, e): + if len(self.loading_hour.value) != None and len(self.loading_hour.value)==0: + self.loading_hour.value = str(e.control.value) + else: + self.loading_hour.value += f' - {e.control.value}' + self.loading_hour.update() + #print(self.loading_hour.value) + + def on_reset_loading_hour_btn_click(self, e): + self.loading_hour.value = None + self.loading_hour.update() + + def add_loading_point_btn_click(self, e): + adr = None + name = None + #print(self.selected_loading_address_id) + for _address in self.all_addresses: + if _address['id'] == self.selected_loading_address_id: + adr = '' + street_and_number = _address["address"].split(" %")[0] + postal_code = _address["address"].split(" %")[1] + city = _address["address"].split(" %")[2] + region_county = _address["address"].split(" %")[3] + country = _address["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + name = _address['name'] + #print(adr) + #print(_address['address']) + + loading_informatins = self.loading_informations.value + date = self.loading_date.value + hour = self.loading_hour.value + + #create loading list + address = { + 'loading_address_id': self.selected_loading_address_id, + 'loading_address_name': name, + 'loading_address': adr, + 'loading_informatins': loading_informatins, + 'loading_date': date, + 'loading_hour': hour + } + #print(address) + + if self.selected_loading_address_id == None: + self.loading_error_message.value = "Please select loading point!" + self.loading_error_message.update() + return + if self.loading_informations.value == None or len(self.loading_informations.value) == 0: + self.loading_error_message.value = "Add loading informations!" + self.loading_error_message.update() + return + if self.loading_date.value == None or len(str(self.loading_date.value)) == 0: + self.loading_error_message.value = "Add loading date!" + self.loading_error_message.update() + return + # if self.loading_hour.value == None or len(str(self.loading_hour.value)) == 0: + # self.loading_error_message.value = "Add loading hour!" + # self.loading_error_message.update() + # return + + if self.selected_loading_address_id: + self.loading_query.append(address) + self.loading.controls.clear() + self.loading.controls = self.create_loading_list(self.loading_query, self.on_delete_loading_address_btn_click) + self.loading.update() + + #reset to default + self.selected_loading_address_id = None + self.loading_informations.value = None + self.loading_informations.update() + self.loading_date.value = None + self.loading_date.update() + self.loading_hour.value = None + self.loading_hour.update() + self.loading_error_message.value = None + self.loading_error_message.update() + else: + self.loading_error_message.value = "All fields of the loading address are required." + self.loading_error_message.update() + + def on_searching_unloading_address(self, e): + query = e.control.value.lower() + self.filtered_addresses_ul = [a for a in self.all_addresses if query in a["name"].lower()] + self.update_unloading_addresses_list(self.filtered_addresses_ul) + + def update_unloading_addresses_list(self, addresses): + self.unloading_address_results.controls.clear() + for address in addresses: + self.unloading_address_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(address["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if address["id"] == getattr(self, "selected_unloading_address_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e,a=address: self.on_unloading_address_selected(e, a), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.unloading_address_results.update() + + def on_unloading_address_selected(self, e, address): + self.selected_unloading_address_id = address["id"] + self.unloading_address_search_field.value = address["name"] + self.unloading_address_search_field.update() + self.update_unloading_addresses_list(self.filtered_addresses_ul) + + def on_unloading_date_click(self, e): + self.unloading_date.value = e.control.value.strftime('%m/%d/%Y') + self.unloading_date.update() + + def on_unloading_hour_click(self, e): + if len(self.unloading_hour.value) != None and len(self.unloading_hour.value)==0: + self.unloading_hour.value = str(e.control.value) + else: + self.unloading_hour.value += f' - {e.control.value}' + self.unloading_hour.update() + + def on_reset_unloading_hour_btn_click(self, e): + self.unloading_hour.value = None + self.unloading_hour.update() + + def add_unloading_point_btn_click(self, e): + adr = None + name = None + #print(self.selected_unloading_address_id) + for _address in self.all_addresses: + if _address['id'] == self.selected_unloading_address_id: + adr = '' + street_and_number = _address["address"].split(" %")[0] + postal_code = _address["address"].split(" %")[1] + city = _address["address"].split(" %")[2] + region_county = _address["address"].split(" %")[3] + country = _address["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + name = _address['name'] + unloading_informatins = self.unloading_informations.value + date = self.unloading_date.value + hour = self.unloading_hour.value + address = { + 'unloading_address_id': self.selected_unloading_address_id, + 'unloading_address_name': name, + 'unloading_address': adr, + 'unloading_informatins': unloading_informatins, + 'unloading_date': date, + 'unloading_hour': hour + } + + if self.selected_unloading_address_id == None: + self.unloading_error_message.value = "Please select unloading point!" + self.unloading_error_message.update() + return + if self.unloading_informations.value == None or len(self.unloading_informations.value) == 0: + self.unloading_error_message.value = "Add unloading informations!" + self.unloading_error_message.update() + return + if self.unloading_date.value == None or len(str(self.unloading_date.value)) == 0: + self.unloading_error_message.value = "Add unloading date!" + self.unloading_error_message.update() + return + # if self.unloading_hour.value == None or len(str(self.unloading_hour.value)) == 0: + # self.unloading_error_message.value = "Add unloading hour!" + # self.unloading_error_message.update() + # return + + if self.selected_unloading_address_id: + self.unloading_query.append(address) + self.unloading.controls.clear() + self.unloading.controls = self.create_unloading_list(self.unloading_query, self.on_delete_unloading_address_btn_click) + self.unloading.update() + + #reset to default + self.selected_unloading_address_id = None + self.unloading_informations.value = None + self.unloading_informations.update() + self.unloading_date.value = None + self.unloading_date.update() + self.unloading_hour.value = None + self.unloading_hour.update() + self.unloading_error_message.value = None + self.unloading_error_message.update() + else: + self.unloading_error_message.value = "All fields of the unloading address are required." + self.unloading_error_message.update() + + def get_all_clients(self): + 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: + return response.json() + else: + print("Failed to fetch clients:", response.status_code) + return [] + + def get_all_transporters(self): + 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/", headers=headers) + if response.status_code == 200: + return response.json() + else: + print("Failed to fetch transporters:", response.status_code) + return [] + + def get_all_addresses(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/destinations/", headers=headers) + if response.status_code == 200: + return response.json() + else: + print("Failed to fetch addresses:", response.status_code) + return [] + + def create_loading_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['loading_address_name'], + expand=True, + size=15, + weight=ft.FontWeight.BOLD + ), + ft.Text( + item['loading_address'], + expand=True, + size=12, + ), + ft.Row( + [ + ft.Text( + f"Date: {item['loading_date']}", + expand=True, + size=12, + ), + ft.Text( + f"Hour: {item['loading_hour']}", + expand=True, + size=12, + ) + ] + ) + + ] + ), + ft.Row( + [ + ft.FilledButton( + "Delete", + bgcolor=ft.Colors.RED, + on_click=lambda e, id=item: on_click_handler(id), + width=100 + ) + ] + ) + + ], + 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 on_delete_loading_address_btn_click(self, item): + self.loading_query.remove(item) + self.loading.controls.clear() + self.loading.controls = self.create_loading_list(self.loading_query, self.on_delete_loading_address_btn_click) + self.loading.update() + + def create_unloading_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['unloading_address_name'], + expand=True, + size=15, + weight=ft.FontWeight.BOLD + ), + ft.Text( + item['unloading_address'], + expand=True, + size=12, + ), + ft.Row( + [ + ft.Text( + f"Date: {item['unloading_date']}", + expand=True, + size=12, + ), + ft.Text( + f"Hour: {item['unloading_hour']}", + expand=True, + size=12, + ) + ] + ) + + ] + ), + ft.Row( + [ + ft.FilledButton( + "Delete", + bgcolor=ft.Colors.RED, + on_click=lambda e, id=item: on_click_handler(id), + width=100 + ) + ] + ) + + ], + 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 on_delete_unloading_address_btn_click(self, item): + #print(item) + #print(self.unloading_query) + self.unloading_query.remove(item) + self.unloading.controls.clear() + self.unloading.controls = self.create_unloading_list(self.unloading_query, self.on_delete_unloading_address_btn_click) + self.unloading.update() + + def on_save_btn_click(self, e): + + loading_addresses = [] + unloading_addresses = [] + for laddr in self.loading_query: + laddr_copy = laddr.copy() + if isinstance(laddr_copy.get("loading_hour"), datetime.time): + laddr_copy["loading_hour"] = laddr_copy["loading_hour"].strftime("%H:%M") + loading_addresses.append(laddr_copy) + + for uaddr in self.unloading_query: + uaddr_copy = uaddr.copy() + if isinstance(uaddr_copy.get("unloading_hour"), datetime.time): + uaddr_copy["unloading_hour"] = uaddr_copy["unloading_hour"].strftime("%H:%M") + unloading_addresses.append(uaddr_copy) + + saved_data = { + 'order_number': self.order_number.value, + 'client_id': self.selected_client_id, + 'transporter_id': self.selected_transporter_id, + 'products_description': self.product_description.value, + 'ldb_quantity': self.ldm_quantity.value, + 'kg_quantity': self.kg_quantity.value, + 'track_reg_number': self.track_reg_number.value, + 'trailer_reg_number': self.trailer_reg_number.value, + 'received_price': self.received_price.value, + 'paid_price': self.paid_price.value, + 'loading_addresses': loading_addresses, + 'unloading_addresses': unloading_addresses + } + #print(saved_data) + if self.order_number.value == None or len(self.order_number.value)==0: + self.error_message.value = "Order number is mandatory!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.selected_client_id == None: + self.error_message.value = "Please select the client!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.selected_transporter_id == None: + self.error_message.value = "Please select the transporter!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.product_description.value == None or len(self.product_description.value)==0: + self.error_message.value = "Please insert product description!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + # if self.ldm_quantity.value == None or len(self.ldm_quantity.value)==0: + # self.error_message.value = "Please insert LDM!" + # self.error_message.color = ft.Colors.RED + # self.error_message.update() + # return + # if self.kg_quantity.value == None or len(self.kg_quantity.value)==0: + # self.error_message.value = "Please insert KG!" + # self.error_message.color = ft.Colors.RED + # self.error_message.update() + # return + if self.track_reg_number.value == None or len(self.track_reg_number.value)==0: + self.error_message.value = "Please insert Track Registration Number!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.trailer_reg_number.value == None or len(self.trailer_reg_number.value)==0: + self.error_message.value = "Please insert Trailer Registration Number!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.received_price.value == None or len(str(self.received_price.value))==0: + self.error_message.value = "Please insert Price received!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.paid_price.value == None or len(str(self.paid_price.value))==0: + self.error_message.value = "Please insert Price paid!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if len(loading_addresses) == 0: + self.error_message.value = "Please add loading point!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if len(unloading_addresses) == 0: + self.error_message.value = "Please add unloading point!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + + # --- POST request to save the order in the database --- + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + user_id = self.page.session.get("user_id") + saved_data["user_id"] = user_id + user = self.get_user_data() + saved_data['terms'] = user['terms'] + + try: + response = requests.put(f"{API_BASE_URL}/orders/{self.order_id}", json=saved_data, headers=headers) + if response.status_code == 200: + self.error_message.value = "Order saved successfully, Please wait!" + self.error_message.color = ft.Colors.GREEN + self.error_message.update() + order_id = response.json()['order_id'] + time.sleep(3) + pdf_name = f'order_{user_id}_{saved_data['order_number']}.pdf' + view_page = ViewPage(self.page, pdf_name, self, self.dashboard, order_id) + self.dashboard.placeholder.content = view_page.build() + self.dashboard.placeholder.update() + else: + self.error_message.value = f"Failed to save order: {response.status_code} - {response.text}" + self.error_message.update() + except Exception as ex: + self.error_message.value = f"Error: {str(ex)}" + self.error_message.update() + + def get_user_data(self): + try: + 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}"}) + if response.status_code == 200: + user_data = response.json() + return user_data + return None + except Exception as e: + return None + + def on_archive_btn_click(self, e): + #archive_page = ArchivePage(self.page, self.dashboard, self) + self.dashboard.placeholder.content = self.archive_page.build() + self.dashboard.placeholder.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) + return None + + def build(self): + self.save_btn = ft.FilledButton( + "Save and Generate", + width=200, + on_click=self.on_save_btn_click, + ) + self.save_row = ft.Row([],alignment=ft.MainAxisAlignment.CENTER) + if self.get_current_subscription_plan()['status'] != 'expired': + self.save_row.controls.append(self.save_btn) + self.init_search_client() + self.init_search_transporter() + return ft.Container( + ft.Column( + [ + ft.Row( + [ + ft.Column( + [ + ft.Text('Edit Order', size=24, weight=ft.FontWeight.BOLD), + ft.Row( + [ + ft.Text("Number", size=18, weight=ft.FontWeight.BOLD), + self.order_number + ] + ) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.ElevatedButton("Archive", on_click=self.on_archive_btn_click, width=150) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Row( + [ + ft.Column( + [ + ft.Text("Client", size=18, weight=ft.FontWeight.BOLD), + self.client_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.client_results], + scroll=ft.ScrollMode.ADAPTIVE, + ), + #clip_behavior=ft.ClipBehavior., + expand=True, + padding=0, + ), + height=250 #if len(self.filtered_clients) > 4 else None, + ) + ], + expand=5 + ), + ft.Column( + [ + ft.Text("Transporter", size=18, weight=ft.FontWeight.BOLD), + self.transporter_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.transporter_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 #if len(self.filtered_transporters) > 4 else None, + ) + ], + expand=5 + ), + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + ft.Row( + [ + ft.Text("Product Details", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + ft.Container( + content = self.product_description, + expand=True + ), + ft.Row( + [ + ft.Text( + value="LDM" + ), + self.ldm_quantity, + ft.Text(" "), + ft.Text( + value="KG" + ), + self.kg_quantity, + ], + expand=True + ) + ], + expand=True, + + ) + ], + expand=5 + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Truck / Trailer Info", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + self.track_reg_number, + self.trailer_reg_number + ], + expand=True + ) + ], + expand=2.5 + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Price", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + self.received_price, + self.paid_price + ], + expand=2.5 + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + ft.Text("Loading Points", size=18, weight=ft.FontWeight.BOLD), + ft.Column( + [ + self.loading_address_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.loading_address_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_addresses) > 4 else None, + ) + ], + expand=5 + ), + ft.Container( + content = self.loading_informations, + expand = True + ), + ft.Row( + [ + self.loading_date, + ft.ElevatedButton( + "Pick date", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2000, month=10, day=1), + last_date=datetime.datetime(year=2025, month=10, day=1), + on_change=self.on_loading_date_click, + ) + ), + ) + ], + expand=True + ), + ft.Row( + [ + self.loading_hour, + ft.ElevatedButton( + "Pick hour - Start", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_loading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Pick hour - End", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_loading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Reset", + on_click = self.on_reset_loading_hour_btn_click + ), + ], + expand=True + ), + self.loading_error_message, + ft.Row( + [ + ft.ElevatedButton( + "Add Loading Point", + on_click=self.add_loading_point_btn_click, + icon=ft.Icons.ADD + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.loading + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + ft.Text("Unloading Points", size=18, weight=ft.FontWeight.BOLD), + ft.Column( + [ + self.unloading_address_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.unloading_address_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_addresses_ul) > 4 else None, + ) + ], + expand=5 + ), + ft.Container( + content = self.unloading_informations, + expand=True + ), + ft.Row( + [ + self.unloading_date, + ft.ElevatedButton( + "Pick date", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2000, month=10, day=1), + last_date=datetime.datetime(year=2025, month=10, day=1), + on_change=self.on_unloading_date_click, + ) + ), + ) + ], + expand=True + ), + ft.Row( + [ + self.unloading_hour, + ft.ElevatedButton( + "Pick hour - Start", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_unloading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Pick hour - End", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_unloading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Reset", + on_click = self.on_reset_unloading_hour_btn_click + ), + ], + expand=True + ), + self.unloading_error_message, + ft.Row( + [ + ft.ElevatedButton( + "Add Unloading Point", + on_click=self.add_unloading_point_btn_click, + icon=ft.Icons.ADD + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.unloading + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Row( + [ + self.error_message, + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.save_row, + ], + expand=True, + scroll=ft.ScrollMode.ADAPTIVE, + spacing=20 + ) + ) + \ No newline at end of file diff --git a/transportmanager/client/pages/orders_in_page.py b/transportmanager/client/pages/orders_in_page.py new file mode 100644 index 0000000..c4e0e8d --- /dev/null +++ b/transportmanager/client/pages/orders_in_page.py @@ -0,0 +1,1105 @@ +import flet as ft +import datetime +import requests +import time +from pages.archive_in_page import ArchiveInPage +from config import API_BASE_URL + +class OrdersInPage: + def __init__(self, page: ft.Page, dashboard): + self.page = page + self.dashboard = dashboard + + self.selected_client_id = None + + self.all_clients = self.get_all_clients() + self.filtered_clients = self.all_clients.copy() + self.client_search_field = ft.TextField(label="Search Clients...", on_change=self.on_searching_client) + self.client_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for client in self.all_clients + ] + ) + + self.all_addresses = self.get_all_addresses() + self.selected_loading_address_id = None + self.filtered_addresses = self.all_addresses.copy() + self.loading_address_search_field = ft.TextField(label="Search Loading Address...", on_change=self.on_searching_loading_address) + self.loading_address_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ + ft.Text(address["name"], expand=True), + ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click=lambda e, t=address: self.on_location_btn_click(t)) + ], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=address: self.on_loading_address_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for address in self.all_addresses + ] + ) + + self.loading_informations = ft.TextField( + label="Loading Instructions", + min_lines=3, + max_lines=7, + multiline=True, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.loading_date = ft.TextField(label="Date", expand=True, read_only=True) + + self.loading_hour = ft.TextField(label="Hour", expand=True, read_only=True) + + self.loading = ft.ListView( + spacing=10, + ) + + self.selected_unloading_address_id = None + self.filtered_addresses_ul = self.all_addresses.copy() + self.unloading_address_search_field = ft.TextField(label="Search Unloading Address...", on_change=self.on_searching_unloading_address) + self.unloading_address_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ + ft.Text(address["name"], expand=True), + ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click=lambda e, t=address: self.on_location_btn_click(t)) + ], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=address: self.on_unloading_address_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for address in self.all_addresses + ] + ) + + self.unloading_informations = ft.TextField( + label="Unloading Instructions", + min_lines=3, + max_lines=7, + multiline=True, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.unloading_date = ft.TextField(label="Date", expand=True, read_only=True) + + self.unloading_hour = ft.TextField(label="Hour", expand=True, read_only=True) + + self.unloading = ft.ListView( + spacing=10, + ) + + self.ldm_quantity = ft.TextField( + expand=True, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") + ) + + self.kg_quantity = ft.TextField( + expand=True, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") + ) + + self.track_reg_number = ft.TextField( + label="Track Reg. Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.trailer_reg_number = ft.TextField( + label="Trailer Reg. Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.received_price = ft.TextField( + label="Price Received - visible only to you!", + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") + ) + + self.error_message = ft.Text(color = ft.Colors.RED) + + self.loading_query = [] + self.loading_error_message = ft.Text(color= ft.Colors.RED) + + self.unloading_query = [] + self.unloading_error_message = ft.Text(color= ft.Colors.RED) + + self.product_description= ft.TextField( + label="Description", + multiline=True, + min_lines=3, + max_lines=5, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.order_number = ft.TextField( + label="Order Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + def on_location_btn_click(self, destination): + query = destination["address"].replace(", ", "+") + maps_url = f"https://www.google.com/maps/search/?api=1&query={query}" + self.page.launch_url(maps_url) + + def on_searching_client(self, e): + query = e.control.value.lower() + self.filtered_clients = [client for client in self.all_clients if query in client["name"].lower()] + self.update_client_list(self.filtered_clients) + + def update_client_list(self, clients): + self.client_results.controls.clear() + for client in clients: + self.client_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.client_results.update() + + def on_client_selected(self, e, client): + self.selected_client_id = client["id"] + self.client_search_field.value = client["name"] + self.client_search_field.update() + self.update_client_list(self.filtered_clients) + + def on_searching_loading_address(self, e): + query = e.control.value.lower() + self.filtered_addresses = [a for a in self.all_addresses if query in a["name"].lower()] + self.update_loading_addresses_list(self.filtered_addresses) + + def update_loading_addresses_list(self, addresses): + self.loading_address_results.controls.clear() + for address in addresses: + self.loading_address_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(address["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if address["id"] == getattr(self, "selected_loading_address_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e,a=address: self.on_loading_address_selected(e, a), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.loading_address_results.update() + + def on_loading_address_selected(self, e, address): + self.selected_loading_address_id = address["id"] + self.loading_address_search_field.value = address["name"] + self.loading_address_search_field.update() + self.update_loading_addresses_list(self.filtered_addresses) + + def on_loading_date_click(self, e): + self.loading_date.value = e.control.value.strftime('%m/%d/%Y') + self.loading_date.update() + #print(self.loading_date.value) + + def on_loading_hour_click(self, e): + if len(self.loading_hour.value) != None and len(self.loading_hour.value)==0: + self.loading_hour.value = str(e.control.value) + else: + self.loading_hour.value += f' - {e.control.value}' + self.loading_hour.update() + #print(self.loading_hour.value) + + def on_reset_loading_hour_btn_click(self, e): + self.loading_hour.value = None + self.loading_hour.update() + + def add_loading_point_btn_click(self, e): + adr = None + name = None + #print(self.selected_loading_address_id) + for _address in self.all_addresses: + if _address['id'] == self.selected_loading_address_id: + adr = '' + street_and_number = _address["address"].split(" %")[0] + postal_code = _address["address"].split(" %")[1] + city = _address["address"].split(" %")[2] + region_county = _address["address"].split(" %")[3] + country = _address["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + name = _address['name'] + #print(adr) + #print(_address['address']) + + loading_informatins = self.loading_informations.value + date = self.loading_date.value + hour = self.loading_hour.value + + #create loading list + address = { + 'loading_address_id': self.selected_loading_address_id, + 'loading_address_name': name, + 'loading_address': adr, + 'loading_informatins': loading_informatins, + 'loading_date': date, + 'loading_hour': hour + } + #print(address) + + if self.selected_loading_address_id == None: + self.loading_error_message.value = "Please select loading point!" + self.loading_error_message.update() + return + if self.loading_informations.value == None or len(self.loading_informations.value) == 0: + self.loading_error_message.value = "Add loading informations!" + self.loading_error_message.update() + return + if self.loading_date.value == None or len(str(self.loading_date.value)) == 0: + self.loading_error_message.value = "Add loading date!" + self.loading_error_message.update() + return + # if self.loading_hour.value == None or len(str(self.loading_hour.value)) == 0: + # self.loading_error_message.value = "Add loading hour!" + # self.loading_error_message.update() + # return + + if self.selected_loading_address_id: + self.loading_query.append(address) + self.loading.controls.clear() + self.loading.controls = self.create_loading_list(self.loading_query, self.on_delete_loading_address_btn_click) + self.loading.update() + + #reset to default + self.selected_loading_address_id = None + self.loading_informations.value = None + self.loading_informations.update() + self.loading_date.value = None + self.loading_date.update() + self.loading_hour.value = None + self.loading_hour.update() + self.loading_error_message.value = None + self.loading_error_message.update() + else: + self.loading_error_message.value = "All fields of the loading address are required." + self.loading_error_message.update() + + def on_searching_unloading_address(self, e): + query = e.control.value.lower() + self.filtered_addresses_ul = [a for a in self.all_addresses if query in a["name"].lower()] + self.update_unloading_addresses_list(self.filtered_addresses_ul) + + def update_unloading_addresses_list(self, addresses): + self.unloading_address_results.controls.clear() + for address in addresses: + self.unloading_address_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(address["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if address["id"] == getattr(self, "selected_unloading_address_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e,a=address: self.on_unloading_address_selected(e, a), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.unloading_address_results.update() + + def on_unloading_address_selected(self, e, address): + self.selected_unloading_address_id = address["id"] + self.unloading_address_search_field.value = address["name"] + self.unloading_address_search_field.update() + self.update_unloading_addresses_list(self.filtered_addresses_ul) + + def on_unloading_date_click(self, e): + self.unloading_date.value = e.control.value.strftime('%m/%d/%Y') + self.unloading_date.update() + + def on_unloading_hour_click(self, e): + if len(self.unloading_hour.value) != None and len(self.unloading_hour.value)==0: + self.unloading_hour.value = str(e.control.value) + else: + self.unloading_hour.value += f' - {e.control.value}' + self.unloading_hour.update() + + def on_reset_unloading_hour_btn_click(self, e): + self.unloading_hour.value = None + self.unloading_hour.update() + + def add_unloading_point_btn_click(self, e): + adr = None + name = None + #print(self.selected_unloading_address_id) + for _address in self.all_addresses: + if _address['id'] == self.selected_unloading_address_id: + adr = '' + street_and_number = _address["address"].split(" %")[0] + postal_code = _address["address"].split(" %")[1] + city = _address["address"].split(" %")[2] + region_county = _address["address"].split(" %")[3] + country = _address["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + name = _address['name'] + unloading_informatins = self.unloading_informations.value + date = self.unloading_date.value + hour = self.unloading_hour.value + address = { + 'unloading_address_id': self.selected_unloading_address_id, + 'unloading_address_name': name, + 'unloading_address': adr, + 'unloading_informatins': unloading_informatins, + 'unloading_date': date, + 'unloading_hour': hour + } + + if self.selected_unloading_address_id == None: + self.unloading_error_message.value = "Please select unloading point!" + self.unloading_error_message.update() + return + if self.unloading_informations.value == None or len(self.unloading_informations.value) == 0: + self.unloading_error_message.value = "Add unloading informations!" + self.unloading_error_message.update() + return + if self.unloading_date.value == None or len(str(self.unloading_date.value)) == 0: + self.unloading_error_message.value = "Add unloading date!" + self.unloading_error_message.update() + return + # if self.unloading_hour.value == None or len(str(self.unloading_hour.value)) == 0: + # self.unloading_error_message.value = "Add unloading hour!" + # self.unloading_error_message.update() + # return + + if self.selected_unloading_address_id: + self.unloading_query.append(address) + self.unloading.controls.clear() + self.unloading.controls = self.create_unloading_list(self.unloading_query, self.on_delete_unloading_address_btn_click) + self.unloading.update() + + #reset to default + self.selected_unloading_address_id = None + self.unloading_informations.value = None + self.unloading_informations.update() + self.unloading_date.value = None + self.unloading_date.update() + self.unloading_hour.value = None + self.unloading_hour.update() + self.unloading_error_message.value = None + self.unloading_error_message.update() + else: + self.unloading_error_message.value = "All fields of the unloading address are required." + self.unloading_error_message.update() + + def get_all_clients(self): + 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: + return response.json() + else: + print("Failed to fetch clients:", response.status_code) + return [] + + def get_all_addresses(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/destinations/", headers=headers) + if response.status_code == 200: + return response.json() + else: + print("Failed to fetch addresses:", response.status_code) + return [] + + def create_loading_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['loading_address_name'], + expand=True, + size=15, + weight=ft.FontWeight.BOLD + ), + ft.Text( + item['loading_address'], + expand=True, + size=12, + ), + ft.Row( + [ + ft.Text( + f"Date: {item['loading_date']}", + expand=True, + size=12, + ), + ft.Text( + f"Hour: {item['loading_hour']}", + expand=True, + size=12, + ) + ] + ) + + ] + ), + ft.Row( + [ + ft.FilledButton( + "Delete", + bgcolor=ft.Colors.RED, + on_click=lambda e, id=item: on_click_handler(id), + width=100 + ) + ] + ) + + ], + 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 on_delete_loading_address_btn_click(self, item): + self.loading_query.remove(item) + self.loading.controls.clear() + self.loading.controls = self.create_loading_list(self.loading_query, self.on_delete_loading_address_btn_click) + self.loading.update() + + def create_unloading_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['unloading_address_name'], + expand=True, + size=15, + weight=ft.FontWeight.BOLD + ), + ft.Text( + item['unloading_address'], + expand=True, + size=12, + ), + ft.Row( + [ + ft.Text( + f"Date: {item['unloading_date']}", + expand=True, + size=12, + ), + ft.Text( + f"Hour: {item['unloading_hour']}", + expand=True, + size=12, + ) + ] + ) + + ] + ), + ft.Row( + [ + ft.FilledButton( + "Delete", + bgcolor=ft.Colors.RED, + on_click=lambda e, id=item: on_click_handler(id), + width=100 + ) + ] + ) + + ], + 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 on_delete_unloading_address_btn_click(self, item): + #print(item) + #print(self.unloading_query) + self.unloading_query.remove(item) + self.unloading.controls.clear() + self.unloading.controls = self.create_unloading_list(self.unloading_query, self.on_delete_unloading_address_btn_click) + self.unloading.update() + + def get_user_data(self): + try: + 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}"}) + if response.status_code == 200: + user_data = response.json() + return user_data + return None + except Exception as e: + return None + + def on_archive_btn_click(self, e): + archive_page = ArchiveInPage(self.page, self.dashboard, self) + self.dashboard.placeholder.content = archive_page.build() + self.dashboard.placeholder.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) + return None + + def on_save_btn_click(self, e): + loading_addresses = [] + unloading_addresses = [] + for laddr in self.loading_query: + laddr_copy = laddr.copy() + #if isinstance(laddr_copy.get("loading_hour"), datetime.time): + # laddr_copy["loading_hour"] = laddr_copy["loading_hour"].strftime("%H:%M") + loading_addresses.append(laddr_copy) + + for uaddr in self.unloading_query: + uaddr_copy = uaddr.copy() + #if isinstance(uaddr_copy.get("unloading_hour"), datetime.time): + # uaddr_copy["unloading_hour"] = uaddr_copy["unloading_hour"].strftime("%H:%M") + unloading_addresses.append(uaddr_copy) + + saved_data = { + 'order_number': self.order_number.value, + 'client_id': self.selected_client_id, + 'products_description': self.product_description.value, + 'ldb_quantity': self.ldm_quantity.value, + 'kg_quantity': self.kg_quantity.value, + 'track_reg_number': self.track_reg_number.value, + 'trailer_reg_number': self.trailer_reg_number.value, + 'received_price': self.received_price.value, + 'loading_addresses': loading_addresses, + 'unloading_addresses': unloading_addresses + } + #print(saved_data) + if self.order_number.value == None or len(self.order_number.value)==0: + self.error_message.value = "Order number is mandatory!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.selected_client_id == None: + self.error_message.value = "Please select the client!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.product_description.value == None or len(self.product_description.value)==0: + self.error_message.value = "Please insert product description!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + # if self.ldm_quantity.value == None or len(self.ldm_quantity.value)==0: + # self.error_message.value = "Please insert LDM!" + # self.error_message.color = ft.Colors.RED + # self.error_message.update() + # return + # if self.kg_quantity.value == None or len(self.kg_quantity.value)==0: + # self.error_message.value = "Please insert KG!" + # self.error_message.color = ft.Colors.RED + # self.error_message.update() + # return + if self.track_reg_number.value == None or len(self.track_reg_number.value)==0: + self.error_message.value = "Please insert Track Registration Number!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.trailer_reg_number.value == None or len(self.trailer_reg_number.value)==0: + self.error_message.value = "Please insert Trailer Registration Number!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.received_price.value == None or len(self.received_price.value)==0: + self.error_message.value = "Please insert Price received!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if len(loading_addresses) == 0: + self.error_message.value = "Please add loading point!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if len(unloading_addresses) == 0: + self.error_message.value = "Please add unloading point!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + + # --- POST request to save the order in the database --- + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + user_id = self.page.session.get("user_id") + saved_data["user_id"] = user_id + user = self.get_user_data() + saved_data['terms'] = user['terms'] + + try: + response = requests.post(f"{API_BASE_URL}/orders_in/", json=saved_data, headers=headers) + if response.status_code == 201: + self.error_message.value = "Order saved successfully! You will be redirected to archive page." + self.error_message.color = ft.Colors.GREEN + self.error_message.update() + time.sleep(3) + archive_page = ArchiveInPage(self.page, self.dashboard, self) + self.dashboard.placeholder.content = archive_page.build() + self.dashboard.placeholder.update() + else: + self.error_message.value = f"Failed to save order: {response.status_code} - {response.text}" + self.error_message.update() + except Exception as ex: + self.error_message.value = f"Error: {str(ex)}" + self.error_message.update() + + def build(self): + self.save_btn = ft.FilledButton( + "Save Order", + width=200, + on_click=self.on_save_btn_click, + ) + self.save_row = ft.Row([],alignment=ft.MainAxisAlignment.CENTER) + subscription = self.get_current_subscription_plan() + if subscription: + if subscription['status'] != 'expired': + self.save_row.controls.append(self.save_btn) + return ft.Container( + ft.Column( + [ + ft.Row( + [ + ft.Column( + [ + ft.Text('Create Order In', size=24, weight=ft.FontWeight.BOLD), + ft.Row( + [ + ft.Text("Number", size=18, weight=ft.FontWeight.BOLD), + self.order_number + ] + ) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.ElevatedButton("Archive", on_click=self.on_archive_btn_click, width=150) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Row( + [ + ft.Column( + [ + ft.Text("Client", size=18, weight=ft.FontWeight.BOLD), + self.client_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.client_results], + scroll=ft.ScrollMode.ADAPTIVE, + ), + #clip_behavior=ft.ClipBehavior., + expand=True, + padding=0, + ), + height=250 if len(self.client_results.controls) > 4 else None, + ) + ], + expand=True + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + ft.Row( + [ + ft.Text("Product Details", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + ft.Container( + content = self.product_description, + expand=True + ), + ft.Row( + [ + ft.Text( + value="LDM" + ), + self.ldm_quantity, + ft.Text(" "), + ft.Text( + value="KG" + ), + self.kg_quantity, + ], + expand=True + ) + ], + expand=True, + + ) + ], + expand=5 + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Truck / Trailer Info", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + self.track_reg_number, + self.trailer_reg_number + ], + expand=True + ) + ], + expand=2.5 + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Price", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + self.received_price, + ], + expand=2.5 + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + ft.Text("Loading Points", size=18, weight=ft.FontWeight.BOLD), + ft.Column( + [ + self.loading_address_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.loading_address_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_addresses) > 4 else None, + ) + ], + expand=5 + ), + ft.Container( + content = self.loading_informations, + expand = True + ), + ft.Row( + [ + self.loading_date, + ft.ElevatedButton( + "Pick date", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2000, month=10, day=1), + last_date=datetime.datetime(year=2025, month=10, day=1), + on_change=self.on_loading_date_click, + ) + ), + ) + ], + expand=True + ), + ft.Row( + [ + self.loading_hour, + ft.ElevatedButton( + "Pick hour - Start", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_loading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Pick hour - End", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_loading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Reset", + on_click = self.on_reset_loading_hour_btn_click + ), + ], + expand=True + ), + self.loading_error_message, + ft.Row( + [ + ft.ElevatedButton( + "Add Loading Point", + on_click=self.add_loading_point_btn_click, + icon=ft.Icons.ADD + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.loading + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + ft.Text("Unloading Points", size=18, weight=ft.FontWeight.BOLD), + ft.Column( + [ + self.unloading_address_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.unloading_address_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_addresses_ul) > 4 else None, + ) + ], + expand=5 + ), + ft.Container( + content = self.unloading_informations, + expand=True + ), + ft.Row( + [ + self.unloading_date, + ft.ElevatedButton( + "Pick date", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2000, month=10, day=1), + last_date=datetime.datetime(year=2025, month=10, day=1), + on_change=self.on_unloading_date_click, + ) + ), + ) + ], + expand=True + ), + ft.Row( + [ + self.unloading_hour, + ft.ElevatedButton( + "Pick hour - Start", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_unloading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Pick hour - End", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_unloading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Reset", + on_click = self.on_reset_unloading_hour_btn_click + ), + ], + expand=True + ), + self.unloading_error_message, + ft.Row( + [ + ft.ElevatedButton( + "Add Unloading Point", + on_click=self.add_unloading_point_btn_click, + icon=ft.Icons.ADD + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.unloading + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Row( + [ + self.error_message, + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.save_row, + ], + expand=True, + scroll=ft.ScrollMode.ADAPTIVE, + spacing=20 + ) + ) + \ No newline at end of file diff --git a/transportmanager/client/pages/orders_out_page.py b/transportmanager/client/pages/orders_out_page.py new file mode 100644 index 0000000..b262d8d --- /dev/null +++ b/transportmanager/client/pages/orders_out_page.py @@ -0,0 +1,1229 @@ +import flet as ft +import datetime +import requests +import time +from pages.view_page import ViewPage +from pages.archive_page import ArchivePage +from config import API_BASE_URL + +class OrdersOutPage: + def __init__(self, page: ft.Page, dashbaord): + self.page = page + self.dashboard = dashbaord + + self.selected_client_id = None + + self.all_clients = self.get_all_clients() + self.filtered_clients = self.all_clients.copy() + self.client_search_field = ft.TextField(label="Search Clients...", on_change=self.on_searching_client) + self.client_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for client in self.all_clients + ] + ) + + self.all_transporters = self.get_all_transporters() + self.filtered_transporters = self.all_transporters.copy() + self.selected_transporter_id = None + self.transporter_search_field = ft.TextField(label="Search Transporters...", on_change=self.on_searching_transporter) + self.transporter_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ft.Text(transporter["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=transporter: self.on_transporter_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for transporter in self.all_transporters + ] + ) + + self.all_addresses = self.get_all_addresses() + self.selected_loading_address_id = None + self.filtered_addresses = self.all_addresses.copy() + self.loading_address_search_field = ft.TextField(label="Search Loading Address...", on_change=self.on_searching_loading_address) + self.loading_address_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ + ft.Text(address["name"], expand=True), + ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click=lambda e, t=address: self.on_location_btn_click(t)) + ], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=address: self.on_loading_address_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for address in self.all_addresses + ] + ) + + self.loading_informations = ft.TextField( + label="Loading Instructions", + min_lines=3, + max_lines=7, + multiline=True, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.loading_date = ft.TextField(label="Date", expand=True, read_only=True) + + self.loading_hour = ft.TextField(label="Hour", expand=True, read_only=True) + + self.loading = ft.ListView( + spacing=10, + ) + + self.selected_unloading_address_id = None + self.filtered_addresses_ul = self.all_addresses.copy() + self.unloading_address_search_field = ft.TextField(label="Search Unloading Address...", on_change=self.on_searching_unloading_address) + self.unloading_address_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ + ft.Text(address["name"], expand=True), + ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click=lambda e, t=address: self.on_location_btn_click(t)) + ], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=address: self.on_unloading_address_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for address in self.all_addresses + ] + ) + + self.unloading_informations = ft.TextField( + label="Unloading Instructions", + min_lines=3, + max_lines=7, + multiline=True, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.unloading_date = ft.TextField(label="Date", expand=True, read_only=True) + + self.unloading_hour = ft.TextField(label="Hour", expand=True, read_only=True) + + self.unloading = ft.ListView( + spacing=10, + ) + + self.ldm_quantity = ft.TextField( + expand=True, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") + ) + + self.kg_quantity = ft.TextField( + expand=True, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") + ) + + self.track_reg_number = ft.TextField( + label="Track Reg. Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.trailer_reg_number = ft.TextField( + label="Trailer Reg. Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.received_price = ft.TextField( + label="Price Received - visible only to you!", + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") + ) + + self.paid_price = ft.TextField( + label="Price Paid", + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") + ) + + self.first_order_number = self.page.session.get("first_order_number") + last_orders = self.load_orders() + if len(last_orders) == 0: + number = self.first_order_number + else: + ln = last_orders[0]['order_number'] + #print(last_orders) + number = int(ln)+1 + self.order_number = ft.TextField( + label=f"Order number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value=number + ) + + self.error_message = ft.Text(color = ft.Colors.RED) + + self.loading_query = [] + self.loading_error_message = ft.Text(color= ft.Colors.RED) + + self.unloading_query = [] + self.unloading_error_message = ft.Text(color= ft.Colors.RED) + + self.product_description= ft.TextField( + label="Description", + multiline=True, + min_lines=3, + max_lines=5, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + 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 on_location_btn_click(self, destination): + query = destination["address"].replace(", ", "+") + maps_url = f"https://www.google.com/maps/search/?api=1&query={query}" + self.page.launch_url(maps_url) + + def on_searching_client(self, e): + query = e.control.value.lower() + self.filtered_clients = [client for client in self.all_clients if query in client["name"].lower()] + self.update_client_list(self.filtered_clients) + + def update_client_list(self, clients): + self.client_results.controls.clear() + for client in clients: + self.client_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.client_results.update() + + def on_client_selected(self, e, client): + self.selected_client_id = client["id"] + self.client_search_field.value = client["name"] + self.client_search_field.update() + self.update_client_list(self.filtered_clients) + + def on_searching_transporter(self, e): + query = e.control.value.lower() + self.filtered_transporters = [t for t in self.all_transporters if query in t["name"].lower()] + self.update_transporter_list(self.filtered_transporters) + + def update_transporter_list(self, transporters): + self.transporter_results.controls.clear() + for transporter in transporters: + self.transporter_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(transporter["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if transporter["id"] == getattr(self, "selected_transporter_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=transporter: self.on_transporter_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.transporter_results.update() + + def on_transporter_selected(self, e, transporter): + self.selected_transporter_id = transporter["id"] + self.transporter_search_field.value = transporter["name"] + self.transporter_search_field.update() + self.update_transporter_list(self.filtered_transporters) + + def on_searching_loading_address(self, e): + query = e.control.value.lower() + self.filtered_addresses = [a for a in self.all_addresses if query in a["name"].lower()] + self.update_loading_addresses_list(self.filtered_addresses) + + def update_loading_addresses_list(self, addresses): + self.loading_address_results.controls.clear() + for address in addresses: + self.loading_address_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(address["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if address["id"] == getattr(self, "selected_loading_address_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e,a=address: self.on_loading_address_selected(e, a), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.loading_address_results.update() + + def on_loading_address_selected(self, e, address): + self.selected_loading_address_id = address["id"] + self.loading_address_search_field.value = address["name"] + self.loading_address_search_field.update() + self.update_loading_addresses_list(self.filtered_addresses) + + def on_loading_date_click(self, e): + self.loading_date.value = e.control.value.strftime('%m/%d/%Y') + self.loading_date.update() + #print(self.loading_date.value) + + def on_loading_hour_click(self, e): + if len(self.loading_hour.value) != None and len(self.loading_hour.value)==0: + self.loading_hour.value = str(e.control.value) + else: + self.loading_hour.value += f' - {e.control.value}' + self.loading_hour.update() + #print(self.loading_hour.value) + + def on_reset_loading_hour_btn_click(self, e): + self.loading_hour.value = None + self.loading_hour.update() + + def add_loading_point_btn_click(self, e): + adr = None + name = None + #print(self.selected_loading_address_id) + for _address in self.all_addresses: + if _address['id'] == self.selected_loading_address_id: + adr = '' + street_and_number = _address["address"].split(" %")[0] + postal_code = _address["address"].split(" %")[1] + city = _address["address"].split(" %")[2] + region_county = _address["address"].split(" %")[3] + country = _address["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + name = _address['name'] + #print(adr) + #print(_address['address']) + + loading_informatins = self.loading_informations.value + date = self.loading_date.value + hour = self.loading_hour.value + + #create loading list + address = { + 'loading_address_id': self.selected_loading_address_id, + 'loading_address_name': name, + 'loading_address': adr, + 'loading_informatins': loading_informatins, + 'loading_date': date, + 'loading_hour': hour + } + #print(address) + + if self.selected_loading_address_id == None: + self.loading_error_message.value = "Please select loading point!" + self.loading_error_message.update() + return + if self.loading_informations.value == None or len(self.loading_informations.value) == 0: + self.loading_error_message.value = "Add loading informations!" + self.loading_error_message.update() + return + if self.loading_date.value == None or len(str(self.loading_date.value)) == 0: + self.loading_error_message.value = "Add loading date!" + self.loading_error_message.update() + return + # if self.loading_hour.value == None or len(str(self.loading_hour.value)) == 0: + # self.loading_error_message.value = "Add loading hour!" + # self.loading_error_message.update() + # return + + if self.selected_loading_address_id: + self.loading_query.append(address) + self.loading.controls.clear() + self.loading.controls = self.create_loading_list(self.loading_query, self.on_delete_loading_address_btn_click) + self.loading.update() + + #reset to default + self.selected_loading_address_id = None + self.loading_informations.value = None + self.loading_informations.update() + self.loading_date.value = None + self.loading_date.update() + self.loading_hour.value = None + self.loading_hour.update() + self.loading_error_message.value = None + self.loading_error_message.update() + else: + self.loading_error_message.value = "All fields of the loading address are required." + self.loading_error_message.update() + + def on_searching_unloading_address(self, e): + query = e.control.value.lower() + self.filtered_addresses_ul = [a for a in self.all_addresses if query in a["name"].lower()] + self.update_unloading_addresses_list(self.filtered_addresses_ul) + + def update_unloading_addresses_list(self, addresses): + self.unloading_address_results.controls.clear() + for address in addresses: + self.unloading_address_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(address["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if address["id"] == getattr(self, "selected_unloading_address_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e,a=address: self.on_unloading_address_selected(e, a), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.unloading_address_results.update() + + def on_unloading_address_selected(self, e, address): + self.selected_unloading_address_id = address["id"] + self.unloading_address_search_field.value = address["name"] + self.unloading_address_search_field.update() + self.update_unloading_addresses_list(self.filtered_addresses_ul) + + def on_unloading_date_click(self, e): + self.unloading_date.value = e.control.value.strftime('%m/%d/%Y') + self.unloading_date.update() + + def on_unloading_hour_click(self, e): + if len(self.unloading_hour.value) != None and len(self.unloading_hour.value)==0: + self.unloading_hour.value = str(e.control.value) + else: + self.unloading_hour.value += f' - {e.control.value}' + self.unloading_hour.update() + + def on_reset_unloading_hour_btn_click(self, e): + self.unloading_hour.value = None + self.unloading_hour.update() + + def add_unloading_point_btn_click(self, e): + adr = None + name = None + #print(self.selected_unloading_address_id) + for _address in self.all_addresses: + if _address['id'] == self.selected_unloading_address_id: + adr = '' + street_and_number = _address["address"].split(" %")[0] + postal_code = _address["address"].split(" %")[1] + city = _address["address"].split(" %")[2] + region_county = _address["address"].split(" %")[3] + country = _address["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + name = _address['name'] + unloading_informatins = self.unloading_informations.value + date = self.unloading_date.value + hour = self.unloading_hour.value + address = { + 'unloading_address_id': self.selected_unloading_address_id, + 'unloading_address_name': name, + 'unloading_address': adr, + 'unloading_informatins': unloading_informatins, + 'unloading_date': date, + 'unloading_hour': hour + } + + if self.selected_unloading_address_id == None: + self.unloading_error_message.value = "Please select unloading point!" + self.unloading_error_message.update() + return + if self.unloading_informations.value == None or len(self.unloading_informations.value) == 0: + self.unloading_error_message.value = "Add unloading informations!" + self.unloading_error_message.update() + return + if self.unloading_date.value == None or len(str(self.unloading_date.value)) == 0: + self.unloading_error_message.value = "Add unloading date!" + self.unloading_error_message.update() + return + # if self.unloading_hour.value == None or len(str(self.unloading_hour.value)) == 0: + # self.unloading_error_message.value = "Add unloading hour!" + # self.unloading_error_message.update() + # return + + if self.selected_unloading_address_id: + self.unloading_query.append(address) + self.unloading.controls.clear() + self.unloading.controls = self.create_unloading_list(self.unloading_query, self.on_delete_unloading_address_btn_click) + self.unloading.update() + + #reset to default + self.selected_unloading_address_id = None + self.unloading_informations.value = None + self.unloading_informations.update() + self.unloading_date.value = None + self.unloading_date.update() + self.unloading_hour.value = None + self.unloading_hour.update() + self.unloading_error_message.value = None + self.unloading_error_message.update() + else: + self.unloading_error_message.value = "All fields of the unloading address are required." + self.unloading_error_message.update() + + def get_all_clients(self): + 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: + return response.json() + else: + print("Failed to fetch clients:", response.status_code) + return [] + + def get_all_transporters(self): + 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/", headers=headers) + if response.status_code == 200: + return response.json() + else: + print("Failed to fetch transporters:", response.status_code) + return [] + + def get_all_addresses(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/destinations/", headers=headers) + if response.status_code == 200: + return response.json() + else: + print("Failed to fetch addresses:", response.status_code) + return [] + + def create_loading_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['loading_address_name'], + expand=True, + size=15, + weight=ft.FontWeight.BOLD + ), + ft.Text( + item['loading_address'], + expand=True, + size=12, + ), + ft.Row( + [ + ft.Text( + f"Date: {item['loading_date']}", + expand=True, + size=12, + ), + ft.Text( + f"Hour: {item['loading_hour']}", + expand=True, + size=12, + ) + ] + ) + + ] + ), + ft.Row( + [ + ft.FilledButton( + "Delete", + bgcolor=ft.Colors.RED, + on_click=lambda e, id=item: on_click_handler(id), + width=100 + ) + ] + ) + + ], + 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 on_delete_loading_address_btn_click(self, item): + self.loading_query.remove(item) + self.loading.controls.clear() + self.loading.controls = self.create_loading_list(self.loading_query, self.on_delete_loading_address_btn_click) + self.loading.update() + + def create_unloading_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['unloading_address_name'], + expand=True, + size=15, + weight=ft.FontWeight.BOLD + ), + ft.Text( + item['unloading_address'], + expand=True, + size=12, + ), + ft.Row( + [ + ft.Text( + f"Date: {item['unloading_date']}", + expand=True, + size=12, + ), + ft.Text( + f"Hour: {item['unloading_hour']}", + expand=True, + size=12, + ) + ] + ) + + ] + ), + ft.Row( + [ + ft.FilledButton( + "Delete", + bgcolor=ft.Colors.RED, + on_click=lambda e, id=item: on_click_handler(id), + width=100 + ) + ] + ) + + ], + 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 on_delete_unloading_address_btn_click(self, item): + #print(item) + #print(self.unloading_query) + self.unloading_query.remove(item) + self.unloading.controls.clear() + self.unloading.controls = self.create_unloading_list(self.unloading_query, self.on_delete_unloading_address_btn_click) + self.unloading.update() + + def on_save_btn_click(self, e): + + loading_addresses = [] + unloading_addresses = [] + for laddr in self.loading_query: + laddr_copy = laddr.copy() + if isinstance(laddr_copy.get("loading_hour"), datetime.time): + laddr_copy["loading_hour"] = laddr_copy["loading_hour"].strftime("%H:%M") + loading_addresses.append(laddr_copy) + + for uaddr in self.unloading_query: + uaddr_copy = uaddr.copy() + if isinstance(uaddr_copy.get("unloading_hour"), datetime.time): + uaddr_copy["unloading_hour"] = uaddr_copy["unloading_hour"].strftime("%H:%M") + unloading_addresses.append(uaddr_copy) + + saved_data = { + 'order_number': self.order_number.value, + 'client_id': self.selected_client_id, + 'transporter_id': self.selected_transporter_id, + 'products_description': self.product_description.value, + 'ldb_quantity': self.ldm_quantity.value, + 'kg_quantity': self.kg_quantity.value, + 'track_reg_number': self.track_reg_number.value, + 'trailer_reg_number': self.trailer_reg_number.value, + 'received_price': self.received_price.value, + 'paid_price': self.paid_price.value, + 'loading_addresses': loading_addresses, + 'unloading_addresses': unloading_addresses + } + #print(saved_data) + if self.order_number.value == None or len(str(self.order_number.value))==0: + self.error_message.value = "Order number is mandatory!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.selected_client_id == None: + self.error_message.value = "Please select the client!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.selected_transporter_id == None: + self.error_message.value = "Please select the transporter!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.product_description.value == None or len(self.product_description.value)==0: + self.error_message.value = "Please insert product description!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + # if self.ldm_quantity.value == None or len(self.ldm_quantity.value)==0: + # self.error_message.value = "Please insert LDM!" + # self.error_message.color = ft.Colors.RED + # self.error_message.update() + # return + # if self.kg_quantity.value == None or len(self.kg_quantity.value)==0: + # self.error_message.value = "Please insert KG!" + # self.error_message.color = ft.Colors.RED + # self.error_message.update() + # return + if self.track_reg_number.value == None or len(self.track_reg_number.value)==0: + self.error_message.value = "Please insert Track Registration Number!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.trailer_reg_number.value == None or len(self.trailer_reg_number.value)==0: + self.error_message.value = "Please insert Trailer Registration Number!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.received_price.value == None or len(self.received_price.value)==0: + self.error_message.value = "Please insert Price received!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.paid_price.value == None or len(self.paid_price.value)==0: + self.error_message.value = "Please insert Price paid!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if len(loading_addresses) == 0: + self.error_message.value = "Please add loading point!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if len(unloading_addresses) == 0: + self.error_message.value = "Please add unloading point!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + + # --- POST request to save the order in the database --- + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + user_id = self.page.session.get("user_id") + saved_data["user_id"] = user_id + user = self.get_user_data() + saved_data['terms'] = user['terms'] + + try: + response = requests.post(f"{API_BASE_URL}/orders/", json=saved_data, headers=headers) + if response.status_code == 201: + self.error_message.value = "Order saved successfully, Please wait!" + self.error_message.color = ft.Colors.GREEN + self.error_message.update() + order_id = response.json()['order_id'] + time.sleep(3) + pdf_name = f'order_{user_id}_{saved_data['order_number']}.pdf' + view_page = ViewPage(self.page, pdf_name, self, self.dashboard, order_id) + self.dashboard.placeholder.content = view_page.build() + self.dashboard.placeholder.update() + else: + self.error_message.value = f"Failed to save order: {response.status_code} - {response.text}" + except Exception as ex: + self.error_message.value = f"Error: {str(ex)}" + self.error_message.update() + + def get_user_data(self): + try: + 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}"}) + if response.status_code == 200: + user_data = response.json() + return user_data + return None + except Exception as e: + return None + + def on_archive_btn_click(self, e): + archive_page = ArchivePage(self.page, self.dashboard, self) + self.dashboard.placeholder.content = archive_page.build() + self.dashboard.placeholder.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) + return None + + def build(self): + self.save_btn = ft.FilledButton( + "Save and Generate", + width=200, + on_click=self.on_save_btn_click, + ) + self.save_row = ft.Row([],alignment=ft.MainAxisAlignment.CENTER) + subscription = self.get_current_subscription_plan() + if subscription: + if subscription['status'] != 'expired': + self.save_row.controls.append(self.save_btn) + return ft.Container( + ft.Column( + [ + ft.Row( + [ + ft.Column( + [ + ft.Text('Create Order Out', size=24, weight=ft.FontWeight.BOLD), + ft.Row( + [ + ft.Text("Number", size=18, weight=ft.FontWeight.BOLD), + self.order_number + ] + ) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.ElevatedButton("Archive", on_click=self.on_archive_btn_click, width=150) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Row( + [ + ft.Column( + [ + ft.Text("Client", size=18, weight=ft.FontWeight.BOLD), + self.client_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.client_results], + scroll=ft.ScrollMode.ADAPTIVE, + ), + #clip_behavior=ft.ClipBehavior., + expand=True, + padding=0, + ), + height=250 if len(self.filtered_clients) > 4 else None, + ) + ], + expand=5 + ), + ft.Column( + [ + ft.Text("Transporter", size=18, weight=ft.FontWeight.BOLD), + self.transporter_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.transporter_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_transporters) > 4 else None, + ) + ], + expand=5 + ), + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + ft.Row( + [ + ft.Text("Product Details", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + ft.Container( + content = self.product_description, + expand=True + ), + ft.Row( + [ + ft.Text( + value="LDM" + ), + self.ldm_quantity, + ft.Text(" "), + ft.Text( + value="KG" + ), + self.kg_quantity, + ], + expand=True + ) + ], + expand=True, + + ) + ], + expand=5 + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Truck / Trailer Info", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + self.track_reg_number, + self.trailer_reg_number + ], + expand=True + ) + ], + expand=2.5 + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Price", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + self.received_price, + self.paid_price + ], + expand=2.5 + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + ft.Text("Loading Points", size=18, weight=ft.FontWeight.BOLD), + ft.Column( + [ + self.loading_address_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.loading_address_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_addresses) > 4 else None, + ) + ], + expand=5 + ), + ft.Container( + content = self.loading_informations, + expand = True + ), + ft.Row( + [ + self.loading_date, + ft.ElevatedButton( + "Pick date", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2000, month=10, day=1), + last_date=datetime.datetime(year=2025, month=10, day=1), + on_change=self.on_loading_date_click, + ) + ), + ) + ], + expand=True + ), + ft.Row( + [ + self.loading_hour, + ft.ElevatedButton( + "Pick hour - Start", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_loading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Pick hour - End", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_loading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Reset", + on_click = self.on_reset_loading_hour_btn_click + ), + ], + expand=True + ), + self.loading_error_message, + ft.Row( + [ + ft.ElevatedButton( + "Add Loading Point", + on_click=self.add_loading_point_btn_click, + icon=ft.Icons.ADD + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.loading + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + ft.Text("Unloading Points", size=18, weight=ft.FontWeight.BOLD), + ft.Column( + [ + self.unloading_address_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.unloading_address_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_addresses_ul) > 4 else None, + ) + ], + expand=5 + ), + ft.Container( + content = self.unloading_informations, + expand=True + ), + ft.Row( + [ + self.unloading_date, + ft.ElevatedButton( + "Pick date", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2000, month=10, day=1), + last_date=datetime.datetime(year=2025, month=10, day=1), + on_change=self.on_unloading_date_click, + ) + ), + ) + ], + expand=True + ), + ft.Row( + [ + self.unloading_hour, + ft.ElevatedButton( + "Pick hour - Start", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_unloading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Pick hour - End", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_unloading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Reset", + on_click = self.on_reset_unloading_hour_btn_click + ), + ], + expand=True + ), + self.unloading_error_message, + ft.Row( + [ + ft.ElevatedButton( + "Add Unloading Point", + on_click=self.add_unloading_point_btn_click, + icon=ft.Icons.ADD + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.unloading + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Row( + [ + self.error_message, + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.save_row, + ], + expand=True, + scroll=ft.ScrollMode.ADAPTIVE, + spacing=20 + ) + ) + \ No newline at end of file diff --git a/transportmanager/client/pages/orders_page.py b/transportmanager/client/pages/orders_page.py new file mode 100644 index 0000000..376b673 --- /dev/null +++ b/transportmanager/client/pages/orders_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/profile_page.py b/transportmanager/client/pages/profile_page.py new file mode 100644 index 0000000..5c3dd2d --- /dev/null +++ b/transportmanager/client/pages/profile_page.py @@ -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 + ), + ) \ No newline at end of file diff --git a/transportmanager/client/pages/register_page.py b/transportmanager/client/pages/register_page.py new file mode 100644 index 0000000..13a11ea --- /dev/null +++ b/transportmanager/client/pages/register_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/report_page.py b/transportmanager/client/pages/report_page.py new file mode 100644 index 0000000..79658ea --- /dev/null +++ b/transportmanager/client/pages/report_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/reset_password_page.py b/transportmanager/client/pages/reset_password_page.py new file mode 100644 index 0000000..10db216 --- /dev/null +++ b/transportmanager/client/pages/reset_password_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/send_email_page.py b/transportmanager/client/pages/send_email_page.py new file mode 100644 index 0000000..fcb3f24 --- /dev/null +++ b/transportmanager/client/pages/send_email_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/subscription_page.py b/transportmanager/client/pages/subscription_page.py new file mode 100644 index 0000000..4dde52d --- /dev/null +++ b/transportmanager/client/pages/subscription_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/transporters_page.py b/transportmanager/client/pages/transporters_page.py new file mode 100644 index 0000000..610b577 --- /dev/null +++ b/transportmanager/client/pages/transporters_page.py @@ -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, + + ) diff --git a/transportmanager/client/pages/two_factor_page.py b/transportmanager/client/pages/two_factor_page.py new file mode 100644 index 0000000..e789998 --- /dev/null +++ b/transportmanager/client/pages/two_factor_page.py @@ -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 + ) \ No newline at end of file diff --git a/transportmanager/client/pages/view_orders_in_page.py b/transportmanager/client/pages/view_orders_in_page.py new file mode 100644 index 0000000..5d6ba7d --- /dev/null +++ b/transportmanager/client/pages/view_orders_in_page.py @@ -0,0 +1,1215 @@ +import flet as ft +import requests +import datetime +import time +from config import API_BASE_URL + +class ViewOrdersIn: + def __init__(self, page: ft.Page, archive, dashboard): + self.page = page + self.archive = archive + self.order_id = self.page.session.get("order_in_id") + self.order = self.get_order_details() + #print("*******") + #print(self.order) + #print("*******") + self.dashboard = dashboard + + self.order_number = ft.TextField( + label=f"Order Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value = self.order['order_number'] + ) + + self.selected_client_id = self.order['client_id'] + + self.all_clients = self.get_all_clients() + self.filtered_clients = self.all_clients.copy() + self.client_search_field = ft.TextField(label="Search Clients...", on_change=self.on_searching_client) + self.client_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for client in self.all_clients + ] + ) + + #update search filed + for client in self.all_clients: + if client["id"] == self.selected_client_id: + self.client_search_field.value = client['name'] + + self.all_addresses = self.get_all_addresses() + self.selected_loading_address_id = None + self.filtered_addresses = self.all_addresses.copy() + self.loading_address_search_field = ft.TextField(label="Search Loading Address...", on_change=self.on_searching_loading_address) + self.loading_address_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ + ft.Text(address["name"], expand=True), + ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click=lambda e, t=address: self.on_location_btn_click(t)) + ], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=address: self.on_loading_address_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for address in self.all_addresses + ] + ) + + self.loading_informations = ft.TextField( + label="Loading Instructions", + min_lines=3, + max_lines=7, + multiline=True, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.loading_date = ft.TextField(label="Date", expand=True, read_only=True) + + self.loading_hour = ft.TextField(label="Hour", expand=True, read_only=True) + + self.loading = ft.ListView( + spacing=10, + ) + + self.selected_unloading_address_id = None + self.filtered_addresses_ul = self.all_addresses.copy() + self.unloading_address_search_field = ft.TextField(label="Search Unloading Address...", on_change=self.on_searching_unloading_address) + self.unloading_address_results = ft.Column( + spacing=10, + controls=[ + ft.Container( + content=ft.Row( + controls=[ + ft.Text(address["name"], expand=True), + ft.IconButton(icon=ft.Icons.LOCATION_PIN, on_click=lambda e, t=address: self.on_location_btn_click(t)) + ], + expand=True + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, t=address: self.on_unloading_address_selected(e, t), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + for address in self.all_addresses + ] + ) + + self.unloading_informations = ft.TextField( + label="Unloading Instructions", + min_lines=3, + max_lines=7, + multiline=True, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + ) + + self.unloading_date = ft.TextField(label="Date", expand=True, read_only=True) + + self.unloading_hour = ft.TextField(label="Hour", expand=True, read_only=True) + + self.unloading = ft.ListView( + spacing=10, + ) + + self.ldm_quantity = ft.TextField( + expand=True, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value=self.order['ldb_quantity'] + ) + + self.kg_quantity = ft.TextField( + expand=True, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value=self.order['kg_quantity'] + ) + + self.track_reg_number = ft.TextField( + label="Track Reg. Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value=self.order['track_reg_number'] + ) + + self.trailer_reg_number = ft.TextField( + label="Trailer Reg. Number", + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value=self.order['trailer_reg_number'] + ) + + self.received_price = ft.TextField( + label="Price Received - visible only to you!", + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value=self.order['received_price'] + ) + + self.error_message = ft.Text(color = ft.Colors.RED) + + self.loading_query = [] + self.loading_error_message = ft.Text(color= ft.Colors.RED) + + self.unloading_query = [] + self.unloading_error_message = ft.Text(color= ft.Colors.RED) + + self.product_description= ft.TextField( + label="Description", + multiline=True, + min_lines=3, + max_lines=5, + input_filter=ft.InputFilter( + allow=True, + regex_string=r"^[\x20-\x7E]*$", + replacement_string="" + ), + value=self.order['products_description'] + ) + + #add loading points + self.init_loading_query = self.order['loading_points'] + addresses = [] + for init_addr in self.init_loading_query: + for all_addr in self.all_addresses: + if init_addr['destination_id'] == all_addr['id']: + adr = '' + street_and_number = all_addr["address"].split(" %")[0] + postal_code = all_addr["address"].split(" %")[1] + city = all_addr["address"].split(" %")[2] + region_county = all_addr["address"].split(" %")[3] + country = all_addr["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + address = { + 'loading_address_id': init_addr['destination_id'], + 'loading_address_name': all_addr['name'], + 'loading_address': adr, + 'loading_date': init_addr['point_data'], + 'loading_hour': init_addr['point_hour'], + 'loading_informatins': init_addr['informatins'] + } + addresses.append(address) + break + self.loading_query = addresses + self.loading.controls = self.create_loading_list(addresses, self.on_delete_loading_address_btn_click) + + #add unloading points + self.init_unloading_query = self.order['unloading_points'] + addresses = [] + for init_addr in self.init_unloading_query: + for all_addr in self.all_addresses: + if init_addr['destination_id'] == all_addr['id']: + adr = '' + street_and_number = all_addr["address"].split(" %")[0] + postal_code = all_addr["address"].split(" %")[1] + city = all_addr["address"].split(" %")[2] + region_county = all_addr["address"].split(" %")[3] + country = all_addr["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + address = { + 'unloading_address_id': init_addr['destination_id'], + 'unloading_address_name': all_addr['name'], + 'unloading_address': adr, + 'unloading_date': init_addr['point_data'], + 'unloading_hour': init_addr['point_hour'], + 'unloading_informatins': init_addr['informatins'] + } + addresses.append(address) + break + self.unloading_query = addresses + self.unloading.controls = self.create_unloading_list(addresses, self.on_delete_unloading_address_btn_click) + + def on_go_back_btn_click(self, e): + self.dashboard.placeholder.content = self.archive.build() + self.dashboard.placeholder.update() + + def get_order_details(self): + try: + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/orders_in/{self.order_id}", headers=headers) + if response.status_code == 200: + return response.json() + else: + return None + except Exception as e: + print("Error loading clients:", e) + return None + + def on_location_btn_click(self, destination): + query = destination["address"].replace(", ", "+") + maps_url = f"https://www.google.com/maps/search/?api=1&query={query}" + self.page.launch_url(maps_url) + + def on_searching_client(self, e): + query = e.control.value.lower() + self.filtered_clients = [client for client in self.all_clients if query in client["name"].lower()] + self.update_client_list(self.filtered_clients) + + def init_search_client(self): + query = self.client_search_field.value.lower() + self.filtered_clients = [client for client in self.all_clients if query in client["name"].lower()] + self.client_results.controls.clear() + for client in self.filtered_clients: + self.client_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + + def update_client_list(self, clients): + self.client_results.controls.clear() + for client in clients: + self.client_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(client["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if client["id"] == self.selected_client_id else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e, c=client: self.on_client_selected(e, c), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.client_results.update() + + def on_client_selected(self, e, client): + self.selected_client_id = client["id"] + self.client_search_field.value = client["name"] + self.client_search_field.update() + self.update_client_list(self.filtered_clients) + + def on_searching_loading_address(self, e): + query = e.control.value.lower() + self.filtered_addresses = [a for a in self.all_addresses if query in a["name"].lower()] + self.update_loading_addresses_list(self.filtered_addresses) + + def update_loading_addresses_list(self, addresses): + self.loading_address_results.controls.clear() + for address in addresses: + self.loading_address_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(address["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if address["id"] == getattr(self, "selected_loading_address_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e,a=address: self.on_loading_address_selected(e, a), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.loading_address_results.update() + + def on_loading_address_selected(self, e, address): + self.selected_loading_address_id = address["id"] + self.loading_address_search_field.value = address["name"] + self.loading_address_search_field.update() + self.update_loading_addresses_list(self.filtered_addresses) + + def on_loading_date_click(self, e): + self.loading_date.value = e.control.value.strftime('%m/%d/%Y') + self.loading_date.update() + #print(self.loading_date.value) + + def on_loading_hour_click(self, e): + if len(self.loading_hour.value) != None and len(self.loading_hour.value)==0: + self.loading_hour.value = str(e.control.value) + else: + self.loading_hour.value += f' - {e.control.value}' + self.loading_hour.update() + #print(self.loading_hour.value) + + def on_reset_loading_hour_btn_click(self, e): + self.loading_hour.value = None + self.loading_hour.update() + + def add_loading_point_btn_click(self, e): + adr = None + name = None + #print(self.selected_loading_address_id) + for _address in self.all_addresses: + if _address['id'] == self.selected_loading_address_id: + adr = '' + street_and_number = _address["address"].split(" %")[0] + postal_code = _address["address"].split(" %")[1] + city = _address["address"].split(" %")[2] + region_county = _address["address"].split(" %")[3] + country = _address["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + name = _address['name'] + #print(adr) + #print(_address['address']) + + loading_informatins = self.loading_informations.value + date = self.loading_date.value + hour = self.loading_hour.value + + #create loading list + address = { + 'loading_address_id': self.selected_loading_address_id, + 'loading_address_name': name, + 'loading_address': adr, + 'loading_informatins': loading_informatins, + 'loading_date': date, + 'loading_hour': hour + } + #print(address) + + if self.selected_loading_address_id == None: + self.loading_error_message.value = "Please select loading point!" + self.loading_error_message.update() + return + if self.loading_informations.value == None or len(self.loading_informations.value) == 0: + self.loading_error_message.value = "Add loading informations!" + self.loading_error_message.update() + return + if self.loading_date.value == None or len(str(self.loading_date.value)) == 0: + self.loading_error_message.value = "Add loading date!" + self.loading_error_message.update() + return + # if self.loading_hour.value == None or len(str(self.loading_hour.value)) == 0: + # self.loading_error_message.value = "Add loading hour!" + # self.loading_error_message.update() + # return + + if self.selected_loading_address_id: + self.loading_query.append(address) + self.loading.controls.clear() + self.loading.controls = self.create_loading_list(self.loading_query, self.on_delete_loading_address_btn_click) + self.loading.update() + + #reset to default + self.selected_loading_address_id = None + self.loading_informations.value = None + self.loading_informations.update() + self.loading_date.value = None + self.loading_date.update() + self.loading_hour.value = None + self.loading_hour.update() + self.loading_error_message.value = None + self.loading_error_message.update() + else: + self.loading_error_message.value = "All fields of the loading address are required." + self.loading_error_message.update() + + def on_searching_unloading_address(self, e): + query = e.control.value.lower() + self.filtered_addresses_ul = [a for a in self.all_addresses if query in a["name"].lower()] + self.update_unloading_addresses_list(self.filtered_addresses_ul) + + def update_unloading_addresses_list(self, addresses): + self.unloading_address_results.controls.clear() + for address in addresses: + self.unloading_address_results.controls.append( + ft.Container( + content=ft.Row( + controls=[ft.Text(address["name"], expand=True)], + expand=True + ), + bgcolor=ft.Colors.BLUE_100 if address["id"] == getattr(self, "selected_unloading_address_id", None) else ft.Colors.BLUE_50, + padding=10, + ink=True, + on_click=lambda e,a=address: self.on_unloading_address_selected(e, a), + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=10, + expand=True + ) + ) + self.unloading_address_results.update() + + def on_unloading_address_selected(self, e, address): + self.selected_unloading_address_id = address["id"] + self.unloading_address_search_field.value = address["name"] + self.unloading_address_search_field.update() + self.update_unloading_addresses_list(self.filtered_addresses_ul) + + def on_unloading_date_click(self, e): + self.unloading_date.value = e.control.value.strftime('%m/%d/%Y') + self.unloading_date.update() + + def on_unloading_hour_click(self, e): + if len(self.unloading_hour.value) != None and len(self.unloading_hour.value)==0: + self.unloading_hour.value = str(e.control.value) + else: + self.unloading_hour.value += f' - {e.control.value}' + self.unloading_hour.update() + + def on_reset_unloading_hour_btn_click(self, e): + self.unloading_hour.value = None + self.unloading_hour.update() + + def add_unloading_point_btn_click(self, e): + adr = None + name = None + #print(self.selected_unloading_address_id) + for _address in self.all_addresses: + if _address['id'] == self.selected_unloading_address_id: + adr = '' + street_and_number = _address["address"].split(" %")[0] + postal_code = _address["address"].split(" %")[1] + city = _address["address"].split(" %")[2] + region_county = _address["address"].split(" %")[3] + country = _address["address"].split(" %")[4] + if len(street_and_number) > 0: + adr += street_and_number +', ' + if len(postal_code) > 0: + adr += postal_code +', ' + if len(city) > 0: + adr += city +', ' + if len(region_county) > 0: + adr += region_county +', ' + if len(country) > 0: + adr += country + name = _address['name'] + unloading_informatins = self.unloading_informations.value + date = self.unloading_date.value + hour = self.unloading_hour.value + address = { + 'unloading_address_id': self.selected_unloading_address_id, + 'unloading_address_name': name, + 'unloading_address': adr, + 'unloading_informatins': unloading_informatins, + 'unloading_date': date, + 'unloading_hour': hour + } + + if self.selected_unloading_address_id == None: + self.unloading_error_message.value = "Please select unloading point!" + self.unloading_error_message.update() + return + if self.unloading_informations.value == None or len(self.unloading_informations.value) == 0: + self.unloading_error_message.value = "Add unloading informations!" + self.unloading_error_message.update() + return + if self.unloading_date.value == None or len(str(self.unloading_date.value)) == 0: + self.unloading_error_message.value = "Add unloading date!" + self.unloading_error_message.update() + return + # if self.unloading_hour.value == None or len(str(self.unloading_hour.value)) == 0: + # self.unloading_error_message.value = "Add unloading hour!" + # self.unloading_error_message.update() + # return + + if self.selected_unloading_address_id: + self.unloading_query.append(address) + self.unloading.controls.clear() + self.unloading.controls = self.create_unloading_list(self.unloading_query, self.on_delete_unloading_address_btn_click) + self.unloading.update() + + #reset to default + self.selected_unloading_address_id = None + self.unloading_informations.value = None + self.unloading_informations.update() + self.unloading_date.value = None + self.unloading_date.update() + self.unloading_hour.value = None + self.unloading_hour.update() + self.unloading_error_message.value = None + self.unloading_error_message.update() + else: + self.unloading_error_message.value = "All fields of the unloading address are required." + self.unloading_error_message.update() + + def get_all_clients(self): + 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: + return response.json() + else: + print("Failed to fetch clients:", response.status_code) + return [] + + def get_all_addresses(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/destinations/", headers=headers) + if response.status_code == 200: + return response.json() + else: + print("Failed to fetch addresses:", response.status_code) + return [] + + def create_loading_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['loading_address_name'], + expand=True, + size=15, + weight=ft.FontWeight.BOLD + ), + ft.Text( + item['loading_address'], + expand=True, + size=12, + ), + ft.Row( + [ + ft.Text( + f"Date: {item['loading_date']}", + expand=True, + size=12, + ), + ft.Text( + f"Hour: {item['loading_hour']}", + expand=True, + size=12, + ) + ] + ) + + ] + ), + ft.Row( + [ + ft.FilledButton( + "Delete", + bgcolor=ft.Colors.RED, + on_click=lambda e, id=item: on_click_handler(id), + width=100 + ) + ] + ) + + ], + 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 on_delete_loading_address_btn_click(self, item): + self.loading_query.remove(item) + self.loading.controls.clear() + self.loading.controls = self.create_loading_list(self.loading_query, self.on_delete_loading_address_btn_click) + self.loading.update() + + def create_unloading_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['unloading_address_name'], + expand=True, + size=15, + weight=ft.FontWeight.BOLD + ), + ft.Text( + item['unloading_address'], + expand=True, + size=12, + ), + ft.Row( + [ + ft.Text( + f"Date: {item['unloading_date']}", + expand=True, + size=12, + ), + ft.Text( + f"Hour: {item['unloading_hour']}", + expand=True, + size=12, + ) + ] + ) + + ] + ), + ft.Row( + [ + ft.FilledButton( + "Delete", + bgcolor=ft.Colors.RED, + on_click=lambda e, id=item: on_click_handler(id), + width=100 + ) + ] + ) + + ], + 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 on_delete_unloading_address_btn_click(self, item): + #print(item) + #print(self.unloading_query) + self.unloading_query.remove(item) + self.unloading.controls.clear() + self.unloading.controls = self.create_unloading_list(self.unloading_query, self.on_delete_unloading_address_btn_click) + self.unloading.update() + + def on_save_btn_click(self, e): + #print(self.unloading_query) + loading_addresses = [] + unloading_addresses = [] + for laddr in self.loading_query: + #print(self.loading_query) + #print(laddr) + laddr_copy = laddr.copy() + #if isinstance(laddr_copy.get("loading_hour"), datetime.time): + #laddr_copy["loading_hour"] = laddr_copy["loading_hour"].strftime("%H:%M") + loading_addresses.append(laddr_copy) + + for uaddr in self.unloading_query: + uaddr_copy = uaddr.copy() + #if isinstance(uaddr_copy.get("unloading_hour"), datetime.time): + #uaddr_copy["unloading_hour"] = uaddr_copy["unloading_hour"].strftime("%H:%M") + unloading_addresses.append(uaddr_copy) + + saved_data = { + 'order_number': self.order_number.value, + 'client_id': self.selected_client_id, + 'products_description': self.product_description.value, + 'ldb_quantity': self.ldm_quantity.value, + 'kg_quantity': self.kg_quantity.value, + 'track_reg_number': self.track_reg_number.value, + 'trailer_reg_number': self.trailer_reg_number.value, + 'received_price': self.received_price.value, + 'loading_addresses': loading_addresses, + 'unloading_addresses': unloading_addresses + } + #print(saved_data) + if self.order_number.value == None or len(self.order_number.value)==0: + self.error_message.value = "Order number is mandatory!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.selected_client_id == None: + self.error_message.value = "Please select the client!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.product_description.value == None or len(self.product_description.value)==0: + self.error_message.value = "Please insert product description!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + # if self.ldm_quantity.value == None or len(self.ldm_quantity.value)==0: + # self.error_message.value = "Please insert LDM!" + # self.error_message.color = ft.Colors.RED + # self.error_message.update() + # return + # if self.kg_quantity.value == None or len(self.kg_quantity.value)==0: + # self.error_message.value = "Please insert KG!" + # self.error_message.color = ft.Colors.RED + # self.error_message.update() + # return + if self.track_reg_number.value == None or len(self.track_reg_number.value)==0: + self.error_message.value = "Please insert Track Registration Number!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if self.trailer_reg_number.value == None or len(self.trailer_reg_number.value)==0: + self.error_message.value = "Please insert Trailer Registration Number!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if len(loading_addresses) == 0: + self.error_message.value = "Please add loading point!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + if len(unloading_addresses) == 0: + self.error_message.value = "Please add unloading point!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return + + # --- POST request to save the order in the database --- + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + user_id = self.page.session.get("user_id") + saved_data["user_id"] = user_id + user = self.get_user_data() + saved_data['terms'] = user['terms'] + + try: + response = requests.put(f"{API_BASE_URL}/orders_in/{self.order_id}", json=saved_data, headers=headers) + if response.status_code == 200: + self.error_message.value = "Order updated successfully, Please wait!" + self.error_message.color = ft.Colors.GREEN + self.error_message.update() + time.sleep(3) + self.dashboard.placeholder.content = self.archive.build() + self.dashboard.placeholder.update() + else: + self.error_message.value = f"Failed to save order: {response.status_code} - {response.text}" + self.error_message.update() + except Exception as ex: + self.error_message.value = f"Error: {str(ex)}" + self.error_message.update() + + def get_user_data(self): + try: + 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}"}) + if response.status_code == 200: + user_data = response.json() + return user_data + return None + except Exception as e: + return None + + 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) + return None + + def build(self): + self.save_btn = ft.FilledButton( + "Save", + width=200, + on_click=self.on_save_btn_click, + ) + self.save_row = ft.Row([],alignment=ft.MainAxisAlignment.CENTER) + if self.get_current_subscription_plan()['status'] != 'expired': + self.save_row.controls.append(self.save_btn) + self.init_search_client() + return ft.Container( + ft.Column( + [ + ft.Row( + [ + ft.Text("View Order In", 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, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Row( + [ + ft.Text("Number", size=18, weight=ft.FontWeight.BOLD), + self.order_number + ] + ), + ft.Row( + [ + ft.Column( + [ + ft.Text("Client", size=18, weight=ft.FontWeight.BOLD), + self.client_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.client_results], + scroll=ft.ScrollMode.ADAPTIVE, + ), + #clip_behavior=ft.ClipBehavior., + expand=True, + padding=0, + ), + height=250 #if len(self.client_results.controls) > 4 else None, + ) + ], + expand=True + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + ft.Row( + [ + ft.Text("Product Details", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + ft.Container( + content = self.product_description, + expand=True + ), + ft.Row( + [ + ft.Text( + value="LDM" + ), + self.ldm_quantity, + ft.Text(" "), + ft.Text( + value="KG" + ), + self.kg_quantity, + ], + expand=True + ) + ], + expand=True, + + ) + ], + expand=5 + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Truck / Trailer Info", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + self.track_reg_number, + self.trailer_reg_number + ], + expand=True + ) + ], + expand=2.5 + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Price", size=18, weight=ft.FontWeight.BOLD) + ], + alignment=ft.MainAxisAlignment.START + ), + self.received_price, + ], + expand=2.5 + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + ft.Text("Loading Points", size=18, weight=ft.FontWeight.BOLD), + ft.Column( + [ + self.loading_address_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.loading_address_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_addresses) > 4 else None, + ) + ], + expand=5 + ), + ft.Container( + content = self.loading_informations, + expand = True + ), + ft.Row( + [ + self.loading_date, + ft.ElevatedButton( + "Pick date", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2000, month=10, day=1), + last_date=datetime.datetime(year=2025, month=10, day=1), + on_change=self.on_loading_date_click, + ) + ), + ) + ], + expand=True + ), + ft.Row( + [ + self.loading_hour, + ft.ElevatedButton( + "Pick hour - Start", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_loading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Pick hour - End", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_loading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Reset", + on_click = self.on_reset_loading_hour_btn_click + ), + ], + expand=True + ), + self.loading_error_message, + ft.Row( + [ + ft.ElevatedButton( + "Add Loading Point", + on_click=self.add_loading_point_btn_click, + icon=ft.Icons.ADD + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.loading + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ), + ft.Column( + [ + ft.Text("Unloading Points", size=18, weight=ft.FontWeight.BOLD), + ft.Column( + [ + self.unloading_address_search_field, + ft.Container( + content=ft.Container( + content=ft.Column( + controls=[self.unloading_address_results], + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + padding=0, + ), + height=250 if len(self.filtered_addresses_ul) > 4 else None, + ) + ], + expand=5 + ), + ft.Container( + content = self.unloading_informations, + expand=True + ), + ft.Row( + [ + self.unloading_date, + ft.ElevatedButton( + "Pick date", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2000, month=10, day=1), + last_date=datetime.datetime(year=2025, month=10, day=1), + on_change=self.on_unloading_date_click, + ) + ), + ) + ], + expand=True + ), + ft.Row( + [ + self.unloading_hour, + ft.ElevatedButton( + "Pick hour - Start", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_unloading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Pick hour - End", + icon=ft.Icons.CALENDAR_MONTH, + on_click=lambda e: self.page.open( + ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=self.on_unloading_hour_click, + time_picker_entry_mode = ft.TimePickerEntryMode.INPUT_ONLY + ) + ), + ), + ft.ElevatedButton( + "Reset", + on_click = self.on_reset_unloading_hour_btn_click + ), + ], + expand=True + ), + self.unloading_error_message, + ft.Row( + [ + ft.ElevatedButton( + "Add Unloading Point", + on_click=self.add_unloading_point_btn_click, + icon=ft.Icons.ADD + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.unloading + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ) + ], + expand=True, + spacing=20, + vertical_alignment=ft.CrossAxisAlignment.START + ), + ft.Row( + [ + self.error_message, + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.save_row, + ], + expand=True, + scroll=ft.ScrollMode.ADAPTIVE, + spacing=20 + ) + ) \ No newline at end of file diff --git a/transportmanager/client/pages/view_page.py b/transportmanager/client/pages/view_page.py new file mode 100644 index 0000000..4e85278 --- /dev/null +++ b/transportmanager/client/pages/view_page.py @@ -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, + ] + ) + ) \ No newline at end of file diff --git a/transportmanager/client/requirements.txt b/transportmanager/client/requirements.txt new file mode 100644 index 0000000..07e9ca9 --- /dev/null +++ b/transportmanager/client/requirements.txt @@ -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 \ No newline at end of file diff --git a/transportmanager/requirements.txt b/transportmanager/requirements.txt new file mode 100644 index 0000000..cec0e37 --- /dev/null +++ b/transportmanager/requirements.txt @@ -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 \ No newline at end of file diff --git a/transportmanager/server/.DS_Store b/transportmanager/server/.DS_Store new file mode 100644 index 0000000..c522407 Binary files /dev/null and b/transportmanager/server/.DS_Store differ diff --git a/transportmanager/server/admin/__init__.py b/transportmanager/server/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/transportmanager/server/admin/billing.py b/transportmanager/server/admin/billing.py new file mode 100644 index 0000000..e69de29 diff --git a/transportmanager/server/admin/subscription.py b/transportmanager/server/admin/subscription.py new file mode 100644 index 0000000..319672f --- /dev/null +++ b/transportmanager/server/admin/subscription.py @@ -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 + diff --git a/transportmanager/server/admin/tenants.py b/transportmanager/server/admin/tenants.py new file mode 100644 index 0000000..9de7a06 --- /dev/null +++ b/transportmanager/server/admin/tenants.py @@ -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("/", 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 diff --git a/transportmanager/server/app.py b/transportmanager/server/app.py new file mode 100644 index 0000000..b1ab84f --- /dev/null +++ b/transportmanager/server/app.py @@ -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) \ No newline at end of file diff --git a/transportmanager/server/database.py b/transportmanager/server/database.py new file mode 100644 index 0000000..f5d2815 --- /dev/null +++ b/transportmanager/server/database.py @@ -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() \ No newline at end of file diff --git a/transportmanager/server/generated_pdfs/.DS_Store b/transportmanager/server/generated_pdfs/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/transportmanager/server/generated_pdfs/.DS_Store differ diff --git a/transportmanager/server/models/client.py b/transportmanager/server/models/client.py new file mode 100644 index 0000000..973f724 --- /dev/null +++ b/transportmanager/server/models/client.py @@ -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() diff --git a/transportmanager/server/models/destinations.py b/transportmanager/server/models/destinations.py new file mode 100644 index 0000000..0236826 --- /dev/null +++ b/transportmanager/server/models/destinations.py @@ -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 \ No newline at end of file diff --git a/transportmanager/server/models/order_in.py b/transportmanager/server/models/order_in.py new file mode 100644 index 0000000..91268cb --- /dev/null +++ b/transportmanager/server/models/order_in.py @@ -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] \ No newline at end of file diff --git a/transportmanager/server/models/order_out.py b/transportmanager/server/models/order_out.py new file mode 100644 index 0000000..38131ae --- /dev/null +++ b/transportmanager/server/models/order_out.py @@ -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] \ No newline at end of file diff --git a/transportmanager/server/models/subscription.py b/transportmanager/server/models/subscription.py new file mode 100644 index 0000000..eb7227b --- /dev/null +++ b/transportmanager/server/models/subscription.py @@ -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") \ No newline at end of file diff --git a/transportmanager/server/models/transporters.py b/transportmanager/server/models/transporters.py new file mode 100644 index 0000000..8f120e8 --- /dev/null +++ b/transportmanager/server/models/transporters.py @@ -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() \ No newline at end of file diff --git a/transportmanager/server/models/user.py b/transportmanager/server/models/user.py new file mode 100644 index 0000000..add745e --- /dev/null +++ b/transportmanager/server/models/user.py @@ -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() \ No newline at end of file diff --git a/transportmanager/server/requirements.txt b/transportmanager/server/requirements.txt new file mode 100644 index 0000000..a774390 --- /dev/null +++ b/transportmanager/server/requirements.txt @@ -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 \ No newline at end of file diff --git a/transportmanager/server/routes/__init__.py b/transportmanager/server/routes/__init__.py new file mode 100644 index 0000000..d01f08f --- /dev/null +++ b/transportmanager/server/routes/__init__.py @@ -0,0 +1,3 @@ +from flask import Blueprint + +# Placeholder for blueprint registration if needed dynamically \ No newline at end of file diff --git a/transportmanager/server/routes/auth.py b/transportmanager/server/routes/auth.py new file mode 100644 index 0000000..f76e9bf --- /dev/null +++ b/transportmanager/server/routes/auth.py @@ -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 \ No newline at end of file diff --git a/transportmanager/server/routes/clients.py b/transportmanager/server/routes/clients.py new file mode 100644 index 0000000..dec313a --- /dev/null +++ b/transportmanager/server/routes/clients.py @@ -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("/", 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("/", 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("/", 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 \ No newline at end of file diff --git a/transportmanager/server/routes/destinations.py b/transportmanager/server/routes/destinations.py new file mode 100644 index 0000000..512750e --- /dev/null +++ b/transportmanager/server/routes/destinations.py @@ -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("/", 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("/", 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("//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 \ No newline at end of file diff --git a/transportmanager/server/routes/orders_out.py b/transportmanager/server/routes/orders_out.py new file mode 100644 index 0000000..a94c510 --- /dev/null +++ b/transportmanager/server/routes/orders_out.py @@ -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("/", 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("/", 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("/", 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/", 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/", 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 \ No newline at end of file diff --git a/transportmanager/server/routes/ouders_in.py b/transportmanager/server/routes/ouders_in.py new file mode 100644 index 0000000..9324845 --- /dev/null +++ b/transportmanager/server/routes/ouders_in.py @@ -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("/", 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("/", 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("/", 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 \ No newline at end of file diff --git a/transportmanager/server/routes/profile.py b/transportmanager/server/routes/profile.py new file mode 100644 index 0000000..fbe44bb --- /dev/null +++ b/transportmanager/server/routes/profile.py @@ -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 \ No newline at end of file diff --git a/transportmanager/server/routes/report.py b/transportmanager/server/routes/report.py new file mode 100644 index 0000000..f445875 --- /dev/null +++ b/transportmanager/server/routes/report.py @@ -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 \ No newline at end of file diff --git a/transportmanager/server/routes/subscription.py b/transportmanager/server/routes/subscription.py new file mode 100644 index 0000000..f34d0fb --- /dev/null +++ b/transportmanager/server/routes/subscription.py @@ -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 \ No newline at end of file diff --git a/transportmanager/server/routes/transporters.py b/transportmanager/server/routes/transporters.py new file mode 100644 index 0000000..97993db --- /dev/null +++ b/transportmanager/server/routes/transporters.py @@ -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("/", 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("/", 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("/", 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 \ No newline at end of file diff --git a/transportmanager/server/schema.sql b/transportmanager/server/schema.sql new file mode 100644 index 0000000..7caf4e9 --- /dev/null +++ b/transportmanager/server/schema.sql @@ -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 +); \ No newline at end of file diff --git a/transportmanager/server/schema_sqlite.sql b/transportmanager/server/schema_sqlite.sql new file mode 100644 index 0000000..cb5ab7f --- /dev/null +++ b/transportmanager/server/schema_sqlite.sql @@ -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 +); \ No newline at end of file diff --git a/transportmanager/server/utils/cancel_order.py b/transportmanager/server/utils/cancel_order.py new file mode 100644 index 0000000..b1e0519 --- /dev/null +++ b/transportmanager/server/utils/cancel_order.py @@ -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 ") + 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}") \ No newline at end of file diff --git a/transportmanager/server/utils/email.py b/transportmanager/server/utils/email.py new file mode 100644 index 0000000..164170d --- /dev/null +++ b/transportmanager/server/utils/email.py @@ -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) \ No newline at end of file diff --git a/transportmanager/server/utils/maps.py b/transportmanager/server/utils/maps.py new file mode 100644 index 0000000..b8d629e --- /dev/null +++ b/transportmanager/server/utils/maps.py @@ -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) \ No newline at end of file diff --git a/transportmanager/server/utils/pdf.py b/transportmanager/server/utils/pdf.py new file mode 100644 index 0000000..84f3c46 --- /dev/null +++ b/transportmanager/server/utils/pdf.py @@ -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"""{user_data.get("name", "")}
+{user_data.get("register_number", "")}
+{user_data.get("vat", "")}
+{address}
+{""}
+{user_data.get("contact_name", "")}
+{user_data.get("phone", "")}
+{user_data.get("email", "")} +""" + transporter_text = f"""{transporter_data.get("name", "")}
+{transporter_data.get("contact_person", "")}
+{transporter_data.get("phone", "")}
+{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"Loading Order: {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 \ No newline at end of file