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 0000000..2a29a84 Binary files /dev/null and b/transportmanager/server/assets/Manual.pdf differ diff --git a/transportmanager/server/models/company_users.py b/transportmanager/server/models/company_users.py new file mode 100644 index 0000000..cc5d36e --- /dev/null +++ b/transportmanager/server/models/company_users.py @@ -0,0 +1,81 @@ +from datetime import datetime +from database import get_connection, is_postgres + +class CompanyUsers: + def __init__(self): + self.ph = "%s" if is_postgres() else "?" + + def access_to_dict(self, row): + access = { + 'id': row[0], + 'company_user_id': row[1], + 'clients': row[2], + 'transporters': row[3], + 'destinations': row[4], + 'orders_in': row[5], + 'orders_out': row[6], + 'report':row[7], + 'created_at': row[8] + } + return access + + def insert_company_user_access(self, access_data): + created_at = datetime.now().isoformat() + with get_connection() as conn: + cursor = conn.cursor() + returning = "RETURNING id" if is_postgres() else "" + query = f""" + INSERT INTO company_user_access ( + company_user_id, clients, transporters, destinations, orders_in, orders_out, report, created_at + ) VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}) {returning} + """ + cursor.execute( + query, + ( + access_data['company_user_id'], + access_data['clients'], + access_data['transporters'], + access_data['destinations'], + access_data['orders_in'], + access_data['orders_out'], + access_data['report'], + 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 get_access_by_company_user_id(self, company_user_id): + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT * FROM company_user_access WHERE company_user_id = {self.ph}", (company_user_id,)) + row = cursor.fetchone() + return self.access_to_dict(row) if row else None + + def update_company_user_access(self, data): + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + f""" + UPDATE company_user_access + SET clients = {self.ph}, transporters = {self.ph}, destinations = {self.ph}, orders_in = {self.ph}, orders_out = {self.ph}, report = {self.ph} + WHERE company_user_id = {self.ph} + """, + ( + data['clients'], + data['transporters'], + data['destinations'], + data['orders_in'], + data['orders_out'], + data['report'], + data['company_user_id'] + ) + ) + if hasattr(conn, "commit"): + conn.commit() \ No newline at end of file diff --git a/transportmanager/server/models/order_in.py b/transportmanager/server/models/order_in.py index 91268cb..9bf933a 100644 --- a/transportmanager/server/models/order_in.py +++ b/transportmanager/server/models/order_in.py @@ -19,6 +19,8 @@ class OrdersIn: "trailer_reg_number": row[8], "received_price": row[9], "created_at": row[10], + "file":row[11], + "expenses": row[12], } def order_point_to_dict(self, row): @@ -41,8 +43,8 @@ class OrdersIn: 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} + ldb_quantity, kg_quantity, track_reg_number, trailer_reg_number, created_at, file_name, expenses) + 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"], @@ -55,6 +57,8 @@ class OrdersIn: data["track_reg_number"], data["trailer_reg_number"], created_at, + data["file"], + data["expenses"] ), ) new_id = cur.fetchone()[0] if is_postgres() else getattr(cur, "lastrowid", None) @@ -71,7 +75,7 @@ class OrdersIn: 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} + trailer_reg_number = {self.ph}, file_name = {self.ph}, expenses = {self.ph} WHERE id = {self.ph} """, ( @@ -84,6 +88,8 @@ class OrdersIn: data["kg_quantity"], data["track_reg_number"], data["trailer_reg_number"], + data['file'], + data['expenses'], data["id"], ), ) diff --git a/transportmanager/server/models/user.py b/transportmanager/server/models/user.py index add745e..f0b9ff5 100644 --- a/transportmanager/server/models/user.py +++ b/transportmanager/server/models/user.py @@ -22,7 +22,10 @@ class Users: 'created_at': row[12], 'otp_code': row[13], 'otp_expiration': row[14], - 'user_role': row[15] + 'user_role': row[15], + 'company_id': row[16], + 'active': row[17], + 'temporary_password': row[18] } return user @@ -70,6 +73,28 @@ class Users: if hasattr(conn, "commit"): conn.commit() return inserted_id + + def insert_company_user(self, name, email, password_hash, company_id): + created_at = datetime.now().isoformat() + company_id = company_id + user_role = 'company_user' + 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, user_role, company_id + ) VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}) {returning} + """ + cursor.execute(query, (name, email, password_hash, created_at, user_role, company_id)) + 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: @@ -190,5 +215,33 @@ class Users: """, (smtp_host, smtp_port, smtp_user, user_id) ) + if hasattr(conn, "commit"): + conn.commit() + + def deactivate_user(self, user_id): + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + f""" + UPDATE users + SET active = {self.ph} + WHERE id = {self.ph} + """, + (0, user_id) + ) + if hasattr(conn, "commit"): + conn.commit() + + def update_temp_pass(self, user_id): + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + f""" + UPDATE users + SET temporary_passwrd = {self.ph} + WHERE id = {self.ph} + """, + (0, user_id) + ) if hasattr(conn, "commit"): conn.commit() \ No newline at end of file diff --git a/transportmanager/server/routes/auth.py b/transportmanager/server/routes/auth.py index f76e9bf..7581cb6 100644 --- a/transportmanager/server/routes/auth.py +++ b/transportmanager/server/routes/auth.py @@ -9,6 +9,7 @@ import os from datetime import timezone from models.user import Users +from utils.welcome_email import WelcomeMessage auth_bp = Blueprint("auth", __name__) @@ -30,8 +31,10 @@ def register(): password_hash = generate_password_hash(password) users.insert_user(name, email, password_hash) - return jsonify({"message": "User registered successfully!"}), 201 + welcome_message = WelcomeMessage(name, email) + welcome_message.send_email() + return jsonify({"message": "User registered successfully!"}), 201 @auth_bp.route("/login", methods=["POST"]) def login(): @@ -46,6 +49,9 @@ def login(): 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 + + if user["active"] != 1: + return jsonify({"error": "Inactive user"}), 401 otp_code = str(random.randint(100000, 999999)) expiration = datetime.datetime.now(timezone.utc) + datetime.timedelta(minutes=10) @@ -188,7 +194,8 @@ def me(): "terms": user["terms"], "first_order_number": user["first_order_number"], "created_at": user["created_at"], - "user_role": user["user_role"] + "user_role": user["user_role"], + "temporary_password": user["temporary_password"] }), 200 @@ -201,4 +208,17 @@ def validate_token(): 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 + return jsonify({"message": "Token is valid"}), 200 + +@auth_bp.route("/temporary_password", methods=["POST"]) +@jwt_required() +def change_passwd(): + data = request.get_json() + if not data: + return jsonify({"error": "Password not found"}), 404 + users = Users() + user_id = get_jwt_identity() + new_password_hash = generate_password_hash(data['password']) + users.update_user_password(user_id, new_password_hash) + users.update_temp_pass(user_id) + return jsonify({"message": "Password has been updated successfully."}), 200 \ No newline at end of file diff --git a/transportmanager/server/routes/clients.py b/transportmanager/server/routes/clients.py index dec313a..7c3f150 100644 --- a/transportmanager/server/routes/clients.py +++ b/transportmanager/server/routes/clients.py @@ -1,5 +1,6 @@ from flask import Blueprint, request, jsonify from models.client import Clients +from models.user import Users from flask_jwt_extended import jwt_required, get_jwt_identity @@ -10,6 +11,10 @@ clients_bp = Blueprint("clients", __name__, url_prefix="/clients") def list_clients(): clients_db = Clients() 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'] clients = clients_db.get_all_by_user(user_id) return jsonify(clients), 200 @@ -19,6 +24,10 @@ def create_client(): clients_db = Clients() user_id = get_jwt_identity() data = request.get_json() + users = Users() + user = users.get_user_by_id(user_id) + if user['user_role'] == 'company_user': + user_id = user['company_id'] client_id = clients_db.create( user_id=user_id, name=data["name"], diff --git a/transportmanager/server/routes/company_user.py b/transportmanager/server/routes/company_user.py new file mode 100644 index 0000000..de0a6fb --- /dev/null +++ b/transportmanager/server/routes/company_user.py @@ -0,0 +1,118 @@ +from flask import Blueprint, request, jsonify +from models.user import Users +from utils.welcome_email import WelcomeMessage +from werkzeug.security import generate_password_hash, check_password_hash +from flask_jwt_extended import jwt_required, get_jwt_identity +from models.company_users import CompanyUsers + +company_user_bp = Blueprint("company_user", __name__, url_prefix="/company_user") + +@company_user_bp.route('/access/', 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)