diff --git a/UI_V2/.DS_Store b/UI_V2/.DS_Store new file mode 100644 index 0000000..7800e44 Binary files /dev/null and b/UI_V2/.DS_Store differ diff --git a/UI_V2/.env b/UI_V2/.env new file mode 100644 index 0000000..05a5b65 --- /dev/null +++ b/UI_V2/.env @@ -0,0 +1,3 @@ +SUPERUSER_EMAIL=macamete.robert@gmail.com +SUPERUSER_PASSWORD=Inteligent1_eu +SUPERUSER_ROLE=admin \ No newline at end of file diff --git a/UI_V2/Dockerfile b/UI_V2/Dockerfile new file mode 100644 index 0000000..4d28573 --- /dev/null +++ b/UI_V2/Dockerfile @@ -0,0 +1,31 @@ +# Slim Python image +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# System deps for pip & curl (for healthcheck) +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Ensure persistent folders exist +RUN mkdir -p /app/instance /app/assets + +# Default port +ENV FLET_PORT=8080 + +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD curl -fsS http://127.0.0.1:8080/ || exit 1 + +# Run entrypoint to create superuser and start the app +ENTRYPOINT ["sh", "-c", "python create_super_user.py && python main.py"] \ No newline at end of file diff --git a/UI_V2/admin/__pycache__/banner.cpython-313.pyc b/UI_V2/admin/__pycache__/banner.cpython-313.pyc new file mode 100644 index 0000000..9c3e199 Binary files /dev/null and b/UI_V2/admin/__pycache__/banner.cpython-313.pyc differ diff --git a/UI_V2/admin/__pycache__/category.cpython-313.pyc b/UI_V2/admin/__pycache__/category.cpython-313.pyc new file mode 100644 index 0000000..b298a93 Binary files /dev/null and b/UI_V2/admin/__pycache__/category.cpython-313.pyc differ diff --git a/UI_V2/admin/__pycache__/clients.cpython-313.pyc b/UI_V2/admin/__pycache__/clients.cpython-313.pyc new file mode 100644 index 0000000..2d35eea Binary files /dev/null and b/UI_V2/admin/__pycache__/clients.cpython-313.pyc differ diff --git a/UI_V2/admin/__pycache__/dashboard.cpython-313.pyc b/UI_V2/admin/__pycache__/dashboard.cpython-313.pyc new file mode 100644 index 0000000..7938067 Binary files /dev/null and b/UI_V2/admin/__pycache__/dashboard.cpython-313.pyc differ diff --git a/UI_V2/admin/__pycache__/orders.cpython-313.pyc b/UI_V2/admin/__pycache__/orders.cpython-313.pyc new file mode 100644 index 0000000..97d5eab Binary files /dev/null and b/UI_V2/admin/__pycache__/orders.cpython-313.pyc differ diff --git a/UI_V2/admin/__pycache__/products.cpython-313.pyc b/UI_V2/admin/__pycache__/products.cpython-313.pyc new file mode 100644 index 0000000..45f9317 Binary files /dev/null and b/UI_V2/admin/__pycache__/products.cpython-313.pyc differ diff --git a/UI_V2/admin/banner.py b/UI_V2/admin/banner.py new file mode 100644 index 0000000..5c54e81 --- /dev/null +++ b/UI_V2/admin/banner.py @@ -0,0 +1,88 @@ +import flet as ft +import os +import shutil + +class Banner: + def __init__(self, page: ft.Page): + self.page = page + + self.banner_image = ft.Image( + src='images/banner_placeholder.png', + height=350, + width=1000, + fit=ft.ImageFit.COVER + ) + + self.file_dialog = ft.FilePicker( + on_result=self.on_file_picker_result, + on_upload=self.on_upload_progress + ) + self.page.overlay.append(self.file_dialog) + self.page.update() # Required to register the FilePicker control + + self.uploaded_files = [] + + def open_file_picker(self, e=None): + self.file_dialog.pick_files( + allow_multiple=False, + allowed_extensions=["png", "jpg", "jpeg"] + ) + + def on_file_picker_result(self, e: ft.FilePickerResultEvent): + if e.files: + file = e.files[0] + file_name = file.name + upload_url = self.page.get_upload_url(file_name, 600) + + print(f"Uploading {file_name} to {upload_url}") + + upload_task = ft.FilePickerUploadFile( + name=file.name, + upload_url=upload_url + ) + self.file_dialog.upload([upload_task]) + + def on_upload_progress(self, e: ft.FilePickerUploadEvent): + if e.progress == 1: + print(f"Upload complete: {e.file_name}") + + base_path = os.getcwd() # <-- The correct root path + uploads_path = os.path.join(base_path, "uploads") + assets_path = os.path.join(base_path, "assets", "images") + os.makedirs(assets_path, exist_ok=True) + + source_file = os.path.join(uploads_path, e.file_name) + destination_file2 = os.path.join(assets_path, e.file_name) + destination_file = os.path.join(assets_path, 'banner.png') + + if not os.path.exists(source_file): + print(f"❌ File not found: {source_file}") + return + + try: + shutil.copy(source_file, destination_file2) + shutil.move(source_file, destination_file) + print(f"✅ File moved: {source_file} → {destination_file}") + self.banner_image.src = f'images/{e.file_name}' + self.banner_image.update() + self.page.update() + self.foto = e.file_name + except Exception as ex: + print(f"❌ Error moving file: {ex}") + + def build(self): + return ft.Container( + content=ft.Column( + [ + self.banner_image, + ft.Button( + "Adauga imagine", + on_click=self.open_file_picker, + icon=ft.Icons.UPLOAD + ) + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + expand=True + ), + expand=True + ) \ No newline at end of file diff --git a/UI_V2/admin/category.py b/UI_V2/admin/category.py new file mode 100644 index 0000000..eacf5a7 --- /dev/null +++ b/UI_V2/admin/category.py @@ -0,0 +1,242 @@ +import flet as ft +from dbActions.categories import Categories +import os +import shutil + +class Category: + def __init__(self, page: ft.Page): + self.page = page + self.categories_manager = Categories() + self.all_cateogies = self.categories_manager.get_categories() + self.list_of_categories = ft.ListView( + controls=self.create_list(self.all_cateogies, self.on_edit_btn_click, self.on_delete_btn_click), + spacing=10, + expand=True + ) + self.foto = None + self.edit_id = None + + self.file_dialog = ft.FilePicker( + on_result=self.on_file_picker_result, + on_upload=self.on_upload_progress + ) + self.page.overlay.append(self.file_dialog) + self.page.update() # Required to register the FilePicker control + + self.uploaded_files = [] + + self.category_image = ft.Image( + width=150, + height=150, + src='images/placeholder.png', + ) + + self.category_name = ft.TextField(label="Denumire") + + self.add_dialog = ft.AlertDialog( + title=ft.Text("Categorie"), + content=ft.Column( + [ + ft.Row( + [ + self.category_image, + ft.Button("Incarca", icon=ft.Icons.UPLOAD, on_click=self.open_file_picker) + ] + ), + self.category_name + ], + height=200, + width=400 + ), + actions=[ + ft.Button( + "Salveaza", + on_click=self.on_save_btn_click, + icon=ft.Icons.SAVE, + ), + ft.TextButton( + "Anuleaza", + on_click=self.on_cancel_btn_click, + icon=ft.Icons.CANCEL, + ), + ] + ) + + self.delete_item_id = None + + self.confirm_delete_alert = ft.AlertDialog( + title=ft.Text("Confirmati?"), + actions=[ + ft.Button( + "Da", + on_click=self.on_delete_product_click, + icon=ft.Icons.DELETE, + ), + ft.TextButton( + "Nu", + on_click=self.on_delete_cancel_btn_click, + icon=ft.Icons.CANCEL, + ), + ] + ) + + def on_file_picker_result(self, e: ft.FilePickerResultEvent): + if e.files: + file = e.files[0] + file_name = file.name + upload_url = self.page.get_upload_url(file_name, 600) + + print(f"Uploading {file_name} to {upload_url}") + + upload_task = ft.FilePickerUploadFile( + name=file.name, + upload_url=upload_url + ) + self.file_dialog.upload([upload_task]) + + def open_file_picker(self, e=None): + self.file_dialog.pick_files( + allow_multiple=False, + allowed_extensions=["png", "jpg", "jpeg"] + ) + + def on_upload_progress(self, e: ft.FilePickerUploadEvent): + if e.progress == 1: + print(f"Upload complete: {e.file_name}") + + # Resolve paths relative to the UI folder (two levels up from this file) + ui_root = os.path.dirname(os.path.dirname(__file__)) + uploads_path = os.path.join(ui_root, "uploads") + assets_path = os.path.join(ui_root, "assets", "images") + os.makedirs(assets_path, exist_ok=True) + + source_file = os.path.join(uploads_path, e.file_name) + destination_file = os.path.join(assets_path, e.file_name) + + if not os.path.exists(source_file): + print(f"❌ File not found: {source_file}") + return + + try: + shutil.move(source_file, destination_file) + print(f"✅ File moved: {source_file} → {destination_file}") + self.category_image.src = f'images/{e.file_name}' + self.category_image.update() + self.page.update() + self.foto = e.file_name + except Exception as ex: + print(f"❌ Error moving file: {ex}") + + def on_add_btn_click(self, e): + self.page.open(self.add_dialog) + + def on_save_btn_click(self, e): + if self.category_image.src == 'images/placeholder.png' or self.category_name.value == '': + return + + if self.edit_id is None: + self.categories_manager.add(self.category_name.value, self.category_image.src) + else: + self.categories_manager.update(self.category_name.value, self.category_image.src, self.edit_id ) + self.edit_id = None + + self.page.close(self.add_dialog) + self.category_image.src = 'images/placeholder.png' + self.category_image.update() + self.category_name.value = '' + self.category_name.update() + self.update_list() + + def on_cancel_btn_click(self, e): + self.page.close(self.add_dialog) + self.category_image.src = 'images/placeholder.png' + self.category_image.update() + self.category_name.value = '' + self.category_name.update() + + def create_list(self, items, on_click_handler, on_click_handler2): + """Helper to create list items for a column.""" + return [ + ft.Container( + content=ft.Row( + [ + + ft.Row( + [ + ft.Icon(ft.Icons.ARROW_RIGHT, size=20), + ft.Text(value=item['name']) + ] + ), + + ft.Row( + [ + ft.IconButton( + icon=ft.Icons.EDIT, + on_click=lambda e, id=item: on_click_handler(id), + ), + ft.IconButton( + icon=ft.Icons.DELETE, + on_click=lambda e, id=item['id']: on_click_handler2(id), + icon_color=ft.Colors.RED, + ), + ] + ) + + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + width=300, + bgcolor=ft.Colors.BROWN_50, + border = ft.border.all(1, ft.Colors.GREY), + padding=10, + border_radius=8, + ) + for item in items + ] + + def on_edit_btn_click(self, item): + self.edit_id = item['id'] + self.category_name.value = item['name'] + self.category_image.src = item['image'] + self.page.open(self.add_dialog) + + def on_delete_btn_click(self, id): + self.delete_item_id = id + self.page.open(self.confirm_delete_alert) + + def on_delete_product_click(self, e): + self.categories_manager.delete(self.delete_item_id ) + self.delete_item_id = None + self.page.close(self.confirm_delete_alert) + print('Update list') + self.update_list() + + def on_delete_cancel_btn_click(self, e): + self.delete_item_id = None + self.page.close(self.confirm_delete_alert) + + def update_list(self): + self.all_cateogies = self.categories_manager.get_categories() + self.list_of_categories.controls.clear() + self.list_of_categories.controls = self.create_list(self.all_cateogies, self.on_edit_btn_click, self.on_delete_btn_click) + self.list_of_categories.update() + self.page.update() + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Categori", size=18, weight=ft.FontWeight.BOLD), + ft.Button("Adauga", icon=ft.Icons.ADD, on_click=self.on_add_btn_click), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + self.list_of_categories + ], + alignment=ft.MainAxisAlignment.START, + expand=True + ), + expand=True, + ) \ No newline at end of file diff --git a/UI_V2/admin/clients.py b/UI_V2/admin/clients.py new file mode 100644 index 0000000..742b9dd --- /dev/null +++ b/UI_V2/admin/clients.py @@ -0,0 +1,352 @@ +import flet as ft +from dbActions.users import Users +from dbActions.company import Company + +class Clients: + def __init__(self, page: ft.Page): + self.page = page + self.user_manager = Users() + self.company_manager = Company() + + self.user_name = ft.TextField(label="Nume si Prenume") + self.email = ft.TextField(label="E-mail") + self.phone = ft.TextField(label="Telefon") + self.address = ft.TextField(label="Adresa", multiline=True, min_lines=3, max_lines=5) + self.company_name = ft.TextField(label="Denumire firma") + self.vat = ft.TextField(label="CUI") + self.register_number = ft.TextField(label="Numar registru comert") + self.company_address = ft.TextField(label="Sediu", multiline=True, min_lines=3, max_lines=5) + self.company_placeholder = ft.Column() + self.second_address_placeholder = ft.Column() + self.second_address = ft.TextField( + label="Adresa de livrare", + multiline=True, + min_lines=3, + max_lines=5 + ) + + self.client_column = ft.Column( + [ + self.user_name, + self.email, + self.phone, + self.address, + ft.Button("Persoana Juridica?", on_click=self.on_is_comopany_btn_click), + self.company_placeholder, + ft.Button("Adresa de livrare difera de adresa de domiciliu?", on_click=self.on_second_address), + self.second_address_placeholder + ], + scroll=ft.ScrollMode.ADAPTIVE, + width=400, + auto_scroll=True + ) + + self.add_user_dialog = ft.AlertDialog( + title="Client", + content=self.client_column, + actions=[ + ft.Button( + "Salveaza", + on_click=self.on_save_btn_click, + icon=ft.Icons.SAVE, + ), + ft.TextButton( + "Anuleaza", + on_click=self.on_cancel_btn_click, + icon=ft.Icons.CANCEL, + ), + ] + ) + self.is_company = False + self.is_second_address = False + + self.all_clients = self.user_manager.get_all() + self.list_of_clients = ft.ListView( + controls=self.create_list(self.all_clients, self.on_edit_btn_click, self.on_delete_btn_click, self.on_view_btn_click), + spacing=10, + expand=True + ) + + self.view_name = ft.Text() + self.view_email = ft.Text() + self.view_phone = ft.Text() + self.view_address = ft.Text() + self.view_company_placeholder = ft.Column() + self.view_second_address_placeholder = ft.Column() + + self.view_company_name = ft.Text() + self.view_company_vat = ft.Text() + self.view_company_rn = ft.Text() + self.view_company_address = ft.Text() + self.view_second_address = ft.Text() + + self.view_dialog = ft.AlertDialog( + title=ft.Text("Vizualizeaza"), + content=ft.Column( + [ + ft.Text("Utilizator:", weight=ft.FontWeight.BOLD), + self.view_name, + self.view_email, + self.view_phone, + self.view_address, + self.view_company_placeholder, + self.view_second_address_placeholder + ], + scroll=ft.ScrollMode.ADAPTIVE, + width=400 + ), + actions=[ + ft.Button("Ok", on_click=self.on_ok_btn_click) + ] + ) + + self.delete_item_id = None + + self.confirm_delete_alert = ft.AlertDialog( + title=ft.Text("Confirmati?"), + actions=[ + ft.Button( + "Da", + on_click=self.on_delete_client_click, + icon=ft.Icons.DELETE, + ), + ft.TextButton( + "Nu", + on_click=self.on_delete_cancel_btn_click, + icon=ft.Icons.CANCEL, + ), + ] + ) + + self.edit_id = None + + def on_ok_btn_click(self, e): + self.page.close(self.view_dialog) + self.view_name.value = '' + self.view_email.value = '' + self.view_phone.value = '' + self.view_address.value = '' + self.view_company_placeholder.controls.clear() + self.view_second_address_placeholder.controls.clear() + + def on_edit_btn_click(self, item): + self.edit_id = item['id'] + self.user_name.value = item['name'] + self.email.value = item['email'] + self.email.disabled = True + self.phone.value = item['phone'] + self.address.value = item['address'].split("~")[0] + company = self.company_manager.get_company(item['id']) + if company: + self.company_name.value = company['name'] + self.company_placeholder.controls.append(self.company_name) + self.vat.value = company['vat'] + self.company_placeholder.controls.append(self.vat) + self.register_number.value = company['register_number'] + self.company_placeholder.controls.append(self.register_number) + self.company_address.value = company['address'] + self.company_placeholder.controls.append(self.company_address) + if len(item['address'].split("~")) > 1: + self.second_address.value = item['address'].split("~")[1] + self.second_address_placeholder.controls.append(self.second_address) + self.page.open(self.add_user_dialog) + + def on_delete_btn_click(self, id): + self.delete_item_id = id + self.page.open(self.confirm_delete_alert) + + def on_delete_client_click(self, e): + self.user_manager.delete(self.delete_item_id) + self.delete_item_id = None + self.page.close(self.confirm_delete_alert) + self.all_clients = self.user_manager.get_all() + self.list_of_clients.controls = self.create_list(self.all_clients, self.on_edit_btn_click, self.on_delete_btn_click, self.on_view_btn_click) + self.list_of_clients.update() + + def on_delete_cancel_btn_click(self, e): + self.delete_item_id = None + self.page.close(self.confirm_delete_alert) + + def on_view_btn_click(self, item): + self.view_name.value = f"Nume: {item['name']}" + self.view_email.value = f"E-mail: {item['email']}" + self.view_phone.value = f"Telefon: {item['phone']}" + self.view_address.value = f"Adresa: {item['address'].split("~")[0]}" + company = self.company_manager.get_company(item['id']) + if company: + self.view_company_placeholder.controls.append(ft.Text("")) + self.view_company_placeholder.controls.append(ft.Text("Date Companie:", weight=ft.FontWeight.BOLD)) + self.view_company_name.value = f"Denumire: {company['name']}" + self.view_company_placeholder.controls.append(self.view_company_name) + self.view_company_vat.value = f"CUI: {company['vat']}" + self.view_company_placeholder.controls.append(self.view_company_vat) + self.view_company_rn.value = f"Registru comertului: {company['register_number']}" + self.view_company_placeholder.controls.append(self.view_company_rn) + self.view_company_address.value = f"Sediu: {company['address']}" + self.view_company_placeholder.controls.append(self.view_company_address) + if len(item['address'].split("~")) > 1: + self.view_second_address_placeholder.controls.append(ft.Text("")) + self.view_second_address_placeholder.controls.append(ft.Text("Adresa Livrare:", weight=ft.FontWeight.BOLD)) + self.view_second_address.value = f"Adresa: {item['address'].split("~")[1]}" + self.view_second_address_placeholder.controls.append(self.view_second_address) + self.page.open(self.view_dialog) + + def create_list(self, items, on_click_handler, on_click_handler2, on_click_handler3): + """Helper to create list items for a column.""" + return [ + ft.Container( + content=ft.Row( + [ + + ft.Column( + [ + ft.Text(value=item['name'], weight=ft.FontWeight.BOLD), + ft.Text(value=item['email'], size=12) + ] + ), + + ft.Row( + [ + ft.IconButton( + icon=ft.Icons.PREVIEW, + on_click=lambda e, id=item: on_click_handler3(id), + ), + ft.IconButton( + icon=ft.Icons.EDIT, + on_click=lambda e, id=item: on_click_handler(id), + ), + ft.IconButton( + icon=ft.Icons.DELETE, + on_click=lambda e, id=item['id']: on_click_handler2(id), + icon_color=ft.Colors.RED, + ), + ] + ) + + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + width=300, + bgcolor=ft.Colors.BROWN_50, + border = ft.border.all(1, ft.Colors.GREY), + padding=10, + border_radius=8, + ) + for item in items + ] + + def on_save_btn_click(self, e): + user_name = self.user_name.value + user_email = self.email.value + user_phone = self.phone.value + user_address = self.address.value + company = {} + + company['name'] = self.company_name.value + company['vat'] = self.vat.value + company['register_number'] = self.register_number.value + company['address'] = self.company_address.value + delivery_address = self.second_address.value + address = f'{user_address} ~ {delivery_address}' + if self.edit_id: + if len(delivery_address) == 0: + self.user_manager.update_user_data(user_name, user_phone, user_address, self.edit_id) + else: + self.user_manager.update_user_data(user_name, user_phone, address, self.edit_id) + exising_company = self.company_manager.get_company(self.edit_id) + if exising_company: + company['user_id'] = self.edit_id + company['id'] = exising_company['id'] + self.company_manager.update_company(company) + else: + + user_id = self.user_manager.invite_user(user_email, user_name, user_phone, user_address) + print(user_id) + if user_id: + company['user_id'] = user_id + if self.is_company: + self.company_manager.add_company(company) + if self.is_second_address: + self.user_manager.update_user_data(user_name, user_phone, address, user_id) + self.clear_fileds() + self.page.close(self.add_user_dialog) + self.all_clients = self.user_manager.get_all() + self.list_of_clients.controls = self.create_list(self.all_clients, self.on_edit_btn_click, self.on_delete_btn_click, self.on_view_btn_click) + self.list_of_clients.update() + + def on_cancel_btn_click(self, e): + self.page.close(self.add_user_dialog) + self.clear_fileds() + + def clear_fileds(self): + self.user_name.value = '' + self.user_name.update() + self.email.value = '' + self.email.update() + self.phone.value = '' + self.phone.update() + self.address.value = '' + self.address.update() + try: + self.company_name.value = '' + self.company_name.update() + self.vat.value = '' + self.vat.update() + self.register_number.value = '' + self.register_number.update() + self.company_address.value = '' + self.company_address.update() + except Exception as e: + print(f'No company, Error: {e}') + try: + self.second_address.value = '' + self.second_address.update() + except Exception as e: + print(f'No second address, Error: {e}') + self.company_placeholder.controls.clear() + self.company_placeholder.update() + self.second_address_placeholder.controls.clear() + self.second_address_placeholder.update() + + def on_is_comopany_btn_click(self, e): + self.is_company = True + self.company_placeholder.controls.append(ft.Text("Date firma")) + self.company_placeholder.controls.append(self.company_name) + self.company_placeholder.controls.append(self.vat) + self.company_placeholder.controls.append(self.register_number) + self.company_placeholder.controls.append(self.company_address) + self.company_placeholder.update() + + def on_second_address(self, e): + self.is_second_address = True + self.second_address_placeholder.controls.append(ft.Text("Adresa de livrare")) + self.second_address_placeholder.controls.append( + self.second_address + ) + self.second_address_placeholder.update() + + def on_add_btn_click(self, e): + self.page.open(self.add_user_dialog) + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Clienti", + weight=ft.FontWeight.BOLD, + size=18 + ), + ft.Button( + icon=ft.Icons.ADD, + text = "Invita", + on_click= self.on_add_btn_click + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + self.list_of_clients + ] + ) + ) \ No newline at end of file diff --git a/UI_V2/admin/dashboard.py b/UI_V2/admin/dashboard.py new file mode 100644 index 0000000..deb3450 --- /dev/null +++ b/UI_V2/admin/dashboard.py @@ -0,0 +1,128 @@ +import flet as ft +from admin.category import Category +from admin.products import ProductsPage +from admin.banner import Banner +from admin.orders import OrdersPage +from admin.clients import Clients + +class Dashboard: + def __init__(self, page: ft.Page): + self.page = page + self.category = Category(self.page) + self.placeholder = ft.Container( + content=self.category.build(), + padding=10, + expand=True + ) + self.rail = ft.NavigationRail( + selected_index=0, + label_type=ft.NavigationRailLabelType.ALL, + min_width=100, + #min_extended_width=400, + leading=ft.Image('images/tainagustului.png', width=80), + group_alignment=-0.9, + destinations=[ + ft.NavigationRailDestination( + icon=ft.Icons.CATEGORY_OUTLINED, + selected_icon=ft.Icons.CATEGORY_ROUNDED, + label="Categori", + ), + ft.NavigationRailDestination( + icon=ft.Icons.ADD_BOX_OUTLINED, + selected_icon=ft.Icons.ADD_BOX_ROUNDED, + label="Produse", + ), + ft.NavigationRailDestination( + icon=ft.Icon(ft.Icons.MANAGE_ACCOUNTS_OUTLINED), + selected_icon=ft.Icon(ft.Icons.MANAGE_ACCOUNTS_ROUNDED), + label="Clienti", + ), + ft.NavigationRailDestination( + icon=ft.Icons.SHOPPING_CART_OUTLINED, + selected_icon=ft.Icon(ft.Icons.SHOPPING_CART_ROUNDED), + label_content=ft.Text("Comenzi"), + ), + ft.NavigationRailDestination( + icon=ft.Icons.SHOPIFY_OUTLINED, + selected_icon=ft.Icon(ft.Icons.SHOPIFY_ROUNDED), + label_content=ft.Text("Magazin"), + ), + ft.NavigationRailDestination( + icon=ft.Icons.IMAGE_OUTLINED, + selected_icon=ft.Icon(ft.Icons.IMAGE_ROUNDED), + label_content=ft.Text("Banner"), + ), + ft.NavigationRailDestination( + icon=ft.Icons.LOGOUT_OUTLINED, + selected_icon=ft.Icon(ft.Icons.LOGOUT_ROUNDED), + label_content=ft.Text("Logout"), + ), + ], + on_change=lambda e: self.navigate_to(e), + ) + + def navigate_to(self, e): + index = e.control.selected_index + match index: + case 0: + self.category = Category(self.page) + self.placeholder.content = self.category.build() + self.placeholder.update() + case 1: + self.products = ProductsPage(self.page) + self.placeholder.content = self.products.build() + self.placeholder.update() + case 2: + self.clients = Clients(self.page) + self.placeholder.content = self.clients.build() + self.placeholder.update() + case 3: + self.orders = OrdersPage(self.page) + self.placeholder.content = self.orders.build() + self.placeholder.update() + case 4: + self.page.launch_url('http://0.0.0.0:5555/') + case 5: + self.banner = Banner(self.page) + self.placeholder.content = self.banner.build() + self.placeholder.update() + case 6: + self.page.client_storage.clear() + self.page.session.clear() + self.page.go('/') + + def build(self): + if self.page.session.get("user") is None or self.page.session.get("user")['role'] != 'admin': + return ft.Container( + content=ft.Column( + [ + ft.Text( + "Nu aveti drepturi sa accesati pagina de admin!", + size=20, + weight=ft.FontWeight.BOLD, + text_align=ft.TextAlign.CENTER + ), + ft.Text("Va rugam sa va autentificati cu un rol de admin",text_align=ft.TextAlign.CENTER) + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ) + ) + else: + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + self.rail, + ft.VerticalDivider(width=1), + self.placeholder + ], + expand=True + ), + #self.shop_page + ], + expand=True + ), + expand=True + ) \ No newline at end of file diff --git a/UI_V2/admin/orders.py b/UI_V2/admin/orders.py new file mode 100644 index 0000000..b91498e --- /dev/null +++ b/UI_V2/admin/orders.py @@ -0,0 +1,374 @@ +import flet as ft +from dbActions.orders import Orders +from dbActions.users import Users +from dbActions.products import Products +from helpers.default_user import DefaultUser + +class OrdersPage: + def __init__(self, page: ft.Page): + self.page = page + self.orders = Orders() + self.users = Users() + self.products = Products() + self.selected_user = None + self.selected_order = None + self.selected_order_products = None + self.original_status = None + self.customer_email = None + self.default_user = DefaultUser(self.page) + self.header = ft.Row( + [ + ft.Text( + "Comenzi", + weight=ft.FontWeight.BOLD, + size=18 + ), + ], + alignment=ft.MainAxisAlignment.START + ) + self.all_orders = self.orders.get_orders() + self.all_orders = self.all_orders[::-1] + self.oll_orders_list = ft.ListView( + controls=self.create_list(self.all_orders, self.on_order_click), + spacing=10, + expand=3, + height=700 + ) + self.fileters = ft.RadioGroup( + content=ft.Row( + [ + ft.Radio(value="on_hold", label="In asteptare"), + ft.Radio(value="new", label="Noua"), + ft.Radio(value="in_progress", label="In lucru"), + ft.Radio(value="completed", label="Complete"), + ft.Radio(value="all", label="Toate") + ] + ), + on_change=self.on_filter_change + ) + self.name = ft.Text() + self.email = ft.Text() + self.phone = ft.Text() + self.address = ft.Text() + self.total_pay = ft.Text(weight=ft.FontWeight.BOLD) + self.products_column = ft.Column([]) + self.status = ft.Text() + self.buttons_state = ft.RadioGroup( + content=ft.Row( + [ + ft.Radio(value="on_hold", label="In asteptare"), + ft.Radio(value="new", label="Noua"), + ft.Radio(value="in_progress", label="In lucru"), + ft.Radio(value="completed", label="Complete"), + ], + ), + on_change=self.on_radio_value_change + ) + self.total_row = ft.Row( + [ + ft.Text( + value="Total platit", + weight=ft.FontWeight.BOLD, + ), + self.total_pay + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ) + + self.order_details = ft.Column( + [ + ft.Text( + value="Detalii utilizator", + weight=ft.FontWeight.BOLD, + size=18 + ), + ft.Row( + [ + ft.Text( + value="Nume si prenume", + weight=ft.FontWeight.BOLD, + ), + self.name + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + value="E-mail", + weight=ft.FontWeight.BOLD, + ), + self.email + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + value="Telefon", + weight=ft.FontWeight.BOLD, + ), + self.phone + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + value="Adresa", + weight=ft.FontWeight.BOLD, + ), + self.address + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + ft.Text( + value="Status", + weight=ft.FontWeight.BOLD, + ), + self.status, + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + self.total_row, + ft.Divider(), + ft.Text( + value="Produse", + weight=ft.FontWeight.BOLD, + size=18 + ), + self.products_column, + ft.Divider(), + self.buttons_state + ], + expand=True + ) + + self.order_details_placeholder = ft.Container( + expand=7, + padding=10, + border_radius=10, + ) + + self.message_field = ft.TextField( + label="Scrie un mesaj clientului", + min_lines=3, + max_lines=5, + multiline=True + ) + + self.change_state_dialog = ft.AlertDialog( + title=ft.Text("Modifica statusul"), + content=self.message_field, + actions=[ + ft.FilledButton( + "Da", + on_click=self.on_change_state_btn_click + ), + ft.FilledButton( + "Nu", + on_click=self.on_cancel_state_btn_click, + bgcolor=ft.Colors.GREY + ) + ] + ) + + def on_radio_value_change(self, e): + self.page.open(self.change_state_dialog) + + def on_change_state_btn_click(self, e): + self.page.close(self.change_state_dialog) + status = self.buttons_state.value + self.orders.update_order_status(status, self.selected_order['id']) + print(status) + self.all_orders = self.orders.get_orders() + self.all_orders = self.all_orders[::-1] + self.oll_orders_list.controls.clear() + self.oll_orders_list.controls = self.create_list(self.all_orders, self.on_order_click) + self.oll_orders_list.update() + self.order_details_placeholder.content = None + self.order_details_placeholder.update() + #-------------------------------------------> TBI Send information email to customer + message = self.message_field.value + self.message_field.value = '' + self.message_field.update() + print(message) + email = self.customer_email + + def on_cancel_state_btn_click(self, e): + self.buttons_state.value = self.original_status + self.buttons_state.update() + self.page.close(self.change_state_dialog) + + def get_total_pay(self): + sume = 0 + print (self.selected_order_products) + if self.selected_order_products: + for product in self.selected_order_products: + sume += (product['price'] - product['price']*product['discount']/100) + print(sume) + return sume + + def on_order_click(self, item): + self.products_column.controls.clear() + self.order_details_placeholder.content = None + self.order_details_placeholder.content = self.order_details + self.order_details_placeholder.bgcolor=ft.Colors.BROWN_50 + try: + self.order_details_placeholder.update() + except: + print('ERROR Unable to update the Order Details Placeholder') + print("order item", item) + self.selected_user = self.users.get(item['user_id']) + if self.selected_user == None: + self.selected_user = self.default_user.default_user + self.selected_order = item + products = self.orders.get_order_products(item['id']) + self.selected_order_products = [] + if products: + for product in products: + prod = self.products.get(product['prdouct_id']) + print(prod) + if prod: + self.selected_order_products.append(prod) + + self.name.value = self.selected_user['name'] if '@default.com' not in self.selected_user['email'] else 'Anonim user' + self.name.update() + self.email.value = self.selected_user['email'] + self.email.update() + self.customer_email = self.selected_user['email'] + self.address.value = self.selected_user['address'] if '@default.com' not in self.selected_user['email'] else 'Anonim user' + self.address.update() + self.phone.value = self.selected_user['phone'] if '@default.com' not in self.selected_user['email'] else 'Anonim user' + self.phone.update() + self.status.value = "Status" + self.status.update() + for product in self.selected_order_products: + name_label = ft.Text( + "Denumire produs", + weight=ft.FontWeight.BOLD + ) + name = ft.Text(product['name']) + name_row = ft.Row( + [ + name_label, + name + ] + ) + image = ft.Image( + src=product['image'], + width=200, + height=200, + fit=ft.ImageFit.CONTAIN + ) + quantity_label = ft.Text("Cantitate") + quantity = ft.Text(product['quantity']) + quantity_row = ft.Row( + [ + quantity_label, + quantity + ] + ) + product_row = ft.Row( + [ + image, + ft.Column( + [ + name_row, + quantity_row + ] + ) + ] + ) + self.products_column.controls.append(product_row) + print(self.products_column.controls) + self.total_pay.value = f"{self.get_total_pay()} Lei" + self.total_pay.update() + self.products_column.update() + + self.buttons_state.value = item['status'] + self.buttons_state.update() + self.original_status = item['status'] + + def on_filter_change(self, e): + print(e.data) + buffer = [] + if e.data == 'all': + self.oll_orders_list.controls = self.create_list(self.all_orders, self.on_order_click) + self.oll_orders_list.update() + else: + for order in self.all_orders: + if order['status'] == e.data: + buffer.append(order) + self.oll_orders_list.controls = self.create_list(buffer, self.on_order_click) + self.oll_orders_list.update() + + def get_status(self, status): + STATUS = { + 'on_hold': "In asteptare", + 'new': "Noua", + 'in_progress': "In lucru", + "completed": "Completa" + } + return STATUS[status] + + def create_list(self, items, on_click_handler): + return [ + ft.Container( + content=ft.Row( + [ + + ft.Row( + [ + ft.Icon(ft.Icons.ARROW_RIGHT, size=20), + ft.Text(value=item['id']) + ] + ), + ft.Row( + [ + ft.Text( + value=self.get_status(item['status']) + ) + ] + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + width=300, + bgcolor=ft.Colors.BROWN_50, + padding=10, + border_radius=8, + ink=True, + on_click=lambda e, i=item: on_click_handler(i), + border = ft.border.all(1, ft.Colors.GREY), + ) + for item in items + ] + + def build(self): + return ft.Container( + content=ft.Column( + [ + self.header, + ft.Row( + [ + ft.Text(value="Filtreaza dupa"), + self.fileters + ] + ), + ft.Row( + [ + self.oll_orders_list, + self.order_details_placeholder + ], + vertical_alignment=ft.CrossAxisAlignment.START + ) + ], + alignment=ft.MainAxisAlignment.START, + expand=True, + #scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + ) \ No newline at end of file diff --git a/UI_V2/admin/products.py b/UI_V2/admin/products.py new file mode 100644 index 0000000..957d0f8 --- /dev/null +++ b/UI_V2/admin/products.py @@ -0,0 +1,308 @@ +import flet as ft +from dbActions.categories import Categories +from dbActions.products import Products +import os +import shutil + +class ProductsPage: + def __init__(self, page: ft.Page): + self.page = page + self.category_manager = Categories() + self.product_manager = Products() + + self.name = ft.TextField(label="Denumire") + self.description = ft.TextField(label="Descriere", multiline=True, min_lines=3, max_lines=5) + self.details = ft.TextField(label="Detalii", multiline=True, min_lines=3, max_lines=5) + self.price = ft.TextField(label="Pret") + self.discount = ft.TextField(label="Reducere (%)") + self.quantity = ft.TextField(label="Cantitate") + self.product_image = ft.Image( + width=150, + height=150, + src='images/placeholder.png', + ) + + self.aviability = ft.Dropdown( + label='Disonibilitate', + options=[ + ft.dropdown.Option(key='in_stock', text="In stoc"), + ft.dropdown.Option(key='in_provider_stock', text="In stoc la furnizor"), + ft.dropdown.Option(key='not_available', text="Indisponibil"), + ], + expand=True + ) + + self.category = ft.Dropdown( + label="Categorie", + options=self.get_categories(), + expand=True + ) + + + self.add_product_dialog = ft.AlertDialog( + title=ft.Text("Produs"), + content=ft.Column( + [ + ft.Row( + [ + self.product_image, + ft.Button("Incarca", icon=ft.Icons.UPLOAD, on_click=self.open_file_picker) + ] + ), + self.name, + self.category, + self.description, + self.details, + self.price, + self.discount, + self.quantity, + self.aviability, + ], + scroll=ft.ScrollMode.ADAPTIVE, + width=400 + ), + actions=[ + ft.Button( + "Salveaza", + on_click=self.on_save_btn_click, + icon=ft.Icons.SAVE, + ), + ft.TextButton( + "Anuleaza", + on_click=self.on_cancel_btn_click, + icon=ft.Icons.CANCEL, + ), + ] + ) + self.foto = None + self.file_dialog = ft.FilePicker( + on_result=self.on_file_picker_result, + on_upload=self.on_upload_progress + ) + self.page.overlay.append(self.file_dialog) + self.page.update() # Required to register the FilePicker control + + self.uploaded_files = [] + + self._all_products = self.product_manager.get_all() + self.products_list = ft.ListView( + controls=self.create_list(self._all_products, self.edit_product, self.delete_product), + spacing=10, + expand=True + ) + + self.edit_item_id = None + self.delete_item_id = None + + self.confirm_delete_alert = ft.AlertDialog( + title=ft.Text("Confirmati?"), + actions=[ + ft.Button( + "Da", + on_click=self.on_delete_product_click, + icon=ft.Icons.DELETE, + ), + ft.TextButton( + "Nu", + on_click=self.on_delete_cancel_btn_click, + icon=ft.Icons.CANCEL, + ), + ] + ) + + def on_file_picker_result(self, e: ft.FilePickerResultEvent): + if e.files: + file = e.files[0] + file_name = file.name + upload_url = self.page.get_upload_url(file_name, 600) + + print(f"Uploading {file_name} to {upload_url}") + + upload_task = ft.FilePickerUploadFile( + name=file.name, + upload_url=upload_url + ) + self.file_dialog.upload([upload_task]) + + def open_file_picker(self, e=None): + self.file_dialog.pick_files( + allow_multiple=False, + allowed_extensions=["png", "jpg", "jpeg"] + ) + + def on_upload_progress(self, e: ft.FilePickerUploadEvent): + if e.progress == 1: + print(f"Upload complete: {e.file_name}") + + # Resolve paths relative to the UI folder (two levels up from this file) + ui_root = os.path.dirname(os.path.dirname(__file__)) + uploads_path = os.path.join(ui_root, "uploads") + assets_path = os.path.join(ui_root, "assets", "images") + os.makedirs(assets_path, exist_ok=True) + + source_file = os.path.join(uploads_path, e.file_name) + destination_file = os.path.join(assets_path, e.file_name) + + if not os.path.exists(source_file): + print(f"❌ File not found: {source_file}") + return + + try: + shutil.move(source_file, destination_file) + print(f"✅ File moved: {source_file} → {destination_file}") + self.product_image.src = f'images/{e.file_name}' + self.product_image.update() + self.page.update() + self.foto = e.file_name + except Exception as ex: + print(f"❌ Error moving file: {ex}") + + def on_add_btn_click(self, e): + self.page.open(self.add_product_dialog) + + def on_save_btn_click(self, e): + product = { + 'name': self.name.value, + 'description': self.description.value, + 'details': self.details.value, + 'price' : self.price.value, + 'discount' : self.discount.value, + 'quantity' : self.quantity.value, + 'image' : self.product_image.src, + 'aviability' : self.aviability.value, + 'category_id' : self.category.value + } + print(product) + if self.edit_item_id == None: + self.product_manager.add(product) + else: + self.product_manager.update(product, self.edit_item_id) + self.edit_item_id = None + + self.page.close(self.add_product_dialog) + self.set_popup_to_default() + + print('Update list') + self._all_products = self.product_manager.get_all() + self.products_list.controls=self.create_list(self._all_products, self.edit_product, self.delete_product) + self.products_list.update() + + def on_cancel_btn_click(self, e): + self.page.close(self.add_product_dialog) + self.set_popup_to_default() + if self.edit_item_id != None: + self.edit_item_id = None + + def set_popup_to_default(self): + self.name.value = '' + self.name.update() + self.description.value = '' + self.description.update() + self.details.value = '' + self.details.update() + self.price.value = '' + self.price.update() + self.discount.value = '' + self.discount.update() + self.quantity.value = '' + self.quantity.update() + self.product_image.src = 'images/placeholder.png' + self.product_image.update() + self.aviability.value = None + self.aviability.update() + self.category.value = None + self.category.update() + + + def get_categories(self): + categories = self.category_manager.get_categories() + return [ + ft.dropdown.Option(key=cat['id'], text=cat['name']) + for cat in categories + ] + + def create_list(self, items, on_click_handler, on_click_handler2): + """Helper to create list items for a column.""" + return [ + ft.Container( + content=ft.Row( + [ + ft.Column( + [ + ft.Text(value=item['name'], weight=ft.FontWeight.BOLD, size=15), + ft.Text(value=f"Pret: {item['price']}", size=12) + ] + ), + ft.Row( + [ + ft.IconButton( + icon=ft.Icons.EDIT, + on_click=lambda e, id=item: on_click_handler(id), + ), + ft.IconButton( + icon = ft.Icons.DELETE, + on_click=lambda e, id=item['id']: on_click_handler2(id), + icon_color=ft.Colors.RED + ) + ] + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + width=300, + bgcolor=ft.Colors.BROWN_50, + padding=10, + border_radius=8, + border = ft.border.all(1, ft.Colors.GREY), + ) + for item in items + ] + + def edit_product(self, item): + self.edit_item_id = item['id'] + self.name.value = item['name'] + self.description.value = item['description'] + self.details.value = item['details'] + self.price.value = item['price'] + self.discount.value = item['discount'] + self.quantity.value = item['quantity'] + self.product_image.src = item['image'] + self.aviability.value = item['aviability'] + self.category.value = item['category_id'] + self.page.open(self.add_product_dialog) + + def delete_product(self, id): + self.delete_item_id = id + self.page.open(self.confirm_delete_alert) + + def on_delete_product_click(self, e): + self.product_manager.delete(self.delete_item_id ) + self.delete_item_id = None + self.page.close(self.confirm_delete_alert) + print('Update list') + self._all_products = self.product_manager.get_all() + self.products_list.controls=self.create_list(self._all_products, self.edit_product, self.delete_product) + self.products_list.update() + + def on_delete_cancel_btn_click(self, e): + self.delete_item_id = None + self.page.close(self.confirm_delete_alert) + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Produse", size=18, weight=ft.FontWeight.BOLD), + ft.Button("Adauga", icon=ft.Icons.ADD, on_click=self.on_add_btn_click), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + self.products_list + ], + alignment=ft.MainAxisAlignment.START, + expand=True + ), + expand=True, + ) \ No newline at end of file diff --git a/UI_V2/assets/.DS_Store b/UI_V2/assets/.DS_Store new file mode 100644 index 0000000..0abe2e4 Binary files /dev/null and b/UI_V2/assets/.DS_Store differ diff --git a/UI_V2/assets/favicon.png b/UI_V2/assets/favicon.png new file mode 100644 index 0000000..cadb931 Binary files /dev/null and b/UI_V2/assets/favicon.png differ diff --git a/UI_V2/assets/icons/loading-animation.png b/UI_V2/assets/icons/loading-animation.png new file mode 100644 index 0000000..cadb931 Binary files /dev/null and b/UI_V2/assets/icons/loading-animation.png differ diff --git a/UI_V2/assets/images/banner.jpg b/UI_V2/assets/images/banner.jpg new file mode 100644 index 0000000..f67958b Binary files /dev/null and b/UI_V2/assets/images/banner.jpg differ diff --git a/UI_V2/assets/images/banner.png b/UI_V2/assets/images/banner.png new file mode 100644 index 0000000..f67958b Binary files /dev/null and b/UI_V2/assets/images/banner.png differ diff --git a/UI_V2/assets/images/banner_placeholder.png b/UI_V2/assets/images/banner_placeholder.png new file mode 100644 index 0000000..2594a2a Binary files /dev/null and b/UI_V2/assets/images/banner_placeholder.png differ diff --git a/UI_V2/assets/images/placeholder.png b/UI_V2/assets/images/placeholder.png new file mode 100644 index 0000000..a362deb Binary files /dev/null and b/UI_V2/assets/images/placeholder.png differ diff --git a/UI_V2/assets/images/tainagustului.png b/UI_V2/assets/images/tainagustului.png new file mode 100644 index 0000000..cadb931 Binary files /dev/null and b/UI_V2/assets/images/tainagustului.png differ diff --git a/UI_V2/assets/images/tainagustului_white.png b/UI_V2/assets/images/tainagustului_white.png new file mode 100644 index 0000000..ba332bd Binary files /dev/null and b/UI_V2/assets/images/tainagustului_white.png differ diff --git a/UI_V2/create_super_user.py b/UI_V2/create_super_user.py new file mode 100644 index 0000000..dab953d --- /dev/null +++ b/UI_V2/create_super_user.py @@ -0,0 +1,31 @@ +from dbActions.users import Users +import hashlib +import os + +def create_super_user(): + users_manager = Users() + + email = os.getenv('SUPERUSER_EMAIL', '').strip() + password = os.getenv('SUPERUSER_PASSWORD', '').strip() + role = 'admin' + + if not email: + raise ValueError("SUPERUSER_EMAIL is not set or empty") + if not password: + raise ValueError("SUPERUSER_PASSWORD is not set or empty") + + # check if user already exists + existing_user = users_manager.get_user(email) + if existing_user: + print(f"Super user '{email}' already exists. Skipping creation.") + return + + passwd_hash = hashlib.md5(password.encode('utf-8')).hexdigest() + users_manager.add_user(email, passwd_hash, role) + print(f"Super user created: {email} (role={role})") + +if __name__ == "__main__": + try: + create_super_user() + except Exception as e: + print(f"Failed to create super user: {e}") \ No newline at end of file diff --git a/UI_V2/dbActions/__pycache__/categories.cpython-313.pyc b/UI_V2/dbActions/__pycache__/categories.cpython-313.pyc new file mode 100644 index 0000000..99160f6 Binary files /dev/null and b/UI_V2/dbActions/__pycache__/categories.cpython-313.pyc differ diff --git a/UI_V2/dbActions/__pycache__/company.cpython-313.pyc b/UI_V2/dbActions/__pycache__/company.cpython-313.pyc new file mode 100644 index 0000000..eeb8227 Binary files /dev/null and b/UI_V2/dbActions/__pycache__/company.cpython-313.pyc differ diff --git a/UI_V2/dbActions/__pycache__/orders.cpython-313.pyc b/UI_V2/dbActions/__pycache__/orders.cpython-313.pyc new file mode 100644 index 0000000..54ac837 Binary files /dev/null and b/UI_V2/dbActions/__pycache__/orders.cpython-313.pyc differ diff --git a/UI_V2/dbActions/__pycache__/products.cpython-313.pyc b/UI_V2/dbActions/__pycache__/products.cpython-313.pyc new file mode 100644 index 0000000..cef3dbc Binary files /dev/null and b/UI_V2/dbActions/__pycache__/products.cpython-313.pyc differ diff --git a/UI_V2/dbActions/__pycache__/users.cpython-313.pyc b/UI_V2/dbActions/__pycache__/users.cpython-313.pyc new file mode 100644 index 0000000..1d266d7 Binary files /dev/null and b/UI_V2/dbActions/__pycache__/users.cpython-313.pyc differ diff --git a/UI_V2/dbActions/categories.py b/UI_V2/dbActions/categories.py new file mode 100644 index 0000000..5ac9fa0 --- /dev/null +++ b/UI_V2/dbActions/categories.py @@ -0,0 +1,94 @@ +import sqlite3 +from typing import Optional + +class Categories: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_categories_table() + + def _create_categories_table(self): + """Create the users table if it doesn't already exist.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + image TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL DEFAULT 'active' + ); + + """) + conn.commit() + + def add(self, name, image: str) -> bool: + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO categories (name, image) + VALUES (?, ?) + """, (name, image)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def get(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM categories + WHERE id = ? + """, (id,)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "name": row[1], + "image": row[2], + "created_at": row[3], + "status":row[4], + } + else: + return None + + def get_categories(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM categories; + """) + rows = cursor.fetchall() + if rows: + buffer = [] + for row in rows: + r = { + "id": row[0], + "name": row[1], + "image": row[2], + "created_at": row[3], + "status":row[4], + } + buffer.append(r) + return buffer + else: + return [] + + def update(self, name, image, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE categories SET name = ?, image = ? + WHERE id = ? + ''', (name, image, id)) + conn.commit() + + def delete(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + DELETE FROM categories WHERE id=?; + ''', (id,)) + conn.commit() \ No newline at end of file diff --git a/UI_V2/dbActions/company.py b/UI_V2/dbActions/company.py new file mode 100644 index 0000000..7227f2c --- /dev/null +++ b/UI_V2/dbActions/company.py @@ -0,0 +1,97 @@ +import sqlite3 +from typing import Optional + +class Company: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_company_table() + + def _create_company_table(self): + """Create the company table if it doesn't already exist.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS company ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + name TEXT, + vat TEXT, + register_number TEXT, + address TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL DEFAULT 'active' + ); + + """) + conn.commit() + + def add_company(self, company): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO company (user_id, name, vat, register_number, address) + VALUES (?, ?, ?, ?, ?) + """, ( + company['user_id'], + company['name'], + company['vat'], + company['register_number'], + company['address'] + ) + ) + conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def get_company(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM company + WHERE user_id = ? + """, (user_id,)) + row = cursor.fetchone() + if row: + return { + 'id': row[0], + 'user_id': row[1], + 'name': row[2], + 'vat': row[3], + 'register_number': row[4], + 'address': row[5], + 'created_at': row[6], + 'status': row[7], + } + return None + + def update_company(self, company): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE company SET name = ?, vat = ?, register_number = ?, address = ? + WHERE id = ? + ''', (company['name'], + company['vat'], + company['register_number'], + company['address'], + company['id'])) + conn.commit() + + def delete(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + DELETE FROM company WHERE id=?; + ''', (id,)) + conn.commit() + + def deactivate(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE company SET status = ? + WHERE id = ? + ''', ('inactive', id)) + conn.commit() \ No newline at end of file diff --git a/UI_V2/dbActions/orders.py b/UI_V2/dbActions/orders.py new file mode 100644 index 0000000..6247371 --- /dev/null +++ b/UI_V2/dbActions/orders.py @@ -0,0 +1,163 @@ +import sqlite3 +from typing import Optional + +class Orders: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_orders_table() + self._create_orders_map_table() + + def _create_orders_table(self): + """Create the orders table if it doesn't already exist.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'on_hold' + ); + """) + conn.commit() + + def _create_orders_map_table(self): + """Create the orders table if it doesn't already exist.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS orders_products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id TEXT, + orders_id TEXT, + quantity INTEGER + ); + + """) + conn.commit() + + def add_order(self, user_id): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO orders (user_id) + VALUES (?) + """, (user_id,)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def add_product_to_order(self, product_id, orders_id, quantity): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO orders_products (product_id, orders_id, quantity) + VALUES (?,?,?) + """, (product_id, orders_id, quantity)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def get_on_hold_order(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM orders + WHERE user_id = ? and status = 'on_hold' + """, (user_id,)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "user_id": row[1], + "status": row[2], + } + return None + + def update_order_status(self, status, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE orders SET status = ? + WHERE id = ? + ''', (status, id)) + conn.commit() + + def get_order_products(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM orders_products + WHERE orders_id = ? + """, (id,)) + rows = cursor.fetchall() + result = [] + if rows: + for row in rows: + buffer = { + "id": row[0], + "prdouct_id": row[1], + "orders_id": row[2], + "quantity": row[3] + } + result.append(buffer) + return result + return [] + + def update_order_map_quantity(self, id, quantity): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE orders_products SET quantity = ? + WHERE id = ? + ''', (quantity, id)) + conn.commit() + + def get_orders(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM orders + """) + rows = cursor.fetchall() + if rows: + buffer = [] + for row in rows: + r = { + "id": row[0], + "user_id": row[1], + "status": row[2], + } + buffer.append(r) + return buffer + return [] + + def get_orders_for_user(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM orders WHERE user_id = ? + """, (user_id,)) + rows = cursor.fetchall() + if rows: + buffer = [] + for row in rows: + r = { + "id": row[0], + "user_id": row[1], + "status": row[2], + } + buffer.append(r) + return buffer + return [] + + def remove_product_from_order(self, order_id, product_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + DELETE FROM orders_products WHERE orders_id=? and product_id=?; + ''', (order_id, product_id)) + conn.commit() \ No newline at end of file diff --git a/UI_V2/dbActions/products.py b/UI_V2/dbActions/products.py new file mode 100644 index 0000000..b837822 --- /dev/null +++ b/UI_V2/dbActions/products.py @@ -0,0 +1,140 @@ +import sqlite3 +from typing import Optional + +class Products: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_products_table() + + def _create_products_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT NOT NULL, + details TEXT NOT NULL, + price REAL, + discount REAL, + quantity REAL, + aviability TEXT NOT NULL, + category_id INTEGER, + image TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + """) + conn.commit() + + def add(self, product) -> bool: + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO products (name, description, details, price, discount, quantity, aviability, category_id, image) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (product['name'], product['description'],product['details'],product['price'], product['discount'], product['quantity'], product['aviability'], product['category_id'], product['image'])) + conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def get(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM products + WHERE id = ? + """, (id,)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "name": row[1], + "description": row[2], + "details":row[3], + "price": row[4], + "discount": row[5], + "quantity": row[6], + "aviability": row[7], + "category_id": row[8], + "image":row[9], + "created_at": row[10] + } + else: + return None + + def get_all(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM products + """) + rows = cursor.fetchall() + if rows: + products = [] + for row in rows: + product={ + "id": row[0], + "name": row[1], + "description": row[2], + "details":row[3], + "price": row[4], + "discount": row[5], + "quantity": row[6], + "aviability": row[7], + "category_id": row[8], + "image":row[9], + "created_at": row[10] + } + products.append(product) + return products + else: + return [] + + def get_all_by_category(self, category_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM products + WHERE category_id = ? + """, (category_id,)) + rows = cursor.fetchall() + if rows: + products = [] + for row in rows: + product={ + "id": row[0], + "name": row[1], + "description": row[2], + "details":row[3], + "price": row[4], + "discount": row[5], + "quantity": row[6], + "aviability": row[7], + "category_id": row[8], + "image": row[9], + "created_at": row[10], + } + products.append(product) + return products + else: + return [] + + def update(self, product, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE products SET name = ?, description =?, details=?, price=?, discount=?, quantity=?, aviability=?, category_id=?, image=? + WHERE id = ? + ''', (product['name'], product['description'], product['details'], product['price'], product['discount'], product['quantity'], product['aviability'], product['category_id'], product['image'] , id)) + conn.commit() + + def delete(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + DELETE FROM products WHERE id=?; + ''', (id,)) + conn.commit() \ No newline at end of file diff --git a/UI_V2/dbActions/users.py b/UI_V2/dbActions/users.py new file mode 100644 index 0000000..588db64 --- /dev/null +++ b/UI_V2/dbActions/users.py @@ -0,0 +1,206 @@ +import sqlite3 +from typing import Optional +import hashlib + +class Users: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_users_table() + + def _create_users_table(self): + """Create the users table if it doesn't already exist.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password TEXT, + token TEXT, + name TEXT, + phone TEXT, + address TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL DEFAULT 'active', + role TEXT DEFAULT 'client' + ); + + """) + conn.commit() + + def hash_password(self, password: str) -> bytes: + return hashlib.md5(password.encode('utf-8')).hexdigest() + + def add_user(self, email, passwd, role): + """Register a new user.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO users (email, password, role) + VALUES (?, ?, ?) + """, (email, passwd, role)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False # Username already exist + + def invite_user(self, email, name, phone, address, role='invited'): + #try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO users (email, name, phone, address, role) + VALUES (?, ?, ?, ?, ?) + """, (email, name, phone, address, role)) + conn.commit() + return cursor.lastrowid + #except sqlite3.IntegrityError: + # return None + + def register_user(self, email: str, password: str) -> bool: + """Register a new user.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO users (email, password) + VALUES (?, ?) + """, (email, password)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False # Username already exist + + def authenticate_user(self, email: str, password: str) -> bool: + """Authenticate a user.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM users + WHERE email = ? AND password = ? + """, (email, password)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "email": row[1], + "token": row[3], + "name":row[4], + "phone": row[5], + "address": row[6], + "created_at": row[7], + "status": row[8], + "role":row[9] + } + else: + return None + + def get_user(self, email: str) -> Optional[dict]: + """Retrieve user details by username.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM users + WHERE email = ? + """, (email,)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "email": row[1], + "token": row[3], + "name":row[4], + "phone": row[5], + "address": row[6], + "created_at": row[7], + "status": row[8], + "role":row[9] + } + return None + + def get(self, id: int) -> Optional[dict]: + """Retrieve user details by username.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM users + WHERE id = ? + """, (id,)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "email": row[1], + "token": row[3], + "name":row[4], + "phone": row[5], + "address": row[6], + "created_at": row[7], + "status": row[8], + "role":row[9] + } + return None + + def get_all(self) -> Optional[dict]: + """Retrieve user details by username.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM users + """) + rows = cursor.fetchall() + if rows: + buffer = [] + for row in rows: + buffer.append({ + "id": row[0], + "email": row[1], + "token": row[3], + "name":row[4], + "phone": row[5], + "address": row[6], + "created_at": row[7], + "status": row[8], + "role":row[9] + }) + return buffer + return [] + + def update_password(self, email, passwd): + '''Update user password''' + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE users SET password = ? + WHERE email = ? + ''', (passwd, email)) + conn.commit() + + def update_token(self, id, token): + '''Update user token''' + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE users SET token = ? + WHERE id = ? + ''', (token, id)) + conn.commit() + + def update_user_data(self, name, phone, address, id): + '''Update user data''' + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE users SET name = ?, phone = ?, address = ? + WHERE id = ? + ''', (name, phone, address, id)) + conn.commit() + + def delete(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + DELETE FROM users WHERE id=?; + ''', (id,)) + conn.commit() \ No newline at end of file diff --git a/UI_V2/docker_compose.yml b/UI_V2/docker_compose.yml new file mode 100644 index 0000000..189f726 --- /dev/null +++ b/UI_V2/docker_compose.yml @@ -0,0 +1,32 @@ +version: "3.9" + +networks: + reverse-proxy: + external: true + +volumes: + tg_instance: + tg_assets: + +services: + tainagustului: + build: . + container_name: tainagustului + restart: unless-stopped + environment: + TZ: "Europe/Bucharest" + FLET_PORT: "8080" + VIRTUAL_HOST: "tainagustului.ro,www.tainagustului.ro" + VIRTUAL_PORT: "8080" + LETSENCRYPT_HOST: "tainagustului.ro,www.tainagustului.ro" + LETSENCRYPT_EMAIL: "macamete.robert@gmail.com" + # If your app reads DB path from env, uncomment one of these: + # DATABASE_URL: "sqlite:////app/instance/shop.db" # SQLAlchemy style + # SHOP_DB_PATH: "/app/instance/shop.db" # custom var + expose: + - "8080" + volumes: + - tg_instance:/app/instance + - tg_assets:/app/assets + networks: + - reverse-proxy \ No newline at end of file diff --git a/UI_V2/helpers/__pycache__/default_user.cpython-313.pyc b/UI_V2/helpers/__pycache__/default_user.cpython-313.pyc new file mode 100644 index 0000000..fe19a1d Binary files /dev/null and b/UI_V2/helpers/__pycache__/default_user.cpython-313.pyc differ diff --git a/UI_V2/helpers/__pycache__/emails.cpython-313.pyc b/UI_V2/helpers/__pycache__/emails.cpython-313.pyc new file mode 100644 index 0000000..449e6bc Binary files /dev/null and b/UI_V2/helpers/__pycache__/emails.cpython-313.pyc differ diff --git a/UI_V2/helpers/default_user.py b/UI_V2/helpers/default_user.py new file mode 100644 index 0000000..ca95937 --- /dev/null +++ b/UI_V2/helpers/default_user.py @@ -0,0 +1,16 @@ +import flet as ft + +class DefaultUser: + def __init__(self, page: ft.Page): + self.page = page + self.default_user = { + 'id':self.page.session_id, + 'email':f'user_{self.page.session_id}@default.com', + 'name': 'Default User', + 'phone': None, + 'address': None, + 'created_at': "None", + 'status':'active', + 'role': 'default_user' + } + \ No newline at end of file diff --git a/UI_V2/helpers/emails.py b/UI_V2/helpers/emails.py new file mode 100644 index 0000000..86a5c77 --- /dev/null +++ b/UI_V2/helpers/emails.py @@ -0,0 +1,139 @@ +import smtplib +from email.message import EmailMessage +import os + +def send_email(to_email, subject, body): + smtp_host = os.environ.get("SMTP_HOST") + smtp_port = int(os.environ.get("SMTP_PORT", 587)) + smtp_user = os.environ.get("SMTP_USER") + smtp_pass = os.environ.get("SMTP_PASS") + sender_email = os.environ.get("SMTP_FROM", smtp_user) + + if not all([smtp_host, smtp_port, smtp_user, smtp_pass]): + raise ValueError("SMTP config incomplete in environment variables.") + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = to_email + msg.set_content(body) + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg) + + +# Send email with attachment +def send_email_with_attachment(to_email, subject, body, attachment_path): + smtp_host = os.environ.get("SMTP_HOST") + smtp_port = int(os.environ.get("SMTP_PORT", 587)) + smtp_user = os.environ.get("SMTP_USER") + smtp_pass = os.environ.get("SMTP_PASS") + sender_email = os.environ.get("SMTP_FROM", smtp_user) + + if not all([smtp_host, smtp_port, smtp_user, smtp_pass]): + raise ValueError("SMTP config incomplete in environment variables.") + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = to_email + msg.set_content(body) + + if attachment_path and os.path.isfile(attachment_path): + with open(attachment_path, "rb") as f: + file_data = f.read() + file_name = os.path.basename(attachment_path) + msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name) + else: + raise FileNotFoundError(f"Attachment file not found: {attachment_path}") + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg) + + +# Send email using Gmail directly +def send_gmail(to_email, subject, body): + smtp_host = "smtp.gmail.com" + smtp_port = 587 + smtp_user = 'macamete.robert@gmail.com' + smtp_pass = 'advx yqlv jkaa czvr' + sender_email = 'macamete.robert@gmail.com' + + if not all([smtp_user, smtp_pass]): + raise ValueError("GMAIL_USER and GMAIL_PASS must be set in environment variables.") + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = to_email + msg.set_content(body) + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg) + + +# Send email with attachment using Gmail directly +def send_gmail_with_attachment(to_email, subject, body, attachment_path): + smtp_host = "smtp.gmail.com" + smtp_port = 587 + smtp_user = 'macamete.robert@gmail.com' + smtp_pass = 'advx yqlv jkaa czvr' + sender_email = 'macamete.robert@gmail.com' + + if not all([smtp_user, smtp_pass]): + raise ValueError("GMAIL_USER and GMAIL_PASS must be set in environment variables.") + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = to_email + msg.set_content(body) + + if attachment_path and os.path.isfile(attachment_path): + with open(attachment_path, "rb") as f: + file_data = f.read() + file_name = os.path.basename(attachment_path) + msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name) + else: + raise FileNotFoundError(f"Attachment file not found: {attachment_path}") + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg) + +# Send email with attachment +def send_custom_email_with_attachment(to_email, subject, body, attachment_path, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS): + smtp_host = SMTP_HOST + smtp_port = int(SMTP_PORT) + smtp_user = SMTP_USER + smtp_pass = SMTP_PASS + sender_email = smtp_user + + if not all([smtp_host, smtp_port, smtp_user, smtp_pass]): + raise ValueError("SMTP config incomplete in environment variables.") + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = to_email + msg.set_content(body) + + if attachment_path and os.path.isfile(attachment_path): + with open(attachment_path, "rb") as f: + file_data = f.read() + file_name = os.path.basename(attachment_path) + msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name) + else: + raise FileNotFoundError(f"Attachment file not found: {attachment_path}") + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg) \ No newline at end of file diff --git a/UI_V2/instance/app_database.db b/UI_V2/instance/app_database.db new file mode 100644 index 0000000..b64951b Binary files /dev/null and b/UI_V2/instance/app_database.db differ diff --git a/UI_V2/main.py b/UI_V2/main.py new file mode 100644 index 0000000..08ab2db --- /dev/null +++ b/UI_V2/main.py @@ -0,0 +1,90 @@ +import flet as ft +from pages.auth.auth import Auth +from pages.home.home import Home +from admin.dashboard import Dashboard +from pages.categories.category import Category +from pages.products.product import ProductPage +from pages.profile.profilepage import ProfilePage +from pages.shopping_cart.cart import Cart +from pages.shopping_cart.peload_card import PreloadCard + +import os +os.environ["FLET_SECRET_KEY"] = os.urandom(12).hex() + +def main(page: ft.Page): + page.title = "Taina Gustului" + page.theme_mode = ft.ThemeMode.LIGHT + page.theme = ft.Theme(color_scheme=ft.ColorScheme(primary=ft.Colors.BROWN)) + page.vertical_alignment = ft.MainAxisAlignment.CENTER + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + page.padding = 0 + + def route_change(route): + page.controls.clear() + + if route == "/auth": + login = Auth(page) + page.add(login.build()) + page.update() + return + + if route in ("/home", "/", None): + home = Home(page) + page.add(home.build()) + page.update() + return + + if route == "/admin": + if not page.client_storage.get("is_authenticated"): + page.go("/auth") + return + dashbaord = Dashboard(page) + page.add(dashbaord.build()) + page.update() + return + + if 'categorie' in route: + category = Category(page) + page.add(category.build()) + page.update() + return + + if 'produs' in route: + produs = ProductPage(page) + page.add(produs.build()) + page.update() + return + + if route == "/profil": + profile = ProfilePage(page) + page.add(profile.build()) + page.update() + return + + if route == "/cos": + cart = Cart(page) + page.add(cart.build()) + page.update() + return + + if route == "/pre_load_cos": + preload = PreloadCard(page) + page.add(preload.build()) + page.update() + return + + # 5) Fallback 404 + page.add(ft.Text("404: Page not found")) + page.update() + + page.on_route_change = lambda _: route_change(page.route) + page.go("/") + +ft.app( + target=main, + assets_dir="assets", + upload_dir="uploads", + view=ft.WEB_BROWSER, + port=8080, + host="0.0.0.0" +) \ No newline at end of file diff --git a/UI_V2/pages/auth/__pycache__/auth.cpython-313.pyc b/UI_V2/pages/auth/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..0a75455 Binary files /dev/null and b/UI_V2/pages/auth/__pycache__/auth.cpython-313.pyc differ diff --git a/UI_V2/pages/auth/__pycache__/forgot_password.cpython-313.pyc b/UI_V2/pages/auth/__pycache__/forgot_password.cpython-313.pyc new file mode 100644 index 0000000..a44dd42 Binary files /dev/null and b/UI_V2/pages/auth/__pycache__/forgot_password.cpython-313.pyc differ diff --git a/UI_V2/pages/auth/__pycache__/login.cpython-313.pyc b/UI_V2/pages/auth/__pycache__/login.cpython-313.pyc new file mode 100644 index 0000000..858c39e Binary files /dev/null and b/UI_V2/pages/auth/__pycache__/login.cpython-313.pyc differ diff --git a/UI_V2/pages/auth/__pycache__/register.cpython-313.pyc b/UI_V2/pages/auth/__pycache__/register.cpython-313.pyc new file mode 100644 index 0000000..b2f65cf Binary files /dev/null and b/UI_V2/pages/auth/__pycache__/register.cpython-313.pyc differ diff --git a/UI_V2/pages/auth/auth.py b/UI_V2/pages/auth/auth.py new file mode 100644 index 0000000..fbf4e73 --- /dev/null +++ b/UI_V2/pages/auth/auth.py @@ -0,0 +1,34 @@ +import flet as ft +from pages.auth.login import Login +from pages.auth.register import Register + +class Auth: + def __init__(self, page: ft.Page, go_to = 'login'): + self.page = page + self.go_to = go_to + self.logo = ft.Image("images/tainagustului.png", width=200) + self.login = Login(self.page, self) + self.register = Register(self.page, self, self.login) + + go_to = 'login' if self.page.session.get('go_to') == None or 'login' else self.page.session.get('go_to') + if go_to == 'login': + self.placeholder = ft.Container( + content=self.login.build(), + width=350 + ) + if go_to == 'register': + self.placeholder = ft.Container( + content=self.register.build(), + width=350 + ) + + def build(self): + return ft.Container( + content=ft.Column( + [ + self.logo, + self.placeholder, + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ) + ) \ No newline at end of file diff --git a/UI_V2/pages/auth/forgot_password.py b/UI_V2/pages/auth/forgot_password.py new file mode 100644 index 0000000..a69fb8a --- /dev/null +++ b/UI_V2/pages/auth/forgot_password.py @@ -0,0 +1,159 @@ +import flet as ft +from dbActions.users import Users +from helpers.emails import send_gmail +import re +import string +import secrets + +class ForgotPassword: + def __init__(self, page: ft.Page, auth, login): + self.page = page + self.auth = auth + self.login = login + self.email = ft.TextField(label="E-mail") + self.password = ft.TextField(label="Parola", password=True, can_reveal_password=True) + self.repeat_password = ft.TextField(label="Repeta Parola", password=True, can_reveal_password=True) + self.inserted_code = ft.TextField(label="Inserati codul primit prin e-mail") + self.error_message = ft.Text("", color=ft.Colors.RED) + self.placeholder = ft.Column([], horizontal_alignment=ft.CrossAxisAlignment.CENTER) + self.otp_code = self._generate_numeric_code() + self.user_manager = Users() + self.trimite_code_placeholder = ft.Column( + [ + self.email, + ft.Button("Trimite cod", on_click=self.send_code_on_email, width=150), + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ) + self.code_placeholder = ft.Column([], horizontal_alignment=ft.CrossAxisAlignment.CENTER) + + def _generate_numeric_code(self) -> str: + digits = string.digits + return ''.join(secrets.choice(digits) for _ in range(6)) + + def send_code_on_email(self, e): + if self._is_valid_email(self.email.value): + self.trimite_code_placeholder.controls = [] + self.trimite_code_placeholder.update() + self.code_placeholder.controls.clear() + self.code_placeholder.controls.append(self.inserted_code) + self.code_placeholder.controls.append(ft.Button("Verifica", width=150, on_click=self.verfy_code)) + self.code_placeholder.update() + print(self.otp_code) + # send_gmail( + # to_email=self.email.value, + # subject="Codul de verificare", + # body=f"Codul de verificare este: {self.otp_code}" + # ) + + + def _are_all_fields_inserted(self, password=None, repeat_password=None): + valid = True + self.error_message.value = '' + if not password: + valid = False + if not repeat_password: + valid = False + if not valid: + self.error_message.value = "All fields are required!" + self.error_message.update() + return valid + + def _check_repeat_password(self, password, confirm_password): + if password == confirm_password: + self.error_message.value = "" + self.error_message.update() + return True + else: + self.error_message.value = "The passwords don't match!" + self.error_message.update() + return False + + def _is_password_strong(self, password): + self.error_message.value = "" + if len(password) < 8: + self.error_message.value = "Password must be at least 8 characters long!" + self.error_message.update() + return False + if not re.search(r"[A-Z]", password): + self.error_message.value = "Password must contain at least one uppercase letter!" + self.error_message.update() + return False + if not re.search(r"[a-z]", password): + self.error_message.value = "Password must contain at least one lowercase letter!" + self.error_message.update() + return False + if not re.search(r"[0-9]", password): + self.error_message.value = "Password must contain at least one digit!" + self.error_message.update() + return False + if not re.search(r"[^a-zA-Z0-9]", password): + self.error_message.value = "Password must contain at least one special character!" + self.error_message.update() + return False + self.error_message.update() + return True + + def on_save_btn_click(self, e): + password = self.password.value + repeat_password = self.repeat_password.value + if self._are_all_fields_inserted(password, repeat_password): + if self._is_password_strong(password): + if self._check_repeat_password(password, repeat_password): + print(self.email.value) + print(password) + if self.user_manager.update_password(self.email.value, password): + self.error_message.value = "Parola a fost salvata cu success." + self.error_message.color = ft.Colors.GREEN + self.error_message.update() + else: + self.error_message.value = "Nu am gasit un cont valid cu aceasta adresa de email." + self.error_message.update() + + def verfy_code(self, e): + self.code_placeholder.controls = [] + self.code_placeholder.update() + inserted_code = self.inserted_code.value + if inserted_code == self.otp_code: + self.placeholder.controls.clear() + self.placeholder.controls.append(self.password) + self.placeholder.controls.append(self.repeat_password) + self.placeholder.controls.append( + ft.Button("Salveaza", width=150, on_click=self.on_save_btn_click) + ) + self.placeholder.update() + + else: + print(inserted_code) + print(self.otp_code) + + def on_back_btn_click(self, e): + self.auth.placeholder.content.clean() + self.auth.placeholder.content = self.login.build() + self.auth.placeholder.update() + + def _is_valid_email(self, email: str) -> bool: + email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if re.fullmatch(email_regex, email) is not None: + self.error_message.value = "" + self.error_message.update() + return True + else: + self.error_message.value = "Va rugam inserati o adresa de e-mail valida!" + self.error_message.update() + return False + + def build(self): + return ft.Container( + content=ft.Column( + [ + self.trimite_code_placeholder, + self.code_placeholder, + self.placeholder, + self.error_message, + ft.TextButton("Inapoi la Autentificare.", on_click=self.on_back_btn_click) + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ) + ) \ No newline at end of file diff --git a/UI_V2/pages/auth/login.py b/UI_V2/pages/auth/login.py new file mode 100644 index 0000000..d498a04 --- /dev/null +++ b/UI_V2/pages/auth/login.py @@ -0,0 +1,68 @@ +import flet as ft +from pages.auth.register import Register +from dbActions.users import Users +from pages.auth.forgot_password import ForgotPassword + +class Login: + def __init__(self, page: ft.Page, auth): + self.page = page + self.auth = auth + self.user_manager = Users() + self.email = ft.TextField(label="E-mail") + self.password = ft.TextField(label="Parola", password=True, can_reveal_password=True) + self.error_message = ft.Text("", color=ft.Colors.RED) + self.register = Register(self.page, self.auth, self) + + def on_login_btn_click(self, e): + email = self.email.value + password = self.password.value + password_hash = self.user_manager.hash_password(password) + user = self.user_manager.authenticate_user(email, password_hash) + if user: + self.page.client_storage.set("is_authenticated", True) + self.page.session.set("user", user) + self.error_message.value = '' + self.error_message.update() + if user['role'] == 'admin': + self.page.go('/admin') + else: + if user['name'] is None or len(user['name'])<=1: + self.page.go("/profil") + else: + self.page.go('/') + else: + self.error_message.value = 'E-mail sau parola sunt gresite!' + self.error_message.update() + + def on_register_btn_click(self, e): + self.auth.placeholder.content = self.register.build() + self.auth.placeholder.update() + + def on_forgot_password_btn_click(self, e): + forgot_password = ForgotPassword(self.page, self.auth, self) + self.auth.placeholder.content.clean() + self.auth.placeholder.content = forgot_password.build() + self.auth.placeholder.update() + + def build(self): + return ft.Column( + [ + self.email, + self.password, + self.error_message, + ft.Row( + [ + ft.Button("Autentificare", width=200, on_click=self.on_login_btn_click) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text(), + ft.Row( + [ + ft.TextButton("Creaza cont", on_click=self.on_register_btn_click), + ft.TextButton("Ai uitat parola?", on_click=self.on_forgot_password_btn_click) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ) + ] + ) \ No newline at end of file diff --git a/UI_V2/pages/auth/register.py b/UI_V2/pages/auth/register.py new file mode 100644 index 0000000..061a384 --- /dev/null +++ b/UI_V2/pages/auth/register.py @@ -0,0 +1,123 @@ +import flet as ft +from dbActions.users import Users +import re +import time + +class Register: + def __init__(self, page: ft.Page, auth, login): + self.page = page + self.auth = auth + self.login = login + self.users_manager = Users() + self.email = ft.TextField(label="E-mail") + self.password = ft.TextField(label="Parola", password=True, can_reveal_password=True) + self.repeat_password = ft.TextField(label="Repeta parola", password=True, can_reveal_password=True) + self.error_message = ft.Text("", color=ft.Colors.RED) + + def on_login_btn_click(self, e): + self.auth.placeholder.content = self.login.build() + self.auth.placeholder.update() + + def _is_valid_email(self, email: str) -> bool: + email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if re.fullmatch(email_regex, email) is not None: + self.error_message.value = "" + self.error_message.update() + return True + else: + self.error_message.value = "Va rugam inserati un e-mail valid!" + self.error_message.update() + return False + + def _are_all_fields_inserted(self, email=None, password=None, repeat_password=None): + valid = True + self.error_message.value = '' + if not email: + valid = False + if not password: + valid = False + if not repeat_password: + valid = False + if not valid: + self.error_message.value = "Toate campurile sunt obligatori!" + self.error_message.update() + return valid + + def _check_repeat_password(self, password, confirm_password): + if password == confirm_password: + self.error_message.value = "" + self.error_message.update() + return True + else: + self.error_message.value = "Parolele nu se potrivesc!" + self.error_message.update() + return False + + def _is_password_strong(self, password): + self.error_message.value = "" + if len(password) < 8: + self.error_message.value = "Parola trebuie sa fie cel putin 8 caractere!" + self.error_message.update() + return False + if not re.search(r"[A-Z]", password): + self.error_message.value = "Parola trebuie sa contina cel putin o litera mare!" + self.error_message.update() + return False + if not re.search(r"[a-z]", password): + self.error_message.value = "Parola trebuie sa contina cel putin o litera mica!" + self.error_message.update() + return False + if not re.search(r"[0-9]", password): + self.error_message.value = "Parola trebuie sa contina cel putin un numar!" + self.error_message.update() + return False + if not re.search(r"[^a-zA-Z0-9]", password): + self.error_message.value = "Parola trebuie sa contina cel putin un caracter special (exp: !@#$%^&*)!" + self.error_message.update() + return False + self.error_message.update() + return True + + + def on_register_btn_click(self, e): + email = self.email.value + password = self.password.value + repeat_password = self.repeat_password.value + if self._are_all_fields_inserted(email, password, repeat_password): + print("All fileds are inserted") + if self._is_valid_email(email): + print("Email is valid") + if self._is_password_strong(password): + print('Password is string') + if self._check_repeat_password(password, repeat_password): + print("Password is valid!") + password_hash = self.users_manager.hash_password(password) + self.users_manager.register_user(email, password_hash) + self.error_message.value = "Inregistrarea a avut loc cu succes, va puteti autentifica!" + self.error_message.color = ft.Colors.GREEN + self.error_message.update() + time.sleep(3) + self.on_login_btn_click('') + + def build(self): + return ft.Column( + [ + self.email, + self.password, + self.repeat_password, + self.error_message, + ft.Row( + [ + ft.Button("Creaza cont", width=200, on_click=self.on_register_btn_click) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text(), + ft.Row( + [ + ft.TextButton("Inapoi la Autentificare", on_click=self.on_login_btn_click), + ], + alignment=ft.MainAxisAlignment.CENTER + ) + ] + ) \ No newline at end of file diff --git a/UI_V2/pages/categories/__pycache__/category.cpython-313.pyc b/UI_V2/pages/categories/__pycache__/category.cpython-313.pyc new file mode 100644 index 0000000..f1fc6cf Binary files /dev/null and b/UI_V2/pages/categories/__pycache__/category.cpython-313.pyc differ diff --git a/UI_V2/pages/categories/category.py b/UI_V2/pages/categories/category.py new file mode 100644 index 0000000..74d7197 --- /dev/null +++ b/UI_V2/pages/categories/category.py @@ -0,0 +1,156 @@ +import flet as ft +from dbActions.products import Products + +class Category: + def __init__(self, page: ft.Page): + self.page = page + self.products_manager = Products() + self.category = self.page.session.get("category") + + self.products_group = ft.GridView( + spacing=10, + runs_count=5, + max_extent=200, + child_aspect_ratio=1.0, + expand=True, + width=1000 + ) + self.products = self.products_manager.get_all_by_category(self.category['id']) + self.add_products(self.products) + self.searchbar = ft.TextField( + label="Cauta produsul aceasta categorie", + expand=True, + on_submit=self.on_search_btn_click + ) + + self.profile_placeholder = ft.Column() + self.profile_btn = ft.IconButton( + icon=ft.Icons.ACCOUNT_CIRCLE_OUTLINED, + on_click=self.on_profile_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ) + self.login_btn = ft.IconButton( + icon=ft.Icons.LOGIN, + on_click=self.on_login_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ) + if self.page.session.get("user") is not None and '@default.com' not in self.page.session.get("user")['email']: + self.profile_placeholder.controls.append(self.profile_btn) + else: + self.profile_placeholder.controls.append(self.login_btn) + + def on_login_btn_click(self, e): + self.page.go('/auth') + + def add_products(self, products): + for product in products: + self.new_price = ft.Text( + value=f"{float(product['price']) - float(product['price'])*float(product['discount'])/100}", + size=14 if product['discount'] != 0 else 12, + color=ft.Colors.RED if product['discount'] != 0 else ft.Colors.BLACK, + weight=ft.FontWeight.BOLD + ) + print(type(product['discount'])) + self.old_price = ft.Text( + value=f"{product['price']}" if product['discount'] != 0 else '', + size=12, + color=ft.Colors.GREY, + style=ft.TextStyle(decoration=ft.TextDecoration.LINE_THROUGH), + ) + card = ft.Card( + content=ft.Container( + content=ft.Column( + [ + ft.Image(src=product['image'], width=170, height=100, border_radius=10, fit=ft.ImageFit.COVER), + ft.Text(product['name'], weight=ft.FontWeight.BOLD), + ft.Row( + [ + ft.Text(f"Pret:", size=12), + self.old_price, + self.new_price, + ft.Text(f"lei/{product['quantity']}g", size=12), + ], + alignment=ft.MainAxisAlignment.CENTER, + spacing=4 + ) + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ), + ink=True, + on_click=lambda e, title=product: self.on_product_click(title), + padding=5 + ) + ) + self.products_group.controls.append(card) + + def on_search_btn_click(self, e): + search = self.searchbar.value + buffer = [] + for product in self.products: + if search.lower() in product['name'].lower(): + buffer.append(product) + self.products_group.controls.clear() + self.add_products(buffer) + self.products_group.update() + + def on_profile_btn_click(self, e): + self.page.go('/profil') + + def on_home_btn_click(self, e): + self.page.go('/') + + def on_product_click(self, product): + self.page.session.set("product", product) + name = product['name'].replace(" ", "-").lower() + self.page.go(f'/produs/{name}') + + def on_cart_btn_click(self, e): + self.page.go("/pre_load_cos") + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + self.searchbar, + ft.IconButton( + icon=ft.Icons.SEARCH, + on_click=self.on_search_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ), + ft.VerticalDivider(), + self.profile_placeholder, + ft.IconButton( + icon=ft.Icons.SHOPPING_CART_OUTLINED, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE, + on_click=self.on_cart_btn_click + ) + ], + width=1000 + ), + ft.Row( + [ + ft.IconButton(icon=ft.Icons.HOME, on_click=self.on_home_btn_click), + ft.Text( + value=self.category['name'], + weight=ft.FontWeight.BOLD, + size=18 + ), + ], + width=1000 + ), + self.products_group + ], + scroll=ft.ScrollMode.ADAPTIVE, + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ), + expand=True, + padding=10, + bgcolor=ft.Colors.WHITE + ) \ No newline at end of file diff --git a/UI_V2/pages/home/__pycache__/home.cpython-313.pyc b/UI_V2/pages/home/__pycache__/home.cpython-313.pyc new file mode 100644 index 0000000..3074f27 Binary files /dev/null and b/UI_V2/pages/home/__pycache__/home.cpython-313.pyc differ diff --git a/UI_V2/pages/home/home.py b/UI_V2/pages/home/home.py new file mode 100644 index 0000000..079e556 --- /dev/null +++ b/UI_V2/pages/home/home.py @@ -0,0 +1,216 @@ +import flet as ft +from dbActions.categories import Categories +from dbActions.products import Products + +class Home: + def __init__(self, page: ft.Page): + self.page = page + self.categories_manager = Categories() + self.header = ft.Row( + [ + #ft.Button("Acasa", icon=ft.Icons.HOME, on_click=self.on_acasa_btn_click) + ft.Text("Categori", weight=ft.FontWeight.BOLD, size=18) + ], + alignment=ft.MainAxisAlignment.CENTER, + width=1000 + ) + self.banner = ft.Image("images/banner.png", width=1000, height=350, fit=ft.ImageFit.COVER) + self.categories_group = ft.Row([], scroll=ft.ScrollMode.ADAPTIVE, expand=True) + self.add_categories() + self.products_manager = Products() + self.products_group = ft.GridView( + spacing=10, + runs_count=5, + max_extent=200, + child_aspect_ratio=1.0, + expand=True, + width=1000 + ) + self.products = self.products_manager.get_all() + self.add_products(self.products) + + self.searchbar = ft.TextField( + label="Cauta produsul in toate categoriile", + expand=True, + on_submit=self.on_search_btn_click + ) + + self.profile_placeholder = ft.Column() + self.profile_btn = ft.IconButton( + icon=ft.Icons.ACCOUNT_CIRCLE_OUTLINED, + on_click=self.on_profile_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ) + self.login_btn = ft.IconButton( + icon=ft.Icons.LOGIN, + on_click=self.on_login_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ) + + if self.page.session.get("user") is not None and '@default.com' not in self.page.session.get("user")['email']: + self.profile_placeholder.controls.append(self.profile_btn) + else: + self.profile_placeholder.controls.append(self.login_btn) + self.search_for = self.page.session.get("search_for") + if self.search_for: + self.searchbar.value = self.page.session.get("search_for") + search = self.searchbar.value + buffer = [] + for product in self.products: + if search.lower() in product['name'].lower(): + buffer.append(product) + self.products_group.controls.clear() + self.add_products(buffer) + self.page.session.set("search_for", None) + + def on_acasa_btn_click(self, e): + self.page.go('/') + + def on_login_btn_click(self, e): + self.page.go('/auth') + + def add_products(self, products): + for product in products: + self.new_price = ft.Text( + value=f"{round(float(product['price']) - float(product['price'])*float(product['discount'])/100,2)}", + size=14 if product['discount'] != 0 else 12, + color=ft.Colors.RED if product['discount'] != 0 else ft.Colors.BLACK, + weight=ft.FontWeight.BOLD + ) + print(type(product['discount'])) + self.old_price = ft.Text( + value=f"{product['price']}" if product['discount'] != 0 else '', + size=12, + color=ft.Colors.GREY, + style=ft.TextStyle(decoration=ft.TextDecoration.LINE_THROUGH), + ) + card = ft.Card( + content=ft.Container( + content=ft.Column( + [ + ft.Image(src=product['image'], width=170, height=100, border_radius=10, fit=ft.ImageFit.COVER), + ft.Text(product['name'], weight=ft.FontWeight.BOLD), + ft.Row( + [ + ft.Text(f"Pret:", size=12), + self.old_price, + self.new_price, + ft.Text(f"lei/{product['quantity']}g", size=12), + ], + alignment=ft.MainAxisAlignment.CENTER, + spacing=4 + ) + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ), + ink=True, + on_click=lambda e, title=product: self.on_product_click(title), + padding=5 + ) + ) + self.products_group.controls.append(card) + + def add_categories(self): + categories = self.categories_manager.get_categories() + for category in categories: + card = ft.Card( + content=ft.Container( + content=ft.Column( + [ + ft.Image( + src=category['image'], + width=100, + height = 80, + border_radius=10, + fit=ft.ImageFit.COVER), + ft.Text(category['name']) + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ink=True, + on_click=lambda e, title=category: self.on_category_click(title), + padding=5, + width=120, + height=120 + ) + ) + self.categories_group.controls.append(card) + + def on_category_click(self, cat): + name = cat['name'].replace(" ","-").lower() + self.page.session.set("category", cat) + self.page.go(f"/categorie/{name}") + + def on_product_click(self, product): + self.page.session.set("product", product) + name = product['name'].replace(" ", "-").lower() + self.page.go(f'/produs/{name}') + + def on_profile_btn_click(self, e): + self.page.go('/profil') + + def on_cart_btn_click(self, e): + self.page.go("/pre_load_cos") + + def on_search_btn_click(self, e): + search = self.searchbar.value + buffer = [] + for product in self.products: + if search.lower() in product['name'].lower(): + buffer.append(product) + self.products_group.controls.clear() + self.add_products(buffer) + self.products_group.update() + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + self.searchbar, + ft.IconButton( + icon=ft.Icons.SEARCH, + on_click=self.on_search_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ), + ft.VerticalDivider(), + self.profile_placeholder, + ft.IconButton( + icon=ft.Icons.SHOPPING_CART_OUTLINED, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE, + on_click = self.on_cart_btn_click + ) + ], + width=1000 + ), + self.banner, + ft.Column( + [ + self.header, + self.categories_group, + ft.Row( + [ + ft.Text("Produse Populare", size=18, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.CENTER, + width=1000 + ), + self.products_group + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + expand=True + ) + ], + scroll=ft.ScrollMode.ADAPTIVE, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ), + expand=True, + padding=10, + bgcolor=ft.Colors.WHITE + ) \ No newline at end of file diff --git a/UI_V2/pages/products/__pycache__/product.cpython-313.pyc b/UI_V2/pages/products/__pycache__/product.cpython-313.pyc new file mode 100644 index 0000000..4c6cdbd Binary files /dev/null and b/UI_V2/pages/products/__pycache__/product.cpython-313.pyc differ diff --git a/UI_V2/pages/products/product.py b/UI_V2/pages/products/product.py new file mode 100644 index 0000000..1987bbd --- /dev/null +++ b/UI_V2/pages/products/product.py @@ -0,0 +1,464 @@ +import flet as ft +from dbActions.orders import Orders +from dbActions.products import Products +from helpers.default_user import DefaultUser + +class ProductPage: + def __init__(self, page: ft.Page, shop=''): + self.page = page + self.shop = shop + self.orders = Orders() + self.product = self.page.session.get('product') + print(self.product) + + self.product_main_image = ft.Image( + src = self.product['image'], + width=300, + height=250, + border_radius=10, + fit=ft.ImageFit.COVER + ) + + self.product_name = ft.Text( + value = self.product['name'], + size=20, + weight=ft.FontWeight.BOLD + ) + self.old_price = ft.Text( + value=f"{self.product['price']} Lei" if self.product['discount'] > 0 else '', + size=12, + color=ft.Colors.GREY, + style=ft.TextStyle(decoration=ft.TextDecoration.LINE_THROUGH) + ) + self.price = ft.Text( + value=f"{round(self.product['price'] - self.product['price']*self.product['discount']/100, 2)} Lei", + size=17 if self.product['discount'] > 0 else None, + color=ft.Colors.RED if self.product['discount'] > 0 else None, + weight=ft.FontWeight.BOLD + ) + + self.description = ft.Text( + value=self.product['description'], + width=600 + ) + + self.details = ft.Text( + value=self.product['details'], + width=600 + ) + + self.quantity = ft.TextField(label="", value="1", width=60) + self.quantify_group = ft.Row( + [ + ft.IconButton(ft.Icons.ARROW_CIRCLE_LEFT, on_click=self.remove_quantity), + self.quantity, + ft.IconButton(ft.Icons.ARROW_CIRCLE_RIGHT, on_click=self.add_quantity), + ] + ) + + aviab = { + 'in_stock': "In stoc", + 'in_provider_stock': "In stoc la furnizor", + 'not_available': "Indisponibil" + } + + self.availability = ft.Text(aviab[self.product['aviability']]) + self.stock_quantity = ft.Text(self.product['quantity']) + + self.similar_products = ft.Row(width=1000, scroll=ft.ScrollMode.ADAPTIVE) + for sp in self.get_similar_porducts(): + product = ft.Card( + content=ft.Container( + content=ft.Column( + [ + ft.Stack( + [ + ft.Image( + src=f"images/{sp['image']}", + fit=ft.ImageFit.COVER, + repeat=ft.ImageRepeat.NO_REPEAT, + border_radius=ft.border_radius.all(5), + width=220, + height=220 + ), + + ft.Container( + content=ft.Column( + [ + ft.Text(f"{sp['name']}", size=12), + ft.Row( + [ + ft.Row( + [ + ft.Text( + f"{sp['price']} Lei/{sp['quantity']}" if sp['discount']>0 else '', + size=12, + color=ft.Colors.GREY, + style=ft.TextStyle(decoration=ft.TextDecoration.LINE_THROUGH) + ), + ft.Text( + f"{sp['price'] - sp['price']*sp['discount']/100} Lei/{sp['quantity']}", + size=14, + weight=ft.FontWeight.BOLD + ), + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.IconButton(ft.Icons.ADD_SHOPPING_CART, on_click=lambda e, i=sp: self.on_sp_add_to_cart_click(i)) + ], + width=200, + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ) + + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ), + bgcolor=ft.Colors.WHITE, + bottom=0, + border_radius=ft.border_radius.only(bottom_left=5, bottom_right=5), + padding=10 + ) + ], + ), + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + + ), + padding=10, + ink=True, + on_click=lambda e, i=sp: self.on_sp_product_click(i) + ), + width=250, + height=250, + ) + self.similar_products.controls.append(product) + + self.product_content_image = ft.Column( + [ + ft.Card( + content=ft.Container( + self.product_main_image, + padding=10 + ) + ) + ] + ) + self.product_content_details = ft.Column( + [ + + ft.Row( + [ + ft.Text("Pret:", weight=ft.FontWeight.BOLD), + self.old_price, + self.price + ] + ), + ft.Text("Descriere", weight=ft.FontWeight.BOLD), + self.description, + ft.Text("Detalii", weight=ft.FontWeight.BOLD), + self.details, + ft.Row( + [ + ft.Text("Disponibilitate", weight=ft.FontWeight.BOLD), + self.availability + ] + ), + ft.Row( + [ + ft.Text('Cantitate (g)', weight=ft.FontWeight.BOLD), + self.stock_quantity + ] + ), + ft.Row( + [ + self.quantify_group, + ft.FilledButton( + "Adauga in cos", + icon=ft.Icons.SHOPPING_CART, + width=150, + on_click= lambda e, i=self.product: self.on_add_to_cart_btn_click(i) + ) + ] + ) + ], + alignment=ft.MainAxisAlignment.START + ) + + self.desktop_container = ft.Row(alignment=ft.MainAxisAlignment.CENTER) + self.mobile_container = ft.Column() + + if self.page.width < 600: + self.mobile_container.controls.append(self.product_content_image) + self.mobile_container.controls.append(self.product_content_details) + else: + self.desktop_container.controls.append(self.product_content_image) + self.desktop_container.controls.append(self.product_content_details) + + self.confirm_dialog = ft.AlertDialog( + title=ft.Text('Adauga in cos?'), + content=ft.Column( + [ + ft.Icon(ft.Icons.SHOPPING_CART, size=100) + ], + height=100, + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ), + actions=[ + ft.FilledButton( + "Anuleaza", + bgcolor=ft.Colors.GREY, + on_click=self.on_cancel_btn_click), + ft.FilledButton( + "Confirma", + on_click=self.on_confirm_btn_click) + ] + ) + self.page.add(self.confirm_dialog) + + self.searchbar = ft.TextField( + label="Cauta produsul in toate categoriile", + expand=True, + on_submit=self.on_search_btn_click + ) + + self.ask_for_login_dialog = ft.AlertDialog( + title=ft.Text("Va rugam sa va autentificati!"), + content=ft.Column( + [ + ft.Text("Daca nu aveti un cont activ, puteti crea unul."), + ft.Text("Dupa confirmare, selectati din nou produsul.") + ], + height=50 + ), + actions=[ + ft.FilledButton( + "Continua fara cont", + bgcolor=ft.Colors.GREY, + on_click=self.on_cancel_go_to_login_btn_click), + ft.FilledButton( + "Autentificare", + on_click=self.on_confirm_go_to_login_btn_click) + ] + ) + + self.selected_item = None + + self.profile_placeholder = ft.Column() + self.profile_btn = ft.IconButton( + icon=ft.Icons.ACCOUNT_CIRCLE_OUTLINED, + on_click=self.on_profile_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ) + self.login_btn = ft.IconButton( + icon=ft.Icons.LOGIN, + on_click=self.on_login_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ) + if self.page.session.get("user") is not None and '@default.com' not in self.page.session.get("user")['email']: + self.profile_placeholder.controls.append(self.profile_btn) + else: + self.profile_placeholder.controls.append(self.login_btn) + + def on_login_btn_click(self, e): + self.page.go('/auth') + + def on_cancel_go_to_login_btn_click(self, e): + self.page.close(self.ask_for_login_dialog) + self.user = DefaultUser(self.page) + #print(self.user.default_user) + self.page.session.set("user", self.user.default_user) + # print(self.page.session.get("user")) + # try: + # quantity = int(self.quantity.value) + # if quantity > 0: + # self.page.open(self.confirm_dialog) + # self._product = self.selected_item + # except Exception as e: + # print(e) + + def on_confirm_go_to_login_btn_click(self, e): + self.page.close(self.ask_for_login_dialog) + self.page.go('/auth') + + def on_search_btn_click(self, e): + self.page.session.set("search_for", self.searchbar.value) + self.page.go('/') + + def on_profile_btn_click(self, e): + self.page.go('/profil') + + def on_cart_btn_click(self, e): + if self.page.session.get('user') == None: + self.page.go("/pre_load_cos") + else: + self.page.go("/cos") + + def on_cancel_btn_click(self, e): + self.page.close(self.confirm_dialog) + + def on_confirm_btn_click(self, e): + item = self.product + self.page.close(self.confirm_dialog) + user = self.page.session.get('user') + print("user:", user) + if user: + _cart = self.orders.get_on_hold_order(user['id']) + if not _cart: + self.orders.add_order(user['id']) + _cart = self.orders.get_on_hold_order(user['id']) + self.add_product_if_not_exists(item, _cart) + + def add_product_if_not_exists(self, product, cart): + user = self.page.session.get('user') + self.on_hold_orders = self.orders.get_on_hold_order(user['id']) + self.order_products = self.orders.get_order_products(self.on_hold_orders['id']) + found = False + for order_product in self.order_products: + if str(product['id']) == str(order_product['prdouct_id']): + quantity = order_product['quantity'] + int(self.quantity.value) + self.orders.update_order_map_quantity(order_product['id'], quantity) + found = True + break + if not found: + self.orders.add_product_to_order(product['id'], cart['id'], int(self.quantity.value)) + + def get_similar_porducts(self): + productsDB = Products() + similar_products = productsDB.get_all_by_category(self.product['category_id']) + products = [] + + for product in similar_products: + if product['id'] != self.product['id']: + products.append(product) + return products + + def remove_quantity(self, e): + buffer = int(self.quantity.value) + if buffer > 0: + self.quantity.value = str(buffer-1) + self.quantity.update() + self.product['quantity'] -=1 + + def add_quantity(self, e): + buffer = int(self.quantity.value) + self.quantity.value = str(buffer+1) + self.quantity.update() + self.product['quantity'] +=1 + + def on_chanage_image_click(self, item): + self.product_main_image.src = 'images/'+item + self.product_main_image.update() + print(self.product_main_image.src) + + def on_home_btn_click(self, e): + self.page.go('/') + + def on_sp_product_click(self, product): + self.page.session.set('product', product) + + def ask_for_create_user(self): + self.page.open(self.ask_for_login_dialog) + + def on_add_to_cart_btn_click(self, item): + self.selected_item = item + user = self.page.session.get('user') + print(user) + if not user: + self.ask_for_create_user() + else: + try: + quantity = int(self.quantity.value) + if quantity > 0: + self.page.open(self.confirm_dialog) + self._product = self.selected_item + except Exception as e: + print(e) + + def on_sp_add_to_cart_click(self, item): + user = self.page.session.get('user') + if not user: + self.page.go('/login') + self.page.client_storage.remove("remembered_token") + self.page.client_storage.remove("remembered_user") + self.page.session.remove('user') + _cart = self.orders.get_on_hold_order(user['id']) + if not _cart: + self.orders.add_order(user['id']) + _cart = self.orders.get_on_hold_order(user['id']) + self.orders.add_product_to_order(item['id'], _cart['id'], 1) + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + self.searchbar, + ft.IconButton( + icon=ft.Icons.SEARCH, + on_click=self.on_search_btn_click, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE + ), + ft.VerticalDivider(), + self.profile_placeholder, + ft.IconButton( + icon=ft.Icons.SHOPPING_CART_OUTLINED, + bgcolor=ft.Colors.BROWN, + icon_color=ft.Colors.WHITE, + on_click=self.on_cart_btn_click + ) + ], + width=1000 + ), + ft.Row( + [ + ft.IconButton( + on_click=self.on_home_btn_click, + icon=ft.Icons.HOME + ), + self.product_name, + ], + alignment=ft.MainAxisAlignment.START, + expand=True, + width=1000 + ), + ft.Container( + content= ft.Column( + [ + self.desktop_container, + self.mobile_container, + ], + alignment=ft.CrossAxisAlignment.START, + width=1000 + ), + padding=10 + ), + ft.Container( + content=ft.Column( + [ + ft.Text("Produse similare", size=17, weight=ft.FontWeight.BOLD), + self.similar_products, + ] + ), + padding=10 + ), + ft.Row( + [ + ft.Text() + ], + expand=True + ) + ], + scroll=ft.ScrollMode.ADAPTIVE, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + width=1000 + ), + expand=True, + bgcolor=ft.Colors.WHITE, + padding=ft.padding.only(left=10, right=10) + ) \ No newline at end of file diff --git a/UI_V2/pages/profile/__pycache__/profilepage.cpython-313.pyc b/UI_V2/pages/profile/__pycache__/profilepage.cpython-313.pyc new file mode 100644 index 0000000..51e659e Binary files /dev/null and b/UI_V2/pages/profile/__pycache__/profilepage.cpython-313.pyc differ diff --git a/UI_V2/pages/profile/profilepage.py b/UI_V2/pages/profile/profilepage.py new file mode 100644 index 0000000..24ef025 --- /dev/null +++ b/UI_V2/pages/profile/profilepage.py @@ -0,0 +1,208 @@ +import flet as ft +from dbActions.users import Users +from dbActions.company import Company + +class ProfilePage: + def __init__(self, page: ft.Page): + self.page = page + self.user_manager = Users() + self.company_manager = Company() + self.user = self.page.session.get("user") + self.company = self.company_manager.get_company(self.user['id']) + self.user_name = ft.TextField(label="Nume si Prenume", value=self.user['name']) + self.email = ft.TextField(label="E-mail", value=self.user['email'], read_only=True) + self.phone = ft.TextField(label="Telefon", value=self.user['phone']) + self.address = ft.TextField( + label="Adresa", + multiline=True, + min_lines=3, + max_lines=5, + value = self.user['address'].split("~")[0] if self.user['address'] else '' + ) + self.company_name = ft.TextField( + label="Denumire firma", + value=self.company['name'] if self.company else '' + ) + self.vat = ft.TextField( + label="CUI", + value=self.company['vat'] if self.company else '' + ) + self.register_number = ft.TextField( + label="Numar registru comert", + value=self.company['register_number'] if self.company else '' + ) + self.company_address = ft.TextField( + label="Sediu", + multiline=True, + min_lines=3, + max_lines=5, + value=self.company['address'] if self.company else '') + self.second_address_placeholder = ft.Column() + self.second_address = ft.TextField( + label="Adresa de livrare", + multiline=True, + min_lines=3, + max_lines=5, + value=self.user['address'].split("~")[1] if self.user['address'] and len(self.user['address'].split("~"))>1 else '' + ) + self.second_address_placeholder =ft.Column() + self.order_placeholder =ft.Column() + self.error_message = ft.Text("", text_align=ft.TextAlign.CENTER) + + if self.user['address'] and len(self.user['address'].split("~"))>1 : + self.is_second_address = True + self.second_address_placeholder.controls.append(self.second_address) + if self.company: + self.is_company = True + self.order_placeholder.controls.append(self.company_name) + self.order_placeholder.controls.append(self.vat) + self.order_placeholder.controls.append(self.register_number) + self.order_placeholder.controls.append(self.company_address) + + self.is_company = False + self.is_second_address = False + + def on_second_address_btn_click(self, e): + self.is_second_address = True + self.second_address_placeholder.controls.append(self.second_address) + self.second_address_placeholder.update() + + def on_order_btn_click(self, e): + self.is_company = True + self.order_placeholder.controls.append(self.company_name) + self.order_placeholder.controls.append(self.vat) + self.order_placeholder.controls.append(self.register_number) + self.order_placeholder.controls.append(self.company_address) + self.order_placeholder.update() + + def check_inserted_user_data(self, username, phone, address): + found = False + if username is None or len(username)< 1: + found = True + if phone is None or len(phone)< 1: + found = True + if address is None or len(address)< 1: + found = True + if found: + self.error_message.value = "Toate campurile sunt obligatori!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + + return found + + def check_second_address_inserted(self, address): + if address is None or len(address)< 1: + self.error_message.value = "Va rugam inserati adresa de livrare!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return True + return False + + def check_company_data_inserted(self, company_name, vat, register_number, company_address): + found = False + if company_name is None or len(company_name)< 1: + found = True + if vat is None or len(vat)< 1: + found = True + if register_number is None or len(register_number)< 1: + found = True + if company_address is None or len(company_address)< 1: + found = True + if found: + self.error_message.value = "Toate campurile sunt obligatori!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return found + + def on_save_btn_click(self, e): + username = self.user_name.value + phone = self.phone.value + address = self.address.value + if self.is_second_address: + if self.check_second_address_inserted(self.second_address.value): + return + address = self.address.value + '~' + self.second_address.value + if self.check_inserted_user_data(username, phone, address): + return + self.user_manager.update_user_data(username, phone, address, self.user['id']) + + if self.is_company: + if self.check_company_data_inserted(self.company_name.value, self.vat.value, self.register_number.value, self.company_address.value): + return + if self.company: + company['name'] = self.company_name.value + company['vat'] = self.vat.value + company['register_number'] = self.register_number.value + company['address'] = self.company_address.value + self.company_manager.update_company(company) + else: + company = { + 'user_id' : self.user['id'], + 'name': self.company_name.value, + 'vat': self.vat.value, + 'register_number': self.register_number.value, + 'address': self.company_address.value + } + self.company_manager.add_company(company) + + self.error_message.value = "Profilul a fost salvat!" + self.error_message.color = ft.Colors.GREEN + self.error_message.update() + + def on_back_btn_click(self, e): + self.page.go('/') + + def on_logout_btn_click(self, e): + self.page.session.clear() + self.page.go('/') + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.FilledButton( + "Inapoi", + icon=ft.Icons.ARROW_BACK, + on_click = self.on_back_btn_click + ) + ], + alignment=ft.MainAxisAlignment.END + ), + ft.Icon(name=ft.Icons.ACCOUNT_CIRCLE, size=100), + self.user_name, + self.email, + self.phone, + self.address, + ft.Divider(), + ft.Text("Adresa de livrare difera de adresa de domiciliu?", text_align=ft.TextAlign.CENTER), + ft.Button("Adauga adresa livrare", width=400, on_click=self.on_second_address_btn_click), + self.second_address_placeholder, + ft.Divider(), + ft.Button("Doriti Factura?", width=400, on_click=self.on_order_btn_click), + self.order_placeholder, + self.error_message, + ft.FilledButton( + "Salveaza", + icon=ft.Icons.SAVE, + on_click=self.on_save_btn_click, + width=400 + ), + ft.FilledButton( + "Deconectare", + icon=ft.Icons.LOGOUT, + on_click=self.on_logout_btn_click, + width=400, + bgcolor=ft.Colors.GREY + ), + ft.Text("") + ], + width=400, + alignment=ft.MainAxisAlignment.START, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + scroll=ft.ScrollMode.ADAPTIVE + ), + padding=10, + expand=True + ) \ No newline at end of file diff --git a/UI_V2/pages/shopping_cart/__pycache__/cart.cpython-313.pyc b/UI_V2/pages/shopping_cart/__pycache__/cart.cpython-313.pyc new file mode 100644 index 0000000..a35d989 Binary files /dev/null and b/UI_V2/pages/shopping_cart/__pycache__/cart.cpython-313.pyc differ diff --git a/UI_V2/pages/shopping_cart/__pycache__/peload_card.cpython-313.pyc b/UI_V2/pages/shopping_cart/__pycache__/peload_card.cpython-313.pyc new file mode 100644 index 0000000..897fa38 Binary files /dev/null and b/UI_V2/pages/shopping_cart/__pycache__/peload_card.cpython-313.pyc differ diff --git a/UI_V2/pages/shopping_cart/cart.py b/UI_V2/pages/shopping_cart/cart.py new file mode 100644 index 0000000..18ce9b5 --- /dev/null +++ b/UI_V2/pages/shopping_cart/cart.py @@ -0,0 +1,535 @@ +import flet as ft +from dbActions.orders import Orders +from dbActions.products import Products +from dbActions.company import Company +from dbActions.users import Users + +class Cart: + def __init__(self, page: ft.Page): + self.page = page + self.user = self.page.session.get('user') + self.orders = Orders() + self.productsDB = Products() + self.company_manager = Company() + self.user_manager = Users() + self.products = [] + self.is_second_address = None + self.is_company = None + + self.delete_dialog = ft.AlertDialog( + title=ft.Text("Stergeti?"), + actions=[ + ft.FilledButton("Da", on_click=self.on_confirm_delete_btn_click), + ft.FilledButton("Nu", on_click=self.on_cancel_delete_btn_click, bgcolor=ft.Colors.GREY) + ] + ) + + self.on_hold_orders = self.orders.get_on_hold_order(self.user['id']) + if self.on_hold_orders: + self.order_products = self.orders.get_order_products(self.on_hold_orders['id']) + + for product in self.order_products: + self.products.append( + { + 'product':self.productsDB.get(product['prdouct_id']), + 'quantity':product['quantity'] + } + ) + self.product_list = ft.ListView( + controls=self.create_list(self.products, self.on_delete_product_click), + spacing=10, + expand=5 + ) + + self.payment = ft.RadioGroup( + content=ft.Row( + [ + ft.Radio(value="ramburs", label="Ramburs la curier"), + ft.Radio(value="plata_online_cu_cardul", label="Plata online cu cardul"), + ] + ), + on_change=self.on_payment_value_change + ) + self.payment_message = ft.Text("") + self.error_message = ft.Text( + color=ft.Colors.RED + ) + + self.confirm_dialog = ft.AlertDialog( + title=ft.Text("Confirma"), + actions=[ + ft.FilledButton( + "Da", + on_click=self.on_confim_btn_click + ), + ft.FilledButton( + "Nu", + on_click=self.on_cancel_btn_click, + bgcolor=ft.Colors.GREY + ) + ] + ) + + if '@default.com' not in self.user['email']: + self.all_orders = self.orders.get_orders_for_user(self.page.session.get('user')['id']) + self.all_orders = self.all_orders[::-1] + self.orders_list = ft.ListView( + controls=self.create_history_list(self.all_orders), + spacing=10, + expand=True + ) + self.user = self.page.session.get("user") + self.company = self.company_manager.get_company(self.user['id']) + self.user_name = ft.TextField( + label="Nume si Prenume", + value=self.user['name'] if "@default.com" not in self.user['email'] else None + ) + self.email = ft.TextField( + label="E-mail", + value=self.user['email'] if "@default.com" not in self.user['email'] else None, + read_only=True + ) + self.phone = ft.TextField( + label="Telefon", + value=self.user['phone'] if "@default.com" not in self.user['email'] else None + ) + self.address = ft.TextField( + label="Adresa", + multiline=True, + min_lines=3, + max_lines=5, + value = self.user['address'].split("~")[0] if self.user['address'] else '' + ) + self.company_name = ft.TextField( + label="Denumire firma", + value=self.company['name'] if self.company else '' + ) + self.vat = ft.TextField( + label="CUI", + value=self.company['vat'] if self.company else '' + ) + self.register_number = ft.TextField( + label="Numar registru comert", + value=self.company['register_number'] if self.company else '' + ) + self.company_address = ft.TextField( + label="Sediu", + multiline=True, + min_lines=3, + max_lines=5, + value=self.company['address'] if self.company else '') + self.second_address_placeholder = ft.Column() + self.second_address = ft.TextField( + label="Adresa de livrare", + multiline=True, + min_lines=3, + max_lines=5, + value=self.user['address'].split("~")[1] if self.user['address'] and len(self.user['address'].split("~"))>1 else '' + ) + self.second_address_placeholder =ft.Column() + self.order_placeholder =ft.Column() + + if self.user['address'] and len(self.user['address'].split("~"))>1 : + self.is_second_address = True + self.second_address_placeholder.controls.append(self.second_address) + if self.company: + self.is_company = True + self.order_placeholder.controls.append(self.company_name) + self.order_placeholder.controls.append(self.vat) + self.order_placeholder.controls.append(self.register_number) + self.order_placeholder.controls.append(self.company_address) + + self.delivery_details = ft.Column( + [ + ft.Text( + "Detaili de livrare", + weight=ft.FontWeight.BOLD + ), + self.user_name, + self.email, + self.phone, + self.address, + ft.Divider(), + ft.Text("Adresa de livrare difera de adresa de domiciliu?", text_align=ft.TextAlign.CENTER), + ft.Button("Adauga adresa livrare", width=500, on_click=self.on_second_address_btn_click), + self.second_address_placeholder, + ft.Divider(), + ft.Button("Doriti Factura?", width=500, on_click=self.on_order_btn_click), + self.order_placeholder, + + ft.Text( + "Metoda de plata", + weight=ft.FontWeight.BOLD + ), + self.payment, + self.payment_message, + ft.Row([self.error_message],alignment=ft.MainAxisAlignment.CENTER), + ft.Row( + [ + ft.FilledButton( + "Comanda", + width=150, + on_click=self.open_confirm_dialog + ) + ], + alignment=ft.MainAxisAlignment.CENTER + ) + ], + expand=5, + alignment=ft.MainAxisAlignment.START + ) + + if self.page.width < 600: + self.cart_placeholder = ft.Column( + [ + self.product_list, + self.delivery_details + ] + ) + else: + self.cart_placeholder = ft.Row( + [ + self.product_list, + self.delivery_details + ], + vertical_alignment=ft.CrossAxisAlignment.START + ) + self.order_list_placeholder = ft.Column() + if '@default.com' not in self.user['email']: + self.order_list_placeholder.controls.append( + ft.Text( + "Istoric comenzi", + weight=ft.FontWeight.BOLD + ) + ) + self.order_list_placeholder.controls.append(self.orders_list) + + self.item_to_be_deleted = None + + self.page.add(self.delete_dialog) + self.page.add(self.confirm_dialog) + + def on_second_address_btn_click(self, e): + self.is_second_address = True + self.second_address_placeholder.controls.append(self.second_address) + self.second_address_placeholder.update() + + def on_order_btn_click(self, e): + self.is_company = True + self.order_placeholder.controls.append(self.company_name) + self.order_placeholder.controls.append(self.vat) + self.order_placeholder.controls.append(self.register_number) + self.order_placeholder.controls.append(self.company_address) + self.order_placeholder.update() + + def on_payment_value_change(self, e): + print(e.data) + if e.data == 'plata_online_cu_cardul': + pass + else: + pass + + def order_products(self, id): + products = self.orders.get_order_products(id) + all_products = [] + for product in products: + name = self.productsDB.get(product['prdouct_id'])['name'] + if name not in all_products: + all_products.append(name) + return ft.Text( + value=' '.join(all_products) + ) + + def on_go_back_button_click(self, e): + self.page.go("/") + + def create_list(self, items, on_click_handler): + """Helper to create list items for a column.""" + return [ + ft.Container( + content=ft.Row( + [ + ft.Column( + [ + ft.Row( + [ + ft.Icon(ft.Icons.ARROW_RIGHT, size=20), + ft.Row( + [ + ft.Image( + src=item['product']['image'], + width=100, + height=100, + fit=ft.ImageFit.COVER, + border_radius=10 + ), + ft.Column( + [ + ft.Text(f"Denumire Produs: {item['product']['name']}", weight=ft.FontWeight.BOLD), + ft.Text(f"Cantitate: {item['quantity']}"), + ft.Text(f"Descriere: {item['product']['description']}", size=12 , color=ft.Colors.GREY), + ] + ) + ] + ) + + ] + ), + ] + ), + ft.Row( + [ + ft.FilledButton("Sterge",on_click=lambda e, id=item: on_click_handler(id), bgcolor=ft.Colors.RED), + ] + ) + + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + width=300, + bgcolor=ft.Colors.BROWN_50, + padding=10, + border_radius=8, + border = ft.border.all(1, ft.Colors.GREY), + ) + for item in items + ] + + def create_history_list(self, items): + return [ + ft.Container( + content=ft.Column( + [ + + ft.Row( + [ + ft.Icon(ft.icons.ARROW_RIGHT, size=20), + ft.Text(value="Numar comanda: "), + ft.Text(value=item['id']) + ] + ), + ft.Row( + [ + ft.Text( + value='Status: ' + ), + ft.Text( + value=item['status'] + ) + ] + ), + ft.Row( + [ + ft.Text( + value='Produse: ' + ), + #self.order_products(item['id']) + ] + ) + ], + alignment=ft.MainAxisAlignment.START, + ), + width=300, + bgcolor=ft.Colors.BROWN_50, + padding=10, + border_radius=8, + border = ft.border.all(1, ft.Colors.GREY), + ) + for item in items + ] + + + def on_delete_product_click(self, item): + print('Delte item', item) + + self.page.open(self.delete_dialog) + self.page.update() + self.item_to_be_deleted = item['product']['id'] + + def on_confirm_delete_btn_click(self, e): + print("confirm delete item", self.item_to_be_deleted) + #remove item + self.orders.remove_product_from_order(self.on_hold_orders['id'], self.item_to_be_deleted) + #update list + self.order_products = self.orders.get_order_products(self.on_hold_orders['id']) + self.products = [] + for product in self.order_products: + self.products.append( + { + 'product':self.productsDB.get(product['prdouct_id']), + 'quantity':product['quantity'] + } + ) + self.product_list.controls.clear() + self.product_list.controls = self.create_list(self.products, self.on_delete_product_click) + self.product_list.update() + + self.item_to_be_deleted = None + self.page.close(self.delete_dialog) + + def on_cancel_delete_btn_click(self, e): + print("cancel item deletion: ", self.item_to_be_deleted) + self.item_to_be_deleted = None + self.page.close(self.delete_dialog) + + + def open_confirm_dialog(self, e): + print('open dialog') + print(self.on_hold_orders) + if self.on_hold_orders: + self.error_message.value = '' + self.error_message.update() + self.page.open(self.confirm_dialog) + else: + self.error_message.value = "Nu aveti nici un produs in cos!" + self.error_message.update() + + def on_cancel_btn_click(self, e): + self.page.close(self.confirm_dialog) + + def check_second_address_inserted(self, address): + if address is None or len(address)< 1: + self.error_message.value = "Va rugam inserati adresa de livrare!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return True + return False + + def check_company_data_inserted(self, company_name, vat, register_number, company_address): + found = False + if company_name is None or len(company_name)< 1: + found = True + if vat is None or len(vat)< 1: + found = True + if register_number is None or len(register_number)< 1: + found = True + if company_address is None or len(company_address)< 1: + found = True + if found: + self.error_message.value = "Toate campurile sunt obligatori!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + return found + + def check_inserted_user_data(self, username, phone, address): + found = False + if username is None or len(username)< 1: + found = True + if phone is None or len(phone)< 1: + found = True + if address is None or len(address)< 1: + found = True + if found: + self.error_message.value = "Toate campurile sunt obligatori!" + self.error_message.color = ft.Colors.RED + self.error_message.update() + + return found + + def create_update_user_details(self): + username = self.user_name.value + phone = self.phone.value + address = self.address.value + + if self.is_second_address: + if self.check_second_address_inserted(self.second_address.value): + return + address = self.address.value + '~' + self.second_address.value + if self.check_inserted_user_data(username, phone, address): + return + + if '@default.com' in self.user['email']: + self.user = self.user_manager.invite_user() + self.page.session.set("user", self.user) + else: + self.user_manager.update_user_data(username, phone, address, self.user['id']) + + if self.is_company: + if self.check_company_data_inserted(self.company_name.value, self.vat.value, self.register_number.value, self.company_address.value): + return + if self.company: + company['name'] = self.company_name.value + company['vat'] = self.vat.value + company['register_number'] = self.register_number.value + company['address'] = self.company_address.value + self.company_manager.update_company(company) + else: + company = { + 'user_id' : self.user['id'], + 'name': self.company_name.value, + 'vat': self.vat.value, + 'register_number': self.register_number.value, + 'address': self.company_address.value + } + self.company_manager.add_company(company) + + def on_confim_btn_click(self, e): + #create / update user details: + self.create_update_user_details() + + print('confirm') + self.page.close(self.confirm_dialog) + self.orders.update_order_status("new", self.on_hold_orders['id']) + + self.products = [] + self.on_hold_orders = self.orders.get_on_hold_order(self.user['id']) + if self.on_hold_orders: + self.order_products = self.orders.get_order_products(self.on_hold_orders['id']) + + for product in self.order_products: + self.products.append( + { + 'product':self.productsDB.get(product['prdouct_id']), + 'quantity':product['quantity'] + } + ) + self.product_list.controls.clear() + self.product_list.controls = self.create_list(self.products, self.on_delete_product_click) + self.product_list.update() + if '@default.com' not in self.user['email']: + self.all_orders = self.orders.get_orders_for_user(self.page.session.get('user')['id']) + self.all_orders = self.all_orders[::-1] + self.orders_list.controls.clear() + self.orders_list.controls = self.create_history_list(self.all_orders) + self.orders_list.update() + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Row( + [ + ft.Icon( + ft.Icons.SHOPPING_CART_CHECKOUT, + size=40, + ), + ft.Text( + "Produse adugate:", + weight=ft.FontWeight.BOLD, + size=15 + ) + ] + ), + + ft.FilledButton( + text="Inapoi", + icon=ft.Icons.ARROW_BACK, + on_click=self.on_go_back_button_click + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Divider(), + self.cart_placeholder, + self.order_list_placeholder, + ft.Text() + ], + expand=True, + scroll=ft.ScrollMode.ADAPTIVE + ), + expand=True, + width=1000, + padding=10 + ) \ No newline at end of file diff --git a/UI_V2/pages/shopping_cart/peload_card.py b/UI_V2/pages/shopping_cart/peload_card.py new file mode 100644 index 0000000..af64006 --- /dev/null +++ b/UI_V2/pages/shopping_cart/peload_card.py @@ -0,0 +1,60 @@ +import flet as ft +from helpers.default_user import DefaultUser + +class PreloadCard: + def __init__(self, page: ft.Page): + self.page = page + self.ask_for_login_dialog = ft.AlertDialog( + title="Va rugam sa va autentificati", + content=ft.Text("Daca nu aveti un cont activ, puteti crea unul."), + actions=[ + ft.FilledButton( + "Continua fara cont", + bgcolor=ft.Colors.GREY, + on_click=self.on_cancel_go_to_login_btn_click), + ft.FilledButton( + "Autentificare", + on_click=self.on_confirm_go_to_login_btn_click) + ] + ) + + def on_cancel_go_to_login_btn_click(self, e): + #self.page.close(self.ask_for_login_dialog) + self.user = DefaultUser(self.page) + self.page.session.set("user", self.user.default_user) + self.page.go('/cos') + + def on_confirm_go_to_login_btn_click(self, e): + #self.page.close(self.ask_for_login_dialog) + self.page.go('/auth') + + def build(self): + if not self.page.session.get("user"): + return ft.Container( + content=ft.Column( + [ + ft.Text("Va rugam sa va autentificati", size=18, weight=ft.FontWeight.BOLD), + ft.Text("Daca nu aveti un cont activ, puteti crea unul."), + ft.Row( + [ + ft.FilledButton( + "Continua fara cont", + bgcolor=ft.Colors.GREY, + on_click=self.on_cancel_go_to_login_btn_click), + ft.FilledButton( + "Autentificare", + on_click=self.on_confirm_go_to_login_btn_click) + ], + alignment=ft.MainAxisAlignment.CENTER + ) + #self.page.open(self.ask_for_login_dialog) + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ) + ) + else: + return ft.Container( + content=self.page.go('/cos') + ) + \ No newline at end of file diff --git a/UI_V2/requirements.txt b/UI_V2/requirements.txt new file mode 100644 index 0000000..38eee15 --- /dev/null +++ b/UI_V2/requirements.txt @@ -0,0 +1 @@ +flet==0.28.3 \ No newline at end of file