From 106045d72ac764a536ff6bdd79344de631cc0d7e Mon Sep 17 00:00:00 2001 From: Marius Robert Macamete Date: Mon, 8 Sep 2025 18:06:00 +0300 Subject: [PATCH] intrgrating suggestions after betta 1 --- .../client/pages/archive_in_page.py | 53 +++- transportmanager/client/pages/archive_page.py | 40 ++- transportmanager/client/pages/clients_page.py | 38 +++ .../client/pages/destinations_page.py | 38 +++ .../client/pages/orders_edit_page.py | 12 +- .../client/pages/orders_in_page.py | 269 ++++++++++++----- .../client/pages/orders_out_page.py | 69 ++++- transportmanager/client/pages/orders_page.py | 53 +++- transportmanager/client/pages/profile_page.py | 62 +++- transportmanager/client/pages/report_page.py | 38 +++ .../client/pages/subscription_page.py | 36 +++ .../client/pages/temporary_password_page.py | 60 ++++ .../client/pages/transporters_page.py | 38 +++ .../client/pages/two_factor_page.py | 29 +- transportmanager/client/pages/users_page.py | 274 ++++++++++++++++++ .../client/pages/view_orders_in_page.py | 78 ++++- transportmanager/server/admin/tenants.py | 28 +- transportmanager/server/app.py | 3 +- transportmanager/server/assets/Manual.pdf | Bin 0 -> 15441 bytes .../server/models/company_users.py | 81 ++++++ transportmanager/server/models/order_in.py | 12 +- transportmanager/server/models/user.py | 55 +++- transportmanager/server/routes/auth.py | 26 +- transportmanager/server/routes/clients.py | 9 + .../server/routes/company_user.py | 118 ++++++++ .../server/routes/destinations.py | 13 + transportmanager/server/routes/orders_out.py | 24 ++ transportmanager/server/routes/ouders_in.py | 54 +++- transportmanager/server/routes/report.py | 5 + .../server/routes/subscription.py | 4 + .../server/routes/transporters.py | 17 ++ transportmanager/server/schema_sqlite.sql | 22 +- transportmanager/server/utils/email.py | 6 +- .../server/utils/welcome_email.py | 31 ++ 34 files changed, 1549 insertions(+), 146 deletions(-) create mode 100644 transportmanager/client/pages/temporary_password_page.py create mode 100644 transportmanager/client/pages/users_page.py create mode 100644 transportmanager/server/assets/Manual.pdf create mode 100644 transportmanager/server/models/company_users.py create mode 100644 transportmanager/server/routes/company_user.py create mode 100644 transportmanager/server/utils/welcome_email.py diff --git a/transportmanager/client/pages/archive_in_page.py b/transportmanager/client/pages/archive_in_page.py index ee5eaae..e8ca603 100644 --- a/transportmanager/client/pages/archive_in_page.py +++ b/transportmanager/client/pages/archive_in_page.py @@ -58,6 +58,11 @@ class ArchiveInPage: self.selected_delete_order = order self.page.open(self.delete_dialog) + def download_order_file(self, order): + file = order['file'] + url=f"{API_BASE_URL}/orders_in/files/{file}" + self.page.launch_url(url) + def load_orders(self): try: token = self.page.client_storage.get("token") @@ -90,6 +95,11 @@ class ArchiveInPage: self.orders_list.controls.clear() for order in self.orders: client = self.get_client(order['client_id']) + buttons = [] + if order.get("file"): + buttons.append(ft.Button("Download", icon=ft.Icons.DOWNLOAD, on_click=lambda e, o=order: self.download_order_file(o))) + buttons.append(ft.Button("View", icon=ft.Icons.PICTURE_AS_PDF, on_click=lambda e, o=order: self.view_order(o))) + buttons.append(ft.Button("Delete", icon=ft.Icons.CANCEL, on_click=lambda e, o=order: self.cancel_order(o))) self.orders_list.controls.append( ft.Container( content=ft.Row([ @@ -97,8 +107,7 @@ class ArchiveInPage: 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)) + *buttons ]), padding=10, border=ft.border.all(1, ft.Colors.GREY_300), @@ -108,6 +117,18 @@ class ArchiveInPage: ) self.page.update() + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + id = self.page.session.get("user_id") + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return True if response.json()['orders_in'] == 1 else False + def build(self): self.refresh() return ft.Container( @@ -115,7 +136,7 @@ class ArchiveInPage: [ ft.Row( [ - ft.Text("Archive", size=24, weight=ft.FontWeight.BOLD), + ft.Text("Archive Orders 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 @@ -123,4 +144,30 @@ class ArchiveInPage: self.orders_list ] ) + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Archive Orders In", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) \ No newline at end of file diff --git a/transportmanager/client/pages/archive_page.py b/transportmanager/client/pages/archive_page.py index f8aeaab..1554263 100644 --- a/transportmanager/client/pages/archive_page.py +++ b/transportmanager/client/pages/archive_page.py @@ -107,6 +107,18 @@ class ArchivePage: self.dashboard.placeholder.content = self.order_page.build() self.dashboard.placeholder.update() + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + id = self.page.session.get("user_id") + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return True if response.json()['orders_out'] == 1 else False + def build(self): self.refresh() return ft.Container( @@ -114,7 +126,7 @@ class ArchivePage: [ ft.Row( [ - ft.Text("Archive", size=24, weight=ft.FontWeight.BOLD), + ft.Text("Archive Orders Out", 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 @@ -122,4 +134,30 @@ class ArchivePage: self.orders_list ] ) + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Archive Orders Out", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) \ No newline at end of file diff --git a/transportmanager/client/pages/clients_page.py b/transportmanager/client/pages/clients_page.py index 351e77d..5400441 100644 --- a/transportmanager/client/pages/clients_page.py +++ b/transportmanager/client/pages/clients_page.py @@ -265,6 +265,18 @@ class ClientsPage: except Exception as e: print("Error loading subscription:", e) + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + id = self.page.session.get("user_id") + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return True if response.json()['clients'] == 1 else False + def build(self): self.load_clients() self.subscription = self.get_current_subscription_plan() @@ -291,4 +303,30 @@ class ClientsPage: expand=True ), expand=True, + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Clients", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) \ No newline at end of file diff --git a/transportmanager/client/pages/destinations_page.py b/transportmanager/client/pages/destinations_page.py index 4292a3c..5ddb5e3 100644 --- a/transportmanager/client/pages/destinations_page.py +++ b/transportmanager/client/pages/destinations_page.py @@ -208,6 +208,18 @@ class DestinationsPage: except Exception as e: print("Error loading subscription:", e) + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + id = self.page.session.get("user_id") + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return True if response.json()['destinations'] == 1 else False + def build(self): self.refresh() self.add_destination_btn = ft.ElevatedButton("Add Destination", icon=ft.Icons.ADD, on_click=lambda e: self.open_dialog()) @@ -230,4 +242,30 @@ class DestinationsPage: self.destinations_column ], expand=True, + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Adderess", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) \ No newline at end of file diff --git a/transportmanager/client/pages/orders_edit_page.py b/transportmanager/client/pages/orders_edit_page.py index aa79cc8..2ed1c97 100644 --- a/transportmanager/client/pages/orders_edit_page.py +++ b/transportmanager/client/pages/orders_edit_page.py @@ -1196,7 +1196,7 @@ class OrdersEditPage: [ self.loading_date, ft.ElevatedButton( - "Pick date", + "Select date", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.DatePicker( @@ -1213,7 +1213,7 @@ class OrdersEditPage: [ self.loading_hour, ft.ElevatedButton( - "Pick hour - Start", + "Select hour - Start", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1226,7 +1226,7 @@ class OrdersEditPage: ), ), ft.ElevatedButton( - "Pick hour - End", + "Select hour - End", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1289,7 +1289,7 @@ class OrdersEditPage: [ self.unloading_date, ft.ElevatedButton( - "Pick date", + "Select date", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.DatePicker( @@ -1306,7 +1306,7 @@ class OrdersEditPage: [ self.unloading_hour, ft.ElevatedButton( - "Pick hour - Start", + "Select hour - Start", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1319,7 +1319,7 @@ class OrdersEditPage: ), ), ft.ElevatedButton( - "Pick hour - End", + "Select hour - End", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( diff --git a/transportmanager/client/pages/orders_in_page.py b/transportmanager/client/pages/orders_in_page.py index c4e0e8d..70a85d0 100644 --- a/transportmanager/client/pages/orders_in_page.py +++ b/transportmanager/client/pages/orders_in_page.py @@ -129,6 +129,7 @@ class OrdersInPage: ) self.ldm_quantity = ft.TextField( + value='13.6', expand=True, keyboard_type=ft.KeyboardType.NUMBER, input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") @@ -159,7 +160,12 @@ class OrdersInPage: ) self.received_price = ft.TextField( - label="Price Received - visible only to you!", + label="Price Received", + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") + ) + self.expenses = ft.TextField( + label="Expenses", keyboard_type=ft.KeyboardType.NUMBER, input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string="") ) @@ -193,6 +199,130 @@ class OrdersInPage: ), ) + self.file_picker = ft.FilePicker(on_result=self.on_file_result) + self.page.overlay.append(self.file_picker) + + # --- Persistent date & time pickers (must be on page.overlay) --- + self.loading_date_picker = 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, + ) + self.unloading_date_picker = 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, + ) + self.loading_time_picker_start = 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, + ) + self.loading_time_picker_end = 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, + ) + self.unloading_time_picker_start = 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, + ) + self.unloading_time_picker_end = 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, + ) + # Attach to overlay + self.page.overlay.extend([ + self.loading_date_picker, + self.unloading_date_picker, + self.loading_time_picker_start, + self.loading_time_picker_end, + self.unloading_time_picker_start, + self.unloading_time_picker_end, + ]) + #self.page.update() + + self.upload_order_btn = ft.Button( + "Upload", + icon=ft.Icons.UPLOAD_FILE, + width=150, + on_click=self.on_upload_document_btn_click + ) + + self.filename = ft.Text() + + def _open_date_picker(self, picker): + # Works on both newer and older Flet + if hasattr(picker, "pick_date"): + picker.pick_date() + else: + picker.open = True + self.page.update() + + def _open_time_picker(self, picker): + # Works on both newer and older Flet + if hasattr(picker, "pick_time"): + picker.pick_time() + else: + picker.open = True + self.page.update() + + + + def on_upload_document_btn_click(self, e): + # Ensure FilePicker is attached to the page (Portainer/Flet sometimes reuses page objects) + if self.file_picker not in getattr(self.page.overlay, "controls", []): + self.page.overlay.append(self.file_picker) + self.page.update() + self.file_picker.pick_files( + allow_multiple=False, + allowed_extensions=["png", "jpg", "jpeg", "pdf"] + ) + + def on_file_result(self, e: ft.FilePickerResultEvent): + if not e.files: + return + file = e.files[0] + # Build a safe filename: order_in_YYYYmmdd_HHMMSS_userid_originalname + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + user_id = self.page.session.get("user_id") + new_filename = f"order_in_{timestamp}_{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)]) + self.filename.value = new_filename + self.filename.update() + + #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 on_location_btn_click(self, destination): query = destination["address"].replace(", ", "+") maps_url = f"https://www.google.com/maps/search/?api=1&query={query}" @@ -266,7 +396,7 @@ class OrdersInPage: #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: + if not self.loading_hour.value: self.loading_hour.value = str(e.control.value) else: self.loading_hour.value += f' - {e.control.value}' @@ -322,10 +452,10 @@ class OrdersInPage: 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_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() @@ -391,7 +521,7 @@ class OrdersInPage: 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: + if not self.unloading_hour.value: self.unloading_hour.value = str(e.control.value) else: self.unloading_hour.value += f' - {e.control.value}' @@ -440,10 +570,10 @@ class OrdersInPage: 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_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() @@ -684,7 +814,9 @@ class OrdersInPage: 'trailer_reg_number': self.trailer_reg_number.value, 'received_price': self.received_price.value, 'loading_addresses': loading_addresses, - 'unloading_addresses': unloading_addresses + 'unloading_addresses': unloading_addresses, + 'file':self.filename.value, + 'expenses': self.expenses.value, } #print(saved_data) if self.order_number.value == None or len(self.order_number.value)==0: @@ -762,6 +894,18 @@ class OrdersInPage: except Exception as ex: self.error_message.value = f"Error: {str(ex)}" self.error_message.update() + + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + id = self.page.session.get("user_id") + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return True if response.json()['orders_in'] == 1 else False def build(self): self.save_btn = ft.FilledButton( @@ -785,7 +929,9 @@ class OrdersInPage: ft.Row( [ ft.Text("Number", size=18, weight=ft.FontWeight.BOLD), - self.order_number + self.order_number, + self.upload_order_btn, + self.filename ] ) ], @@ -882,11 +1028,12 @@ class OrdersInPage: [ ft.Row( [ - ft.Text("Price", size=18, weight=ft.FontWeight.BOLD) + ft.Text("Price / Expenses", size=18, weight=ft.FontWeight.BOLD) ], alignment=ft.MainAxisAlignment.START ), self.received_price, + self.expenses ], expand=2.5 ) @@ -926,15 +1073,9 @@ class OrdersInPage: [ self.loading_date, ft.ElevatedButton( - "Pick date", + "Select 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, - ) - ), + on_click=lambda e: self._open_date_picker(self.loading_date_picker), ) ], expand=True @@ -943,30 +1084,14 @@ class OrdersInPage: [ self.loading_hour, ft.ElevatedButton( - "Pick hour - Start", + "Select 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 - ) - ), + on_click=lambda e: self._open_time_picker(self.loading_time_picker_start), ), ft.ElevatedButton( - "Pick hour - End", + "Select 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 - ) - ), + on_click=lambda e: self._open_time_picker(self.loading_time_picker_end), ), ft.ElevatedButton( "Reset", @@ -1019,15 +1144,9 @@ class OrdersInPage: [ self.unloading_date, ft.ElevatedButton( - "Pick date", + "Select 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, - ) - ), + on_click=lambda e: self._open_date_picker(self.unloading_date_picker), ) ], expand=True @@ -1036,30 +1155,14 @@ class OrdersInPage: [ self.unloading_hour, ft.ElevatedButton( - "Pick hour - Start", + "Select 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 - ) - ), + on_click=lambda e: self._open_time_picker(self.unloading_time_picker_start), ), ft.ElevatedButton( - "Pick hour - End", + "Select 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 - ) - ), + on_click=lambda e: self._open_time_picker(self.unloading_time_picker_end), ), ft.ElevatedButton( "Reset", @@ -1101,5 +1204,31 @@ class OrdersInPage: scroll=ft.ScrollMode.ADAPTIVE, spacing=20 ) + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Create Order In", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) \ No newline at end of file diff --git a/transportmanager/client/pages/orders_out_page.py b/transportmanager/client/pages/orders_out_page.py index b262d8d..2a9a103 100644 --- a/transportmanager/client/pages/orders_out_page.py +++ b/transportmanager/client/pages/orders_out_page.py @@ -156,7 +156,8 @@ class OrdersOutPage: 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="") + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value=13.6 ) self.kg_quantity = ft.TextField( @@ -402,10 +403,10 @@ class OrdersOutPage: 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_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() @@ -520,10 +521,10 @@ class OrdersOutPage: 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_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() @@ -867,6 +868,18 @@ class OrdersOutPage: except Exception as e: print("Error loading subscription:", e) return None + + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + id = self.page.session.get("user_id") + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return True if response.json()['orders_out'] == 1 else False def build(self): self.save_btn = ft.FilledButton( @@ -1050,7 +1063,7 @@ class OrdersOutPage: [ self.loading_date, ft.ElevatedButton( - "Pick date", + "Select date", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.DatePicker( @@ -1067,7 +1080,7 @@ class OrdersOutPage: [ self.loading_hour, ft.ElevatedButton( - "Pick hour - Start", + "Select hour - Start", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1080,7 +1093,7 @@ class OrdersOutPage: ), ), ft.ElevatedButton( - "Pick hour - End", + "Select hour - End", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1143,7 +1156,7 @@ class OrdersOutPage: [ self.unloading_date, ft.ElevatedButton( - "Pick date", + "Select date", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.DatePicker( @@ -1160,7 +1173,7 @@ class OrdersOutPage: [ self.unloading_hour, ft.ElevatedButton( - "Pick hour - Start", + "Select hour - Start", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1173,7 +1186,7 @@ class OrdersOutPage: ), ), ft.ElevatedButton( - "Pick hour - End", + "Select hour - End", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1225,5 +1238,31 @@ class OrdersOutPage: scroll=ft.ScrollMode.ADAPTIVE, spacing=20 ) + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Create Order Out", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) \ No newline at end of file diff --git a/transportmanager/client/pages/orders_page.py b/transportmanager/client/pages/orders_page.py index 376b673..c840112 100644 --- a/transportmanager/client/pages/orders_page.py +++ b/transportmanager/client/pages/orders_page.py @@ -1,6 +1,8 @@ import flet as ft from pages.orders_in_page import OrdersInPage from pages.orders_out_page import OrdersOutPage +from pages.archive_in_page import ArchiveInPage +from pages.archive_page import ArchivePage class OrdersPage: def __init__(self, page: ft.Page, dashboard): @@ -17,6 +19,16 @@ class OrdersPage: self.dashboard.placeholder.content = orders_out_page.build() self.dashboard.placeholder.update() + def on_archive_order_out_btn_click(self, e): + archive = ArchivePage(self.page, self.dashboard, self) + self.dashboard.placeholder.content = archive.build() + self.dashboard.placeholder.update() + + def on_archive_order_in_btn_click(self, e): + archive = ArchiveInPage(self.page, self.dashboard, self) + self.dashboard.placeholder.content = archive.build() + self.dashboard.placeholder.update() + def build(self): return ft.Container( content=ft.Column( @@ -31,7 +43,7 @@ class OrdersPage: ft.Container( ft.Row( [ - ft.Text("Incoming orders", size=20) + ft.Text("Orders In", size=20) ], alignment=ft.MainAxisAlignment.CENTER ), @@ -39,11 +51,22 @@ class OrdersPage: width=250, height=80 ), - ft.FilledButton( - "Orders In", - on_click=self.on_orders_in_btn_click, - width=150 + ft.Row( + [ + ft.FilledButton( + "Create", + on_click=self.on_orders_in_btn_click, + width=100 + ), + ft.FilledButton( + "Archive", + on_click=self.on_archive_order_in_btn_click, + width=100 + ) + ], + alignment=ft.MainAxisAlignment.SPACE_EVENLY ) + ], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER @@ -62,7 +85,7 @@ class OrdersPage: ft.Container( ft.Row( [ - ft.Text("Outcoming orders", size=20) + ft.Text("Orders Out", size=20) ], alignment=ft.MainAxisAlignment.CENTER ), @@ -70,10 +93,20 @@ class OrdersPage: width=250, height=80 ), - ft.FilledButton( - "Orders Out", - on_click=self.on_orders_out_btn_click, - width=150 + ft.Row( + [ + ft.FilledButton( + "Create", + on_click=self.on_orders_out_btn_click, + width=100 + ), + ft.FilledButton( + "Archive", + on_click=self.on_archive_order_out_btn_click, + width=100 + ) + ], + alignment=ft.MainAxisAlignment.SPACE_EVENLY ) ], alignment=ft.MainAxisAlignment.CENTER, diff --git a/transportmanager/client/pages/profile_page.py b/transportmanager/client/pages/profile_page.py index 5c3dd2d..594522e 100644 --- a/transportmanager/client/pages/profile_page.py +++ b/transportmanager/client/pages/profile_page.py @@ -2,6 +2,7 @@ import flet as ft import requests import time from config import API_BASE_URL +from pages.users_page import Users class ProfilePage: def __init__(self, page: ft.Page, dashboard): @@ -138,9 +139,9 @@ class ProfilePage: ), ) 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.edit_button = ft.ElevatedButton(text="Edit Profile", on_click=self.on_edit_click, width=100) + self.save_button = ft.ElevatedButton(text="Save Changes", visible=False, on_click=self.on_save_click, width=130) + self.cancel_button = ft.ElevatedButton(text="Cancel", visible=False, on_click=self.on_cancel_click, width=100) 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) @@ -177,6 +178,18 @@ class ProfilePage: ), ) + self.add_users = ft.Button( + "Add Users", + icon=ft.Icons.MANAGE_ACCOUNTS_ROUNDED, + width=250, + on_click=self.on_add_users_btn_click + ) + + def on_add_users_btn_click(self, e): + users = Users(self.page, self.dashboard) + self.dashboard.placeholder.content = users.build() + self.dashboard.placeholder.update() + def _auth_headers(self): """Build Authorization header from client storage robustly (web/desktop).""" t = self.page.client_storage.get("token") @@ -408,12 +421,22 @@ class ProfilePage: self.message.color = ft.Colors.RED self.page.update() + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + return False + def build(self): self.populate_user_data() return ft.Container( content=ft.Column( [ - ft.Text("User Profile", size=24, weight=ft.FontWeight.BOLD), + ft.Text("Company Profile", size=24, weight=ft.FontWeight.BOLD), ft.Row( [ ft.Column( @@ -425,7 +448,10 @@ class ProfilePage: self.smtp_user, self.smtp_host, self.smtp_port, - ft.Row([self.edit_button, self.save_button, self.cancel_button]) + ft.Text(), + ft.Row([self.edit_button, self.save_button, self.cancel_button]), + ft.Divider(), + self.add_users,ft.Text() ], horizontal_alignment=ft.CrossAxisAlignment.CENTER, width=250 @@ -455,4 +481,30 @@ class ProfilePage: ], scroll=ft.ScrollMode.ADAPTIVE ), + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Company Profile", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) \ No newline at end of file diff --git a/transportmanager/client/pages/report_page.py b/transportmanager/client/pages/report_page.py index 6b71065..fa2d583 100644 --- a/transportmanager/client/pages/report_page.py +++ b/transportmanager/client/pages/report_page.py @@ -326,6 +326,18 @@ class ReportPage: self.total.value = f"Total: {total}" self.total.update() + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + id = self.page.session.get("user_id") + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return True if response.json()['report'] == 1 else False + def build(self): return ft.Container( content=ft.Column( @@ -386,4 +398,30 @@ class ReportPage: alignment=ft.MainAxisAlignment.START, ), expand=True + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Reports", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + 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 index 4dde52d..a06af5d 100644 --- a/transportmanager/client/pages/subscription_page.py +++ b/transportmanager/client/pages/subscription_page.py @@ -93,6 +93,16 @@ class Subscription: self.current_subscription_status.value = self.status[status] self.current_subscription_status.update() + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + return False + def build(self): return ft.Container( content=ft.Column( @@ -244,4 +254,30 @@ class Subscription: spacing=50 ), expand=True + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Subscription", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) \ No newline at end of file diff --git a/transportmanager/client/pages/temporary_password_page.py b/transportmanager/client/pages/temporary_password_page.py new file mode 100644 index 0000000..031a430 --- /dev/null +++ b/transportmanager/client/pages/temporary_password_page.py @@ -0,0 +1,60 @@ +import flet as ft +import requests +import time +from config import API_BASE_URL +import re +from pages.dashboard_page import DashboardPage + +class TemporaryPassword: + def __init__(self, page: ft.Page): + self.page = page + 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.message = ft.Text("", color=ft.Colors.RED, text_align=ft.TextAlign.CENTER) + + def on_save_btn_click(self, e): + # 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.message.update() + return + # Placeholder for register logic + if self.password.value != self.confirm_password.value: + self.message.value = "Passwords do not match" + self.message.update() + return + try: + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{API_BASE_URL}/auth/temporary_password", headers=headers, json={ + "password": self.password.value + }) + if response.status_code == 200: + self.message.color = ft.Colors.GREEN + self.message.value = "Password updated!\nYou will be redirected to Dashboard" + self.message.update() + time.sleep(3) + # Proceed to Dashboard + self.page.go("/dashboard") + else: + self.message.color = ft.Colors.RED + self.message.value = response.json().get("message", "Password update fail!") + self.message.update() + except Exception as e: + print(e) + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Text("This is your first login,\nplease change your password!", weight=ft.FontWeight.BOLD, size=20, text_align=ft.TextAlign.CENTER), + self.password, + self.confirm_password, + self.message, + ft.Button("Save", width=150, on_click=self.on_save_btn_click) + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + alignment=ft.MainAxisAlignment.CENTER, + spacing=20, + ) + ) \ No newline at end of file diff --git a/transportmanager/client/pages/transporters_page.py b/transportmanager/client/pages/transporters_page.py index 610b577..444d204 100644 --- a/transportmanager/client/pages/transporters_page.py +++ b/transportmanager/client/pages/transporters_page.py @@ -245,6 +245,18 @@ class TransportersPage: except Exception as e: print("Error loading subscription:", e) + def get_client_access(self): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/profile/", headers=headers, timeout=10) + user = response.json() + if user['user_role'] == 'user': + return True + else: + id = self.page.session.get("user_id") + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return True if response.json()['transporters'] == 1 else False + def build(self): self.transporter_list = ft.Column(spacing=10, expand=True, scroll=ft.ScrollMode.ADAPTIVE,) self.refresh() @@ -270,4 +282,30 @@ class TransportersPage: ], alignment=ft.MainAxisAlignment.START, + ) if self.get_client_access() else ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Transporters", size=24, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + "You do not have access to this page content", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.Colors.RED + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text("") + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ), + expand=True ) diff --git a/transportmanager/client/pages/two_factor_page.py b/transportmanager/client/pages/two_factor_page.py index e789998..aabdbf5 100644 --- a/transportmanager/client/pages/two_factor_page.py +++ b/transportmanager/client/pages/two_factor_page.py @@ -1,6 +1,7 @@ import flet as ft import requests from config import API_BASE_URL +from pages.temporary_password_page import TemporaryPassword class TwoFactorAuth: def __init__(self, page: ft.Page, email: str, login, auth): @@ -35,10 +36,30 @@ class TwoFactorAuth: 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 + ui = requests.get( + f"{API_BASE_URL}/auth/me", + headers={"Authorization": f"Bearer {token}"}, + timeout=10, + ) + + if ui.status_code == 200: + uj = ui.json() + try: + logo_filename = uj.get("logo_filename", "") + if logo_filename: + self.page.client_storage.set("custom_logo", logo_filename) + except Exception: + pass + if uj['temporary_password'] == 1: + self.auth.placeholder.content.clean() + temp_passwd_page = TemporaryPassword(self.page) + self.auth.placeholder.content = temp_passwd_page.build() + self.auth.placeholder.update() + else: + self.success_text.value = "Verification successful. You are now logged in." + self.error_text.value = "" + self.page.update() + self.page.go("/dashboard") else: self.error_text.value = "Invalid or expired code." self.success_text.value = "" diff --git a/transportmanager/client/pages/users_page.py b/transportmanager/client/pages/users_page.py new file mode 100644 index 0000000..39be647 --- /dev/null +++ b/transportmanager/client/pages/users_page.py @@ -0,0 +1,274 @@ +import flet as ft +import requests +from config import API_BASE_URL + +class Users: + def __init__(self, page: ft.Page, dashboard): + self.page = page + self.dashboard = dashboard + self.users = self.get_users() + self.selected_user = None + self.users_list = ft.ListView( + controls=self.create_users_list(self.users, self.on_edit_btn_click, self.on_delete_btn_click), + spacing=10, + expand=True + ) + self.add_user_dialog = ft.AlertDialog( + title="Add Company User", + content=ft.Column( + [ + ft.TextField(label="Name"), + ft.TextField(label="Email"), + ft.TextField(label="Temporary Password", password=True, can_reveal_password=True), + ft.Text("Give user access to add and modify:"), + ft.Checkbox(label="Orders In"), + ft.Checkbox(label="Order Out"), + ft.Checkbox(label="Addresses"), + ft.Checkbox(label="Clients"), + ft.Checkbox(label="Transporters"), + ft.Checkbox(label="Report"), + ft.Text("", color=ft.Colors.RED) + ], + width=600, + height=500 + ), + actions=[ + ft.TextButton( + "Cancel", + width=100, + on_click=self.on_cancel_btn_click + ), + ft.ElevatedButton( + "Save", + width=100, + on_click=self.on_save_btn_click + ) + ] + ) + + self.delete_dialog = ft.AlertDialog( + title=ft.Text("Delete User?"), + actions=[ + ft.TextButton("Cancel", on_click=self.on_cancel_delete_btn_click, width=100), + ft.Button("Yes", on_click=self.on_confirm_delete_btn_click, width=100), + ] + ) + + self.edit_user_dialog = ft.AlertDialog( + title=ft.Text(f"Edit User"), + content=ft.Column( + [ + ft.Checkbox(label="Orders In"), + ft.Checkbox(label="Order Out"), + ft.Checkbox(label="Addresses"), + ft.Checkbox(label="Clients"), + ft.Checkbox(label="Transporters"), + ft.Checkbox(label="Report"), + ], + width=400, + height=350 + ), + actions=[ + ft.TextButton( + "Cancel", + width=100, + on_click=self.on_edit_cancel_btn_click + ), + ft.ElevatedButton( + "Save", + width=100, + on_click=self.on_edit_save_btn_click + ) + ] + ) + + def on_edit_cancel_btn_click(self, e): + self.page.close(self.edit_user_dialog) + + def on_edit_save_btn_click(self, e): + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + access_payload = { + 'company_user_id':self.selected_user['id'], + 'orders_in':1 if self.edit_user_dialog.content.controls[0].value else 0, + 'orders_out':1 if self.edit_user_dialog.content.controls[1].value else 0, + 'addresses':1 if self.edit_user_dialog.content.controls[2].value else 0, + 'clients':1 if self.edit_user_dialog.content.controls[3].value else 0, + 'transporters':1 if self.edit_user_dialog.content.controls[4].value else 0, + 'report':1 if self.edit_user_dialog.content.controls[5].value else 0 + } + response = requests.put(f"{API_BASE_URL}/company_user/update_access", headers=headers, json=access_payload) + + #set to default + self.selected_user = None + self.page.close(self.edit_user_dialog) + self.edit_user_dialog.content.controls[0].value = False + self.edit_user_dialog.content.controls[1].value = False + self.edit_user_dialog.content.controls[2].value = False + self.edit_user_dialog.content.controls[3].value = False + self.edit_user_dialog.content.controls[4].value = False + self.edit_user_dialog.content.controls[5].value = False + self.edit_user_dialog.update() + + def on_confirm_delete_btn_click(self, e): + self.page.close(self.delete_dialog) + try: + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + delete_payload = { + 'user_id':self.selected_user['id'] + } + response = requests.post(f"{API_BASE_URL}/admin/users/deactivate", headers=headers, json=delete_payload) + except Exception as e: + print("Error deleting company users:", e) + users = self.get_users() + self.users_list.controls.clear() + self.users_list.controls = self.create_users_list(users, self.on_edit_btn_click, self.on_delete_btn_click) + self.users_list.update() + self.selected_user = None + + def on_cancel_delete_btn_click(self, e): + self.page.close(self.delete_dialog) + + def add_new_user(self, e): + self.page.open(self.add_user_dialog) + + def on_edit_btn_click(self, item): + self.selected_user = item + access = self.get_user_access(item['id']) + self.edit_user_dialog.content.controls[0].value = True if access['orders_in']==1 else False + self.edit_user_dialog.content.controls[1].value = True if access['orders_out']==1 else False + self.edit_user_dialog.content.controls[2].value = True if access['destinations']==1 else False + self.edit_user_dialog.content.controls[3].value = True if access['clients']==1 else False + self.edit_user_dialog.content.controls[4].value = True if access['transporters']==1 else False + self.edit_user_dialog.content.controls[5].value = True if access['report']==1 else False + self.page.open(self.edit_user_dialog) + + def get_user_access(self, id): + try: + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/company_user/access/{id}", headers=headers) + return response.json() + except Exception as e: + print("Error loading company user access:", e) + + def on_delete_btn_click(self, item): + self.selected_user = item + self.page.open(self.delete_dialog) + + def on_save_btn_click(self, e): + self.page.close(self.add_user_dialog) + try: + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + create_payload = { + 'name':self.add_user_dialog.content.controls[0].value, + 'email':self.add_user_dialog.content.controls[1].value, + 'company_id':self.page.session.get("user_id"), + 'password':self.add_user_dialog.content.controls[2].value + } + response = requests.post(f"{API_BASE_URL}/company_user/register_company_user", headers=headers, json=create_payload) + company_user_id = response.json()['company_user_id'] if response.status_code == 201 else self.show_error_mesage + if company_user_id: + access_payload = { + 'company_user_id':company_user_id, + 'orders_in':1 if self.add_user_dialog.content.controls[4].value else 0, + 'orders_out':1 if self.add_user_dialog.content.controls[5].value else 0, + 'addresses':1 if self.add_user_dialog.content.controls[6].value else 0, + 'clients':1 if self.add_user_dialog.content.controls[7].value else 0, + 'transporters':1 if self.add_user_dialog.content.controls[8].value else 0, + 'report':1 if self.add_user_dialog.content.controls[9].value else 0 + } + response = requests.post(f"{API_BASE_URL}/company_user/access", headers=headers, json=access_payload) + except Exception as e: + print("Error loading company users:", e) + + users = self.get_users() + self.users_list.controls.clear() + self.users_list.controls = self.create_users_list(users, self.on_edit_btn_click, self.on_delete_btn_click) + self.users_list.update() + #set dialog to default valuse: + self.add_user_dialog.content.controls[0].value = '' + self.add_user_dialog.content.controls[1].value = '' + self.add_user_dialog.content.controls[2].value = '' + self.add_user_dialog.content.controls[4].value = False + self.add_user_dialog.content.controls[5].value = False + self.add_user_dialog.content.controls[6].value = False + self.add_user_dialog.content.controls[7].value = False + self.add_user_dialog.content.controls[8].value = False + self.add_user_dialog.content.controls[9].value = False + self.add_user_dialog.content.controls[10].value = "" + self.add_user_dialog.update() + + def show_error_mesage(self): + self.add_user_dialog.content.controls[10].value = "A user with this email has been already created, to reactivate it send a request at office@ordergo.eu" + self.add_user_dialog.update() + + def on_cancel_btn_click(self, e): + self.page.close(self.add_user_dialog) + + def get_users(self): + try: + user_id = self.page.session.get("user_id") + users = [] + token = self.page.client_storage.get("token") + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{API_BASE_URL}/admin/users/company_users", headers=headers) + if response.status_code == 200: + all_users=response.json() + for user in all_users: + if user['company_id'] == user_id and user['active'] == 1: + users.append(user) + return users + else: + return [] + except Exception as e: + print("Error loading company users:", e) + + def create_users_list(self, items, on_click_handler, on_click_handler2): + return [ + ft.Container( + content=ft.Row( + [ + ft.Column( + [ + ft.Text(item['name'], expand=True, weight=ft.FontWeight.BOLD), + ft.Text(item['email'], size=12) + ] + ), + ft.Row( + [ + ft.Button("Edit", on_click=lambda e, id=item: on_click_handler(id), width=100), + ft.FilledButton("Delete", on_click=lambda e, id=item: on_click_handler2(id), width=100, bgcolor=ft.Colors.RED) + ] + ) + ], + 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), + ) + for item in items + ] + + def build(self): + return ft.Container( + content= ft.Column( + [ + ft.Row( + [ + ft.Text("Users", size=24, weight=ft.FontWeight.BOLD), + ft.Button("Add User", icon=ft.Icons.ADD, on_click=self.add_new_user) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + self.users_list + ], + expand=True + ), + expand=True + ) \ 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 index 5d6ba7d..991d86c 100644 --- a/transportmanager/client/pages/view_orders_in_page.py +++ b/transportmanager/client/pages/view_orders_in_page.py @@ -188,6 +188,12 @@ class ViewOrdersIn: input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), value=self.order['received_price'] ) + self.expenses = ft.TextField( + label="Expenses", + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=ft.InputFilter(allow=True, regex_string=r"^[0-9]*\.?[0-9]*$", replacement_string=""), + value=self.order['expenses'] + ) self.error_message = ft.Text(color = ft.Colors.RED) @@ -280,6 +286,18 @@ class ViewOrdersIn: self.unloading_query = addresses self.unloading.controls = self.create_unloading_list(addresses, self.on_delete_unloading_address_btn_click) + self.file_picker = ft.FilePicker(on_result=self.on_file_result) + self.page.overlay.append(self.file_picker) + + self.upload_order_btn = ft.Button( + "Upload", + icon=ft.Icons.UPLOAD_FILE, + width=150, + on_click=self.on_upload_document_btn_click + ) + + self.filename = ft.Text(value=self.order['file']) + def on_go_back_btn_click(self, e): self.dashboard.placeholder.content = self.archive.build() self.dashboard.placeholder.update() @@ -447,10 +465,10 @@ class ViewOrdersIn: 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_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() @@ -480,6 +498,29 @@ class ViewOrdersIn: self.loading_error_message.value = "All fields of the loading address are required." self.loading_error_message.update() + def on_upload_document_btn_click(self, e): + # Ensure FilePicker is attached to the page (Portainer/Flet sometimes reuses page objects) + if self.file_picker not in getattr(self.page.overlay, "controls", []): + self.page.overlay.append(self.file_picker) + self.page.update() + self.file_picker.pick_files( + allow_multiple=False, + allowed_extensions=["png", "jpg", "jpeg", "pdf"] + ) + + def on_file_result(self, e: ft.FilePickerResultEvent): + if not e.files: + return + file = e.files[0] + # Build a safe filename: order_in_YYYYmmdd_HHMMSS_userid_originalname + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + user_id = self.page.session.get("user_id") + new_filename = f"order_in_{timestamp}_{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)]) + self.filename.value = new_filename + self.filename.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()] @@ -565,10 +606,10 @@ class ViewOrdersIn: 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_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() @@ -782,7 +823,9 @@ class ViewOrdersIn: 'trailer_reg_number': self.trailer_reg_number.value, 'received_price': self.received_price.value, 'loading_addresses': loading_addresses, - 'unloading_addresses': unloading_addresses + 'unloading_addresses': unloading_addresses, + 'file':self.filename.value, + 'expenses': self.expenses.value, } #print(saved_data) if self.order_number.value == None or len(self.order_number.value)==0: @@ -904,7 +947,9 @@ class ViewOrdersIn: ft.Row( [ ft.Text("Number", size=18, weight=ft.FontWeight.BOLD), - self.order_number + self.order_number, + self.upload_order_btn, + self.filename ] ), ft.Row( @@ -998,6 +1043,7 @@ class ViewOrdersIn: alignment=ft.MainAxisAlignment.START ), self.received_price, + self.expenses, ], expand=2.5 ) @@ -1037,7 +1083,7 @@ class ViewOrdersIn: [ self.loading_date, ft.ElevatedButton( - "Pick date", + "Select date", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.DatePicker( @@ -1054,7 +1100,7 @@ class ViewOrdersIn: [ self.loading_hour, ft.ElevatedButton( - "Pick hour - Start", + "Select hour - Start", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1067,7 +1113,7 @@ class ViewOrdersIn: ), ), ft.ElevatedButton( - "Pick hour - End", + "Select hour - End", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1130,7 +1176,7 @@ class ViewOrdersIn: [ self.unloading_date, ft.ElevatedButton( - "Pick date", + "Select date", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.DatePicker( @@ -1147,7 +1193,7 @@ class ViewOrdersIn: [ self.unloading_hour, ft.ElevatedButton( - "Pick hour - Start", + "Select hour - Start", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( @@ -1160,7 +1206,7 @@ class ViewOrdersIn: ), ), ft.ElevatedButton( - "Pick hour - End", + "Select hour - End", icon=ft.Icons.CALENDAR_MONTH, on_click=lambda e: self.page.open( ft.TimePicker( diff --git a/transportmanager/server/admin/tenants.py b/transportmanager/server/admin/tenants.py index 9de7a06..0f454c1 100644 --- a/transportmanager/server/admin/tenants.py +++ b/transportmanager/server/admin/tenants.py @@ -5,13 +5,21 @@ 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"]) +@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 all users with role "user" +@admin_user_bp.route("/company_users", methods=["GET"]) +@jwt_required() +def get_all_company_users(): + users_model = Users() + users = users_model.get_all_users_with_role("company_user") + return jsonify(users), 200 + # Get a single user by ID @admin_user_bp.route("/", methods=["GET"]) @jwt_required() @@ -38,3 +46,21 @@ def update_user(): users_model.update_user(data) return jsonify({"message": "User updated successfully."}), 200 + +# Deactivate +@admin_user_bp.route("/deactivate", methods=["POST"]) +@jwt_required() +def deactivate_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() + user_id = data.get("user_id") + if not user_id: + return jsonify({"error": "Missing user_id"}), 400 + + users_model = Users() + users_model.deactivate_user(user_id) + + return jsonify({"message": "User updated successfully."}), 200 \ No newline at end of file diff --git a/transportmanager/server/app.py b/transportmanager/server/app.py index b1ab84f..3c6ca11 100644 --- a/transportmanager/server/app.py +++ b/transportmanager/server/app.py @@ -12,6 +12,7 @@ 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 routes.company_user import company_user_bp from apscheduler.schedulers.background import BackgroundScheduler from models.subscription import Subscription @@ -69,7 +70,7 @@ 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) - +app.register_blueprint(company_user_bp) def update_subscription_statuses_job(): print("[Scheduler] Running daily subscription status check...") diff --git a/transportmanager/server/assets/Manual.pdf b/transportmanager/server/assets/Manual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2a29a84c140a9fc0155b252cee82947ee55540f5 GIT binary patch literal 15441 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 XaD;^SneP@!ul;Ie74fdz|rOw{&*vc%oJ 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', methods=["GET"]) +@jwt_required() +def get_access(id): + company_user_id = id + if not company_user_id: + return jsonify({"error":"Missing required fields"}), 400 + + company_users = CompanyUsers() + access = company_users.get_access_by_company_user_id(company_user_id) + return jsonify(access) + +@company_user_bp.route("/register_company_user", methods=["POST"]) +@jwt_required() +def register_company_user(): + users = Users() + data = request.get_json() + name = data.get("name") + email = data.get("email") + company_id = data.get("company_id") + password = data.get("password") + + if not name or not email or not password or not company_id: + 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) + company_user_id = users.insert_company_user(name, email, password_hash, company_id) + + welcome_message = WelcomeMessage(name, email) + welcome_message.send_email() + + return jsonify({"message": "User registered successfully!", "company_user_id":company_user_id}), 201 + +@company_user_bp.route('/access', methods=["POST"]) +@jwt_required() +def create_access(): + data = request.get_json() + company_user_id = data.get('company_user_id') + orders_in = data.get("orders_in") + orders_out = data.get("orders_out") + addresses = data.get("addresses") + clients = data.get("clients") + transporters = data.get("transporters") + report = data.get("report") + company_users = CompanyUsers() + # Use explicit None checks so that False/0 values are allowed + if ( + company_user_id is None or + orders_in is None or + orders_out is None or + addresses is None or + clients is None or + transporters is None or + report is None + ): + return jsonify({"error": "Missing required fields"}), 400 + + access_data = { + 'company_user_id': company_user_id, + 'clients': clients, + 'transporters': transporters, + 'destinations': addresses, + 'orders_in': orders_in, + 'orders_out': orders_out, + 'report': report + } + company_users.insert_company_user_access(access_data) + + return jsonify({"message": "Company User access inserted!"}) + +@company_user_bp.route('/update_access', methods=["PUT"]) +@jwt_required() +def update_access(): + data = request.get_json() + company_user_id = data.get('company_user_id') + orders_in = data.get("orders_in") + orders_out = data.get("orders_out") + addresses = data.get("addresses") + clients = data.get("clients") + transporters = data.get("transporters") + report = data.get("report") + company_users = CompanyUsers() + # Use explicit None checks so that False/0 values are allowed + if ( + company_user_id is None or + orders_in is None or + orders_out is None or + addresses is None or + clients is None or + transporters is None or + report is None + ): + return jsonify({"error": "Missing required fields"}), 400 + + access_data = { + 'company_user_id': company_user_id, + 'clients': clients, + 'transporters': transporters, + 'destinations': addresses, + 'orders_in': orders_in, + 'orders_out': orders_out, + 'report': report + } + company_users.update_company_user_access(access_data) + + return jsonify({"message": "Company User access updated!"}) \ No newline at end of file diff --git a/transportmanager/server/routes/destinations.py b/transportmanager/server/routes/destinations.py index 512750e..f2291db 100644 --- a/transportmanager/server/routes/destinations.py +++ b/transportmanager/server/routes/destinations.py @@ -2,6 +2,7 @@ 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 +from models.user import Users destinations_bp = Blueprint("destinations", __name__, url_prefix="/destinations") @@ -10,6 +11,10 @@ destinations_bp = Blueprint("destinations", __name__, url_prefix="/destinations" def list_destinations(): destinations_db = Destinations() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] destinations = destinations_db.get_all_by_user(user_id) return jsonify([dict(d) for d in destinations]), 200 @@ -18,6 +23,10 @@ def list_destinations(): def create_destination(): destinations_db = Destinations() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] data = request.get_json() destination_id = destinations_db.create(user_id, data.get("name"), data.get("address")) # coordinates = AdressCoordinates(data.get("address")) @@ -33,6 +42,10 @@ def create_destination(): def update_destination(id): destinations_db = Destinations() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] data = request.get_json() destinations_db.update(id, user_id, data.get("name"), data.get("address")) coordinates = AdressCoordinates(data.get("address")) diff --git a/transportmanager/server/routes/orders_out.py b/transportmanager/server/routes/orders_out.py index a94c510..12f084f 100644 --- a/transportmanager/server/routes/orders_out.py +++ b/transportmanager/server/routes/orders_out.py @@ -17,6 +17,10 @@ orders_bp = Blueprint("orders", __name__, url_prefix="/orders") @jwt_required() def create_order_route(): user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] orders = OrdersOut() incoming_data = request.json #here we need to first implement the order pdf @@ -83,6 +87,10 @@ def update_order_route(order_id): orders = OrdersOut() data = request.json user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] order = orders.get_order_by_id(order_id) if not order: return jsonify({"error": "Order not found"}), 404 @@ -150,6 +158,10 @@ def update_order_route(order_id): def delete_order_route(order_id): orders = OrdersOut() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] order = orders.get_order_by_id(order_id) if not order: return jsonify({"error": "Order not found"}), 404 @@ -168,6 +180,10 @@ def delete_order_route(order_id): def list_orders(): orders = OrdersOut() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] try: user_orders = orders.get_orders_by_user(user_id) #result = [{"id": order["id"], "order_number": order["order_number"]} for order in user_orders] @@ -180,6 +196,10 @@ def list_orders(): def get_order(order_id): orders = OrdersOut() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] order = orders.get_order_by_id(order_id) points = orders.get_order_points_by_order(order['id']) loading_points = [] @@ -251,6 +271,10 @@ def cancel_order(order_id): orders = OrdersOut() order = orders.get_order_by_id(order_id) user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] pdf_name = f'order_{user_id}_{order['order_number']}.pdf' cancel_order_pdf(pdf_name) orders.cancel_order(order_id) diff --git a/transportmanager/server/routes/ouders_in.py b/transportmanager/server/routes/ouders_in.py index 9324845..859d204 100644 --- a/transportmanager/server/routes/ouders_in.py +++ b/transportmanager/server/routes/ouders_in.py @@ -1,9 +1,12 @@ -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify, abort 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 +import os +from flask import send_from_directory +import mimetypes orders_in_bp = Blueprint("orders_in", __name__, url_prefix="/orders_in") @@ -11,6 +14,10 @@ orders_in_bp = Blueprint("orders_in", __name__, url_prefix="/orders_in") @jwt_required() def create_order_in_route(): user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] orders = OrdersIn() incoming_data = request.json try: @@ -25,7 +32,10 @@ def create_order_in_route(): 'track_reg_number': incoming_data["track_reg_number"], 'trailer_reg_number': incoming_data["trailer_reg_number"], 'products_description': incoming_data["products_description"], + 'file': incoming_data['file'], + 'expenses': incoming_data['expenses'] } + #print(order_data) order_id = orders.create_order(order_data) for address in incoming_data["loading_addresses"]: @@ -60,6 +70,10 @@ def update_order_route(order_id): orders = OrdersIn() data = request.json user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] order = orders.get_order_by_id(order_id) if not order: return jsonify({"error": "Order in not found"}), 404 @@ -77,6 +91,8 @@ def update_order_route(order_id): "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"]), + 'file': data.get("file", order["file"]), + 'expenses': data.get("expenses", order["expenses"]), "user_id":user_id }) @@ -113,6 +129,10 @@ def update_order_route(order_id): def delete_order_route(order_id): orders = OrdersIn() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] order = orders.get_order_by_id(order_id) if not order: return jsonify({"error": "Order in not found"}), 404 @@ -131,6 +151,10 @@ def delete_order_route(order_id): def list_orders(): orders = OrdersIn() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] try: user_orders = orders.get_orders_by_user(user_id) #result = [{"id": order["id"], "order_number": order["order_number"]} for order in user_orders] @@ -143,6 +167,10 @@ def list_orders(): def get_order(order_id): orders = OrdersIn() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] order = orders.get_order_by_id(order_id) points = orders.get_order_points_by_order(order['id']) loading_points = [] @@ -160,4 +188,26 @@ def get_order(order_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 + return jsonify(order), 200 + +@orders_in_bp.route("/files/", methods=["GET"]) +#@jwt_required() +def serve_order_pdf(filename): + try: + # Directory containing uploaded client files + uploads_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "client", "uploads") + ) + # Security: prevent path traversal and ensure file exists + abs_file_path = os.path.abspath(os.path.join(uploads_dir, filename)) + if not abs_file_path.startswith(uploads_dir + os.sep): + abort(404) + if not os.path.isfile(abs_file_path): + abort(404) + + guessed_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" + # send_from_directory expects the directory and the filename relative to it + return send_from_directory(uploads_dir, filename, mimetype=guessed_type, as_attachment=False) + except Exception as e: + print(e) + return jsonify({"error": "File not found"}), 404 \ No newline at end of file diff --git a/transportmanager/server/routes/report.py b/transportmanager/server/routes/report.py index f445875..6d1a07b 100644 --- a/transportmanager/server/routes/report.py +++ b/transportmanager/server/routes/report.py @@ -2,6 +2,7 @@ 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 +from models.user import Users report_bp = Blueprint("report", __name__, url_prefix="/report") @@ -10,6 +11,10 @@ report_bp = Blueprint("report", __name__, url_prefix="/report") def get_profit_report(): try: user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] # Get filters from query params date_from = request.args.get("date_from") date_to = request.args.get("date_to") diff --git a/transportmanager/server/routes/subscription.py b/transportmanager/server/routes/subscription.py index f34d0fb..a0d5ce2 100644 --- a/transportmanager/server/routes/subscription.py +++ b/transportmanager/server/routes/subscription.py @@ -11,6 +11,10 @@ subscription_bp = Blueprint("subscription", __name__, url_prefix="/subscription" @jwt_required() def get_subscription(): user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] subscription_model = Subscription() subscriptions = subscription_model.get_by_user_id(user_id) return jsonify(subscriptions), 200 diff --git a/transportmanager/server/routes/transporters.py b/transportmanager/server/routes/transporters.py index 97993db..e22a434 100644 --- a/transportmanager/server/routes/transporters.py +++ b/transportmanager/server/routes/transporters.py @@ -1,6 +1,7 @@ from flask import Blueprint, request, jsonify from flask_jwt_extended import jwt_required, get_jwt_identity from models.transporters import Transporters +from models.user import Users transporters_bp = Blueprint("transporters", __name__, url_prefix="/transporters") @@ -8,6 +9,10 @@ transporters_bp = Blueprint("transporters", __name__, url_prefix="/transporters" @jwt_required() def list_transporters(): user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] transporters_db = Transporters() transporters = transporters_db.get_all_transporters_by_user(user_id) return jsonify(transporters), 200 @@ -18,6 +23,10 @@ def create_transporter(): transporters_db = Transporters() data = request.get_json() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] transporter_id = transporters_db.create_transporter( user_id=user_id, name=data.get("name"), @@ -36,6 +45,10 @@ def create_transporter(): def update_transporter(transporter_id): transporters_db = Transporters() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] data = request.get_json() transporter = transporters_db.get_transporter_by_id(transporter_id) if not transporter: @@ -58,6 +71,10 @@ def update_transporter(transporter_id): def delete_transporter(transporter_id): transporters_db = Transporters() user_id = get_jwt_identity() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] transporter = transporters_db.get_transporter_by_id(transporter_id) if not transporter: return jsonify({"error": "Transporter not found"}), 404 diff --git a/transportmanager/server/schema_sqlite.sql b/transportmanager/server/schema_sqlite.sql index cb5ab7f..016683e 100644 --- a/transportmanager/server/schema_sqlite.sql +++ b/transportmanager/server/schema_sqlite.sql @@ -15,7 +15,10 @@ CREATE TABLE IF NOT EXISTS users ( 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')) + user_role TEXT NOT NULL DEFAULT 'user' CHECK (user_role IN ('user', 'admin', 'company_user')), + company_id INTEGER DEFAULT 0, + active INTEGER DEFAULT 1, + temporary_passwrd INTEGER DEFAULT 0 ); -- Clients table @@ -94,6 +97,8 @@ CREATE TABLE IF NOT EXISTS orders_in ( trailer_reg_number TEXT, received_price DOUBLE PRECISION, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + file_name TEXT, + expenses DOUBLE PRECISION, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE ); @@ -143,4 +148,17 @@ CREATE TABLE IF NOT EXISTS email ( 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 +); + +CREATE TABLE IF NOT EXISTS company_user_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + company_user_id INTEGER NOT NULL, + clients INTEGER, + transporters INTEGER, + destinations INTEGER, + orders_in INTEGER, + orders_out INTEGER, + report INTEGER, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(company_user_id) REFERENCES users(id) ON DELETE CASCADE +); diff --git a/transportmanager/server/utils/email.py b/transportmanager/server/utils/email.py index 164170d..86a5c77 100644 --- a/transportmanager/server/utils/email.py +++ b/transportmanager/server/utils/email.py @@ -82,9 +82,9 @@ def send_gmail(to_email, subject, body): 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 + 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.") diff --git a/transportmanager/server/utils/welcome_email.py b/transportmanager/server/utils/welcome_email.py new file mode 100644 index 0000000..0496fa1 --- /dev/null +++ b/transportmanager/server/utils/welcome_email.py @@ -0,0 +1,31 @@ +import os +from utils.email import send_gmail_with_attachment + +class WelcomeMessage: + def __init__(self, name, email): + self.name = name + self.email = email + self.subject = 'Welcome to Order Go - TMS - Your Account is Ready' + self.assets_folder = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "assets") + ) + self.manual = os.path.join(self.assets_folder, "manual.pdf") + self.body = f''' + + Dear {self.name}, + + We are pleased to welcome you to Order Go - TMS. Thank you for choosing our platform to support your business needs. + + To assist you in getting started, we have attached the User Manual to this email. It provides step-by-step instructions on account setup, feature overview, and best practices for using Order Go - TMS efficiently. + + We recommend reviewing the manual at your convenience to become familiar with the system's capabilities. Should you require any further assistance, our support team is available at support@ordergotms.com. + + We look forward to supporting your success and building a long-term partnership. + + Sincerely, + The Order Go - TMS Team + + ''' + + def send_email(self): + send_gmail_with_attachment(to_email=self.email, subject=self.subject, body=self.body, attachment_path=self.manual)