commit 650b69a97dc09819340ba8234f0defdb808406e1 Author: Marius Robert Macamete Date: Sat Jun 13 21:46:37 2026 +0300 first commit diff --git a/client/.DS_Store b/client/.DS_Store new file mode 100644 index 0000000..3f34ba5 Binary files /dev/null and b/client/.DS_Store differ diff --git a/client/assets/.DS_Store b/client/assets/.DS_Store new file mode 100644 index 0000000..d4cea0e Binary files /dev/null and b/client/assets/.DS_Store differ diff --git a/client/assets/favicon.png b/client/assets/favicon.png new file mode 100644 index 0000000..4b9102f Binary files /dev/null and b/client/assets/favicon.png differ diff --git a/client/assets/icons/.DS_Store b/client/assets/icons/.DS_Store new file mode 100644 index 0000000..93f5625 Binary files /dev/null and b/client/assets/icons/.DS_Store differ diff --git a/client/assets/icons/loading-animation.png b/client/assets/icons/loading-animation.png new file mode 100644 index 0000000..4b9102f Binary files /dev/null and b/client/assets/icons/loading-animation.png differ diff --git a/client/assets/images/.DS_Store b/client/assets/images/.DS_Store new file mode 100644 index 0000000..24074bd Binary files /dev/null and b/client/assets/images/.DS_Store differ diff --git a/client/assets/images/eng.png b/client/assets/images/eng.png new file mode 100644 index 0000000..3c7b93e Binary files /dev/null and b/client/assets/images/eng.png differ diff --git a/client/assets/images/logo_juridic_block.png b/client/assets/images/logo_juridic_block.png new file mode 100644 index 0000000..4b9102f Binary files /dev/null and b/client/assets/images/logo_juridic_block.png differ diff --git a/client/assets/images/rom.png b/client/assets/images/rom.png new file mode 100644 index 0000000..a166372 Binary files /dev/null and b/client/assets/images/rom.png differ diff --git a/client/assets/uploads/20260613183323_panel.png b/client/assets/uploads/20260613183323_panel.png new file mode 100644 index 0000000..102c005 Binary files /dev/null and b/client/assets/uploads/20260613183323_panel.png differ diff --git a/client/assets/uploads/20260613190042_taina_gustuli_brown_bg_white.png b/client/assets/uploads/20260613190042_taina_gustuli_brown_bg_white.png new file mode 100644 index 0000000..441a1b4 Binary files /dev/null and b/client/assets/uploads/20260613190042_taina_gustuli_brown_bg_white.png differ diff --git a/client/assets/uploads/20260613190652_taina_gustuli_brown_bg.png b/client/assets/uploads/20260613190652_taina_gustuli_brown_bg.png new file mode 100644 index 0000000..9eace36 Binary files /dev/null and b/client/assets/uploads/20260613190652_taina_gustuli_brown_bg.png differ diff --git a/client/assets/uploads/20260613190736_beef cow_0.jpg b/client/assets/uploads/20260613190736_beef cow_0.jpg new file mode 100644 index 0000000..3b6f918 Binary files /dev/null and b/client/assets/uploads/20260613190736_beef cow_0.jpg differ diff --git a/client/assets/uploads/20260613191538_bell-icon-with-one-notification-1.png b/client/assets/uploads/20260613191538_bell-icon-with-one-notification-1.png new file mode 100644 index 0000000..85aa02e Binary files /dev/null and b/client/assets/uploads/20260613191538_bell-icon-with-one-notification-1.png differ diff --git a/client/assets/uploads/20260613191751_1.PNG b/client/assets/uploads/20260613191751_1.PNG new file mode 100644 index 0000000..a3ba986 Binary files /dev/null and b/client/assets/uploads/20260613191751_1.PNG differ diff --git a/client/assets/uploads/20260613205856_taina_gustuli_brown_bg.png b/client/assets/uploads/20260613205856_taina_gustuli_brown_bg.png new file mode 100644 index 0000000..9eace36 Binary files /dev/null and b/client/assets/uploads/20260613205856_taina_gustuli_brown_bg.png differ diff --git a/client/assets/uploads/20260613210201_taina_gustuli_brown_bg.png b/client/assets/uploads/20260613210201_taina_gustuli_brown_bg.png new file mode 100644 index 0000000..9eace36 Binary files /dev/null and b/client/assets/uploads/20260613210201_taina_gustuli_brown_bg.png differ diff --git a/client/helpers/__pycache__/document_status.cpython-313.pyc b/client/helpers/__pycache__/document_status.cpython-313.pyc new file mode 100644 index 0000000..6b7ea31 Binary files /dev/null and b/client/helpers/__pycache__/document_status.cpython-313.pyc differ diff --git a/client/helpers/__pycache__/emails.cpython-313.pyc b/client/helpers/__pycache__/emails.cpython-313.pyc new file mode 100644 index 0000000..bb52262 Binary files /dev/null and b/client/helpers/__pycache__/emails.cpython-313.pyc differ diff --git a/client/helpers/__pycache__/payment_type.cpython-313.pyc b/client/helpers/__pycache__/payment_type.cpython-313.pyc new file mode 100644 index 0000000..acee349 Binary files /dev/null and b/client/helpers/__pycache__/payment_type.cpython-313.pyc differ diff --git a/client/helpers/__pycache__/roles.cpython-313.pyc b/client/helpers/__pycache__/roles.cpython-313.pyc new file mode 100644 index 0000000..179d92a Binary files /dev/null and b/client/helpers/__pycache__/roles.cpython-313.pyc differ diff --git a/client/helpers/__pycache__/translate.cpython-313.pyc b/client/helpers/__pycache__/translate.cpython-313.pyc new file mode 100644 index 0000000..f1f4352 Binary files /dev/null and b/client/helpers/__pycache__/translate.cpython-313.pyc differ diff --git a/client/helpers/__pycache__/validations.cpython-313.pyc b/client/helpers/__pycache__/validations.cpython-313.pyc new file mode 100644 index 0000000..0258a3c Binary files /dev/null and b/client/helpers/__pycache__/validations.cpython-313.pyc differ diff --git a/client/helpers/document_status.py b/client/helpers/document_status.py new file mode 100644 index 0000000..0bc5404 --- /dev/null +++ b/client/helpers/document_status.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass + +@dataclass +class DocumentsStatus: + NEW = 'new' + ANALISE = 'analise' + IN_PROGRESS = 'in_progress' + WAITING_FOR_PAYMENT = 'waiting_for_payment' + COMPLETED = 'completed' + CANCELED = 'canceld' + + + @staticmethod + def get_label(status): + mapping = { + DocumentsStatus.NEW: 'Nou', + DocumentsStatus.ANALISE: 'Analiza', + DocumentsStatus.IN_PROGRESS: 'In progres', + DocumentsStatus.WAITING_FOR_PAYMENT: 'Asteptam plata', + DocumentsStatus.COMPLETED: 'Complet', + DocumentsStatus.CANCELED: 'Anulat' + } + return mapping.get(status, status) + diff --git a/client/helpers/emails.py b/client/helpers/emails.py new file mode 100644 index 0000000..86a5c77 --- /dev/null +++ b/client/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/client/helpers/payment_type.py b/client/helpers/payment_type.py new file mode 100644 index 0000000..5b55155 --- /dev/null +++ b/client/helpers/payment_type.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + +@dataclass +class PaymentType: + ONE_TIME_ONLY = 'o singura data' + SUBSCRIPTION = 'abonament' \ No newline at end of file diff --git a/client/helpers/roles.py b/client/helpers/roles.py new file mode 100644 index 0000000..bf56cf3 --- /dev/null +++ b/client/helpers/roles.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass + +@dataclass +class Roles: + USER = "user" + ADMIN = "admin" + PROPRIETAR = "proprietar" + CENZOR = "cenzor" + ADMINISTRATOR = "administrator" + PRESEDINTE = "presedinte" + EXPERT = "expert" + BA = "ba" + SUPER_USER = 'super_user' + + +@dataclass +class Priorities: + USER = "0" + PROPRIETAR = "1" + CENZOR = "2" + ADMINISTRATOR = "2" + PRESEDINTE = "2" + EXPERT = "3" + BA = "4" + SUPER_USER = "5" \ No newline at end of file diff --git a/client/helpers/validations.py b/client/helpers/validations.py new file mode 100644 index 0000000..84f7f67 --- /dev/null +++ b/client/helpers/validations.py @@ -0,0 +1,66 @@ +import re + +class Validations: + def __init__(self, error_message, page): + self.page = page + self.error_message = error_message + + 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 e email valida!" + 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 obligatorii!" + 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 aiba 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 o cifra!" + 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 (de exemplu: !@#$%^&*)!" + self.error_message.update() + return False + self.error_message.update() + return True diff --git a/client/main.py b/client/main.py new file mode 100644 index 0000000..40ceca8 --- /dev/null +++ b/client/main.py @@ -0,0 +1,88 @@ +import asyncio + +import flet as ft +from pages.auth.auth import Auth +from pages.dashboard.home import Home + +import os +os.environ["FLET_SECRET_KEY"] = os.urandom(12).hex() +os.environ['PASSWORD_TOKEN'] = os.urandom(12).hex() + +import requests, json + +async def main(page: ft.Page): + page.title = "Administarare locuintelor in mod eficient" + page.theme_mode = ft.ThemeMode.LIGHT + page.theme = ft.Theme( + color_scheme=ft.ColorScheme( + primary=ft.Colors.BLUE + ) + ) + page.vertical_alignment = ft.MainAxisAlignment.CENTER + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + page.padding = 0 + page.web = True + + async def route_change(): + page.controls.clear() + route = page.route + print(route) + + #set backend url + page.session.store.set('api_base_url', 'http://localhost:5000') + + if route == "/auth": + login = Auth(page) + page.add(login.build()) + return + + if route =="/home" or "/": + token = page.session.store.get('token') + user = page.session.store.get('user') + + # Dacă nu avem sesiune activă, verificăm stocarea persistentă + if not token or not user: + token = await get_value("mi_tocken") + if not token: + await page.push_route('/auth') + return + + API_BASE_URL = page.session.store.get('api_base_url') + try: + response = requests.get( + f"{API_BASE_URL}/auth/me", + headers={"Authorization": f"Bearer {token}"}, + timeout=10, + ) + if response.status_code == 200: + user = response.json() + page.session.store.set('token', token) + page.session.store.set('user', user) + else: + await page.push_route('/auth') + return + except Exception: + await page.push_route('/auth') + return + + home = Home(page) + page.add(await home.build()) + page.update() + return + + page.add(ft.Text("404: Page not found")) + + async def get_value(key): + return await ft.SharedPreferences().get(key) + + page.on_route_change = route_change + await page.push_route('/home') + +ft.run( + main = main, + assets_dir = "assets", + upload_dir = "assets/uploads", + view=ft.AppView.WEB_BROWSER, + port=8090, + host="0.0.0.0" +) \ No newline at end of file diff --git a/client/models/chats/__pycache__/conversation.cpython-313.pyc b/client/models/chats/__pycache__/conversation.cpython-313.pyc new file mode 100644 index 0000000..52c58bf Binary files /dev/null and b/client/models/chats/__pycache__/conversation.cpython-313.pyc differ diff --git a/client/models/chats/__pycache__/message.cpython-313.pyc b/client/models/chats/__pycache__/message.cpython-313.pyc new file mode 100644 index 0000000..4c19bbb Binary files /dev/null and b/client/models/chats/__pycache__/message.cpython-313.pyc differ diff --git a/client/models/chats/__pycache__/participant.cpython-313.pyc b/client/models/chats/__pycache__/participant.cpython-313.pyc new file mode 100644 index 0000000..42d2e61 Binary files /dev/null and b/client/models/chats/__pycache__/participant.cpython-313.pyc differ diff --git a/client/models/chats/conversation.py b/client/models/chats/conversation.py new file mode 100644 index 0000000..c2bed18 --- /dev/null +++ b/client/models/chats/conversation.py @@ -0,0 +1,49 @@ +import sqlite3 +from dataclasses import dataclass +from typing import Optional, List + +@dataclass +class ConversationModel: + id: Optional[int] = None + is_group: Optional[int] = None + name: Optional[str] = None + created_at: Optional[str] = None + +class Conversations: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_tables() + + def _create_tables(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS conversations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + is_group INTEGER DEFAULT 0, + name TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """) + conn.commit() + + def create(self, is_group: int, name: Optional[str] = None) -> int: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO conversations (is_group, name) VALUES (?, ?)", + (is_group, name) + ) + return cursor.lastrowid + + def get_by_id(self, conv_id: int) -> Optional[ConversationModel]: + with sqlite3.connect(self.db_path) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("SELECT * FROM conversations WHERE id = ?", (conv_id,)) + row = cursor.fetchone() + return ConversationModel(**dict(row)) if row else None + + def delete(self, conv_id: int): + with sqlite3.connect(self.db_path) as conn: + conn.execute("DELETE FROM conversations WHERE id = ?", (conv_id,)) \ No newline at end of file diff --git a/client/models/chats/message.py b/client/models/chats/message.py new file mode 100644 index 0000000..401e0cb --- /dev/null +++ b/client/models/chats/message.py @@ -0,0 +1,57 @@ +import sqlite3 +from dataclasses import dataclass +from typing import Optional, List + +# --- DATACLASSES --- + +@dataclass +class MessageModel: + id: Optional[int] = None + conversation_id: Optional[int] = None + sender_id: Optional[int] = None + content: Optional[str] = None + created_at: Optional[str] = None + +class Messages: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_tables() + + def _create_tables(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conversation_id INTEGER, + sender_id INTEGER, + content TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE, + FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE + ); + """) + conn.commit() + + def send(self, conversation_id: int, sender_id: int, content: str) -> int: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO messages (conversation_id, sender_id, content) VALUES (?, ?, ?)", + (conversation_id, sender_id, content) + ) + return cursor.lastrowid + + def get_history(self, conversation_id: int, limit: int = 50) -> List[MessageModel]: + with sqlite3.connect(self.db_path) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute( + "SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at DESC LIMIT ?", + (conversation_id, limit) + ) + return [MessageModel(**dict(row)) for row in cursor.fetchall()] + + def delete_message(self, message_id: int): + with sqlite3.connect(self.db_path) as conn: + conn.execute("DELETE FROM messages WHERE id = ?", (message_id,)) \ No newline at end of file diff --git a/client/models/chats/participant.py b/client/models/chats/participant.py new file mode 100644 index 0000000..3ecb85d --- /dev/null +++ b/client/models/chats/participant.py @@ -0,0 +1,49 @@ +import sqlite3 +from dataclasses import dataclass +from typing import Optional, List + +@dataclass +class ParticipantModel: + conversation_id: Optional[int] = None + user_id: Optional[int] = None + joined_at: Optional[str] = None + +class Participants: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_tables() + + def _create_tables(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS participants ( + conversation_id INTEGER, + user_id INTEGER, + joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (conversation_id, user_id), + FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + """) + conn.commit() + + def add_user(self, conversation_id: int, user_id: int): + with sqlite3.connect(self.db_path) as conn: + conn.execute( + "INSERT OR IGNORE INTO participants (conversation_id, user_id) VALUES (?, ?)", + (conversation_id, user_id) + ) + + def get_conversation_members(self, conversation_id: int) -> List[int]: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT user_id FROM participants WHERE conversation_id = ?", (conversation_id,)) + return [row[0] for row in cursor.fetchall()] + + def remove_user(self, conversation_id: int, user_id: int): + with sqlite3.connect(self.db_path) as conn: + conn.execute( + "DELETE FROM participants WHERE conversation_id = ? AND user_id = ?", + (conversation_id, user_id) + ) \ No newline at end of file diff --git a/client/navigation/__pycache__/ba.cpython-313.pyc b/client/navigation/__pycache__/ba.cpython-313.pyc new file mode 100644 index 0000000..376bf3a Binary files /dev/null and b/client/navigation/__pycache__/ba.cpython-313.pyc differ diff --git a/client/navigation/__pycache__/user.cpython-313.pyc b/client/navigation/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000..6c4ef0a Binary files /dev/null and b/client/navigation/__pycache__/user.cpython-313.pyc differ diff --git a/client/navigation/ba.py b/client/navigation/ba.py new file mode 100644 index 0000000..8653573 --- /dev/null +++ b/client/navigation/ba.py @@ -0,0 +1,96 @@ +import flet as ft +from pages.documents.ba import Documents +from pages.settings.settings import Settings + +class NavigationBA: + def __init__(self, page: ft.Page, home): + self.page = page + self.home = home + + self.documente_juridice = ft.NavigationRailDestination( + icon=ft.Icons.BALANCE_OUTLINED, + selected_icon=ft.Icons.BALANCE, + label="Documente Custom", + ) + self.articole_si_publicatii = ft.NavigationRailDestination( + icon=ft.Icons.ARTICLE_OUTLINED, + selected_icon=ft.Icons.ARTICLE, + label="Articole si Publicatii", + ) + self.comunicare = ft.NavigationRailDestination( + icon=ft.Icons.CHAT_BUBBLE_OUTLINE, + selected_icon=ft.Icons.CHAT, + label=ft.Text("Comunicare"), + ) + self.consultanta = ft.NavigationRailDestination( + icon=ft.Icons.HANDSHAKE_OUTLINED, + selected_icon=ft.Icons.HANDSHAKE, + label=ft.Text("Consultanta"), + ) + self.convocator = ft.NavigationRailDestination( + icon=ft.Icons.BUSINESS_OUTLINED, + selected_icon=ft.Icons.BUSINESS, + label=ft.Text("Convocator"), + ) + self.licitatii_si_lucrari = ft.NavigationRailDestination( + icon=ft.Icons.ASSIGNMENT_TURNED_IN_OUTLINED, + selected_icon=ft.Icons.ASSIGNMENT_TURNED_IN, + label=ft.Text("Licitatii si Lucrari"), + ) + self.profil = ft.NavigationRailDestination( + icon=ft.Icons.ACCOUNT_BOX_OUTLINED, + selected_icon=ft.Icons.ACCOUNT_BOX, + label=ft.Text("Profil"), + ) + self.abonamente = ft.NavigationRailDestination( + icon=ft.Icons.REPEAT_OUTLINED, + selected_icon=ft.Icons.REPEAT_ON, + label=ft.Text("Abonamente"), + ) + self.setari = ft.NavigationRailDestination( + icon=ft.Icons.SETTINGS_OUTLINED, + selected_icon=ft.Icons.SETTINGS, + label=ft.Text("Setari"), + ) + self.logout = ft.NavigationRailDestination( + icon=ft.Icons.LOGOUT_OUTLINED, + selected_icon=ft.Icons.LOGOUT, + label=ft.Text("Deconectare"), + ) + + def build(self): + return [ + self.documente_juridice, + # self.articole_si_publicatii, + # self.comunicare, + # self.consultanta, + # self.convocator, + # self.licitatii_si_lucrari, + # self.profil, + self.abonamente, + self.setari, + self.logout + ] + + def build_documente_juridice(self): + self.docs = Documents(self.page, self.home) + return self.docs.build() + + async def on_nav_change(self, e): + print( "Selected destination:", e.control.selected_index) + if e.control.selected_index == 0: + self.home.placeholder.content = self.build_documente_juridice() + self.page.update() + + if e.control.selected_index == 1: + pass + + if e.control.selected_index == 2: + self.settings = Settings(self.page, self) + self.home.placeholder.content = self.settings.build() + self.page.update() # Actualizează întreaga pagină inclusiv overlay-ul + + if e.control.selected_index == 3: + await ft.SharedPreferences().clear() + self.page.session.store.clear() + self.page.go('/auth') \ No newline at end of file diff --git a/client/navigation/user.py b/client/navigation/user.py new file mode 100644 index 0000000..f08f638 --- /dev/null +++ b/client/navigation/user.py @@ -0,0 +1,93 @@ +import flet as ft +from pages.documents.home import DocumentsHome + +class NavigationUser: + def __init__(self, page: ft.Page, home): + self.page = page + self.home = home + + self.documente_juridice = ft.NavigationRailDestination( + icon=ft.Icons.BALANCE_OUTLINED, + selected_icon=ft.Icons.BALANCE, + label="Documente Juridice", + ) + self.articole_si_publicatii = ft.NavigationRailDestination( + icon=ft.Icons.ARTICLE_OUTLINED, + selected_icon=ft.Icons.ARTICLE, + label="Articole si Publicatii", + ) + self.comunicare = ft.NavigationRailDestination( + icon=ft.Icons.CHAT_BUBBLE_OUTLINE, + selected_icon=ft.Icons.CHAT, + label=ft.Text("Comunicare"), + ) + self.consultanta = ft.NavigationRailDestination( + icon=ft.Icons.HANDSHAKE_OUTLINED, + selected_icon=ft.Icons.HANDSHAKE, + label=ft.Text("Consultanta"), + ) + self.convocator = ft.NavigationRailDestination( + icon=ft.Icons.BUSINESS_OUTLINED, + selected_icon=ft.Icons.BUSINESS, + label=ft.Text("Convocator"), + ) + self.licitatii_si_lucrari = ft.NavigationRailDestination( + icon=ft.Icons.ASSIGNMENT_TURNED_IN_OUTLINED, + selected_icon=ft.Icons.ASSIGNMENT_TURNED_IN, + label=ft.Text("Licitatii si Lucrari"), + ) + self.profil = ft.NavigationRailDestination( + icon=ft.Icons.ACCOUNT_BOX_OUTLINED, + selected_icon=ft.Icons.ACCOUNT_BOX, + label=ft.Text("Profil"), + ) + self.abonamente = ft.NavigationRailDestination( + icon=ft.Icons.REPEAT_OUTLINED, + selected_icon=ft.Icons.REPEAT_ON, + label=ft.Text("Abonamente"), + ) + self.setari = ft.NavigationRailDestination( + icon=ft.Icons.SETTINGS_OUTLINED, + selected_icon=ft.Icons.SETTINGS, + label=ft.Text("Setari"), + ) + self.logout = ft.NavigationRailDestination( + icon=ft.Icons.LOGOUT_OUTLINED, + selected_icon=ft.Icons.LOGOUT, + label=ft.Text("Deconectare"), + ) + + def build(self): + return [ + self.documente_juridice, + self.articole_si_publicatii, + self.comunicare, + self.consultanta, + self.convocator, + self.licitatii_si_lucrari, + self.profil, + self.abonamente, + self.setari, + self.logout + ] + + def build_documente_juridice(self): + self.docs = DocumentsHome(self.page, self.home) + return self.docs.build() + + async def on_nav_change(self, e): + print( "Selected destination:", e.control.selected_index) + if e.control.selected_index == 0: + self.home.placeholder.content = self.build_documente_juridice() + self.page.update() + + if e.control.selected_index == 1: + pass + + if e.control.selected_index == 2: + pass + + if e.control.selected_index == 3: + await ft.SharedPreferences().clear() + self.page.session.store.clear() + self.page.go('/auth') \ No newline at end of file diff --git a/client/pages/.DS_Store b/client/pages/.DS_Store new file mode 100644 index 0000000..2871291 Binary files /dev/null and b/client/pages/.DS_Store differ diff --git a/client/pages/auth/__pycache__/auth.cpython-313.pyc b/client/pages/auth/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..b1e3118 Binary files /dev/null and b/client/pages/auth/__pycache__/auth.cpython-313.pyc differ diff --git a/client/pages/auth/__pycache__/forgot_password.cpython-313.pyc b/client/pages/auth/__pycache__/forgot_password.cpython-313.pyc new file mode 100644 index 0000000..761bba7 Binary files /dev/null and b/client/pages/auth/__pycache__/forgot_password.cpython-313.pyc differ diff --git a/client/pages/auth/__pycache__/login.cpython-313.pyc b/client/pages/auth/__pycache__/login.cpython-313.pyc new file mode 100644 index 0000000..7747b16 Binary files /dev/null and b/client/pages/auth/__pycache__/login.cpython-313.pyc differ diff --git a/client/pages/auth/__pycache__/register.cpython-313.pyc b/client/pages/auth/__pycache__/register.cpython-313.pyc new file mode 100644 index 0000000..3280965 Binary files /dev/null and b/client/pages/auth/__pycache__/register.cpython-313.pyc differ diff --git a/client/pages/auth/__pycache__/verify_code.cpython-313.pyc b/client/pages/auth/__pycache__/verify_code.cpython-313.pyc new file mode 100644 index 0000000..ef9a192 Binary files /dev/null and b/client/pages/auth/__pycache__/verify_code.cpython-313.pyc differ diff --git a/client/pages/auth/auth.py b/client/pages/auth/auth.py new file mode 100644 index 0000000..63d0c88 --- /dev/null +++ b/client/pages/auth/auth.py @@ -0,0 +1,62 @@ +import flet as ft +from pages.auth.login import Login + +class Auth: + def __init__(self, page: ft.Page): + self.page = page + self.placeholder = ft.Container( + width=350, + border_radius=20, + bgcolor=ft.Colors.BLUE_200 if self.page.theme_mode == ft.ThemeMode.LIGHT else ft.Colors.BLUE_900, + padding=20 + ) + self.login = Login(self.page, self) + self.placeholder.content = self.login.build() + self.choose_them_color_btn = ft.IconButton( + icon=ft.Icons.DARK_MODE, + on_click=self.change_theme_mode + ) + + def change_theme_mode(self, e): + self.page.theme_mode = ft.ThemeMode.DARK if self.page.theme_mode == ft.ThemeMode.LIGHT else ft.ThemeMode.LIGHT + self.choose_them_color_btn.icon = ft.Icons.DARK_MODE if self.page.theme_mode == ft.ThemeMode.LIGHT else ft.Icons.SUNNY + self.choose_them_color_btn.update() + self.placeholder.bgcolor = ft.Colors.BLUE_200 if self.page.theme_mode == ft.ThemeMode.LIGHT else ft.Colors.BLUE_800 + self.placeholder.update() + self.page.update() + + + async def set_value(self, key, value): + await ft.SharedPreferences().set(key, str(value)) + + def build(self): + return ft.Container( + content=ft.Column( + [ + # ft.Row( + # [ + # self.choose_them_color_btn + # ], + # width=350, + # alignment=ft.MainAxisAlignment.END, + # spacing=20 + # ), + ft.Row( + [ + ft.Image( + src = "images/logo_juridic_block.png", + width=250 + ), + ], + alignment=ft.MainAxisAlignment.CENTER, + width=350, + ), + self.placeholder, + ], + alignment=ft.MainAxisAlignment.CENTER, + spacing=20, + expand=True + ), + padding=20, + expand=True + ) \ No newline at end of file diff --git a/client/pages/auth/forgot_password.py b/client/pages/auth/forgot_password.py new file mode 100644 index 0000000..cfa9fb9 --- /dev/null +++ b/client/pages/auth/forgot_password.py @@ -0,0 +1,116 @@ +import flet as ft +from helpers.emails import send_gmail +from helpers.validations import Validations +import requests +import string +import secrets +import os + +class ForgotPassword: + def __init__(self, page: ft.Page, login, auth): + self.page = page + self.login = login + self.auth = auth + self.email = ft.TextField(label="E-mail", expand=True) + self.code = ft.TextField(label="Codul de securitate", expand=True) + self.password = ft.TextField(label="Parola", password=True, can_reveal_password=True, expand=True) + self.confirm_password = ft.TextField(label="Confirmați parola", password=True, can_reveal_password=True, expand=True) + self.error_message = ft.Text(color=ft.Colors.RED) + self.inserted_code = ft.TextField(label="Codul de verificare primit pe email", expand=True) + + self.otp_code = self._generate_numeric_code() + self.validate = Validations(self.error_message, self.page) + + self.title = ft.Text( + "Ai uitat parola?", + text_align=ft.TextAlign.CENTER, + size=20, + weight=ft.FontWeight.BOLD, + width=350 + ) + + self.main_column = ft.Column( + [ + self.title, + self.email, + self.error_message, + ft.Button("Recupereaza parola", on_click=self.send_code_on_email), + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ) + self.go_to_login = ft.TextButton("Mergeti la autentificare", on_click=self.on_go_to_login_btn_click, width=350) + + self.page_column = ft.Column( + [ + self.main_column, + ft.Text(), + self.go_to_login + ] + ) + + 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.validate.is_valid_email(self.email.value): + self.main_column.controls.clear() + self.main_column.controls.append(self.title) + self.main_column.controls.append(self.inserted_code) + self.main_column.controls.append(self.error_message) + self.main_column.controls.append("Verifica", width=150, on_click=self.verfy_code) + self.main_column.update() + #print(self.otp_code) + send_gmail( + to_email=self.email.value, + subject="Cod de verificare", + body=f"Codul tau de verificare este: {self.otp_code}" + ) + + def verfy_code(self, e): + inserted_code = self.inserted_code.value + if inserted_code == self.otp_code: + self.main_column.controls.clear() + self.main_column.controls.append(self.title) + self.main_column.controls.append(self.password) + self.main_column.controls.append(self.confirm_password) + self.main_column.controls.append(self.error_message) + self.main_column.controls.append( + ft.Button("Salveaza", width=150, on_click=self.on_save_btn_click) + ) + self.error_message.value = '' + else: + self.error_message.value = "Code invalid sau expirat!" + print(inserted_code) + print(self.otp_code) + + def on_go_to_login_btn_click(self, e): + self.auth.placeholder.content = self.login.build() + + def on_save_btn_click(self, e): + password = self.password.value + repeat_password = self.confirm_password.value + if self.validate.is_password_strong(password): + if self.validate.check_repeat_password(password, repeat_password): + base_url = self.page.session.store.get('api_base_url') + token = os.getenv('PASSWORD_TOKEN') + response = requests.post(f'{base_url}/auth/update_passwrod', json={"email": self.email.value, "password": password, "token": token}) + if response.status_code == 200: + self.error_message.value = 'Parola a fost salvata cu succes. Acum va puteti autentifica!' + self.error_message.color = ft.Colors.GREEN + self.error_message.update() + + self.page_column.controls.clear() + self.page_column.controls.append(self.title) + self.page_column.controls.append(self.error_message) + self.page_column.controls.append(self.go_to_login) + self.go_to_login.content = "Mergeti la autentificare" + else: + self.error_message.value = 'Recuperarea parolei a esuat. Va rugam conactati administratorul.' + self.error_message.color = ft.Colors.RED + self.error_message.update() + + def build(self): + return self.page_column + + \ No newline at end of file diff --git a/client/pages/auth/login.py b/client/pages/auth/login.py new file mode 100644 index 0000000..7cfca7f --- /dev/null +++ b/client/pages/auth/login.py @@ -0,0 +1,85 @@ +import flet as ft +from pages.auth.register import Register +from pages.auth.forgot_password import ForgotPassword +import requests +from pages.auth.verify_code import VerifyCode + +class Login: + def __init__(self, page: ft.Page, auth): + self.auth = auth + self.page = page + self.email = ft.TextField(label="E-mail", expand=True) + self.password = ft.TextField(label='Parola', password=True, can_reveal_password=True, expand=True) + self.error_message = ft.Text(color=ft.Colors.RED) + self.verify_code = VerifyCode(self.page, self.auth) + self.keep_me_auth = ft.Checkbox(label='Pastreaza-ma autentificat', on_change=self.on_keep_me_authenticated) + + def on_register_btn_click(self, e): + self.register = Register(self.page, self, self.auth) + self.auth.placeholder.content = self.register.build() + + def on_forgot_password_btn_click(self, e): + self.forgot_password = ForgotPassword(self.page, self, self.auth) + self.auth.placeholder.content = self.forgot_password.build() + + async def on_login_btn_click(self, e): + email = self.email.value + self.page.session.store.set('email',email) + if not email: + self.error_message.value = 'Toate campurile sunt obligatorii!' + print('email not found') + return + password = self.password.value + if not password: + self.error_message.value = 'Toate campurile sunt obligatorii!' + print('password not found') + return + base_url = self.page.session.store.get('api_base_url') + + response = requests.post(f'{base_url}/auth/login', json={"email": email, "password": password}) + if response.status_code == 200: + self.auth.placeholder.content = self.verify_code.build() + else: + self.error_message.value = 'Credentiale incorecte!' + print('no user found') + + async def set_value(self, key, value): + await ft.SharedPreferences().set(key, str(value)) + + def on_keep_me_authenticated(self, e): + self.page.session.store.set('keep_me_authenticated', e.data) + + def build(self): + return ft.Column( + [ + ft.Text( + 'Bine ati venit!', + text_align=ft.TextAlign.CENTER, + size=20, + weight=ft.FontWeight.BOLD, + width=350 + ), + self.email, + self.password, + self.error_message, + ft.Button( + 'Autentificare', + on_click=self.on_login_btn_click + ), + ft.Row([self.keep_me_auth], alignment=ft.MainAxisAlignment.CENTER), + 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, + ) + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ) \ No newline at end of file diff --git a/client/pages/auth/register.py b/client/pages/auth/register.py new file mode 100644 index 0000000..dc58f18 --- /dev/null +++ b/client/pages/auth/register.py @@ -0,0 +1,81 @@ +import flet as ft +from helpers.validations import Validations +import requests +import time + +class Register: + def __init__(self, page, login, auth): + self.page = page + self.login = login + self.auth = auth + + self.email = ft.TextField(label="E-mail", expand=True) + self.password = ft.TextField(label="Parola", password=True, can_reveal_password=True, expand=True) + self.confirm_password = ft.TextField(label="Confirmați parola", password=True, can_reveal_password=True, expand=True) + self.error_message = ft.Text(color=ft.Colors.RED) + + self.validation = Validations(self.error_message, self.page) + + def on_register_btn_click(self, e): + email = self.email.value + password = self.password.value + repeat_password = self.confirm_password.value + if self.validation.are_all_fields_inserted(email, password, repeat_password): + print("All fileds are inserted") + if self.validation.is_valid_email(email): + print("Email valid", email) + if self.validation.is_password_strong(password): + print("Password is strong") + if self.validation.check_repeat_password(password, repeat_password): + print('Password Valid!') + self.error_message.value = "Validam datele, va rugam asteptati!" + self.error_message.color = ft.Colors.WHITE + self.error_message.update() + API_BASE_URL = self.page.session.store.get('api_base_url') + response = requests.post(f"{API_BASE_URL}/auth/register", json={ + "workspace_id": 0, + "email": self.email.value, + "password": self.password.value + }) + if response.status_code == 201: + self.error_message.value = "Inregistrare finalizata, acum va puteti autentifica!" + self.error_message.color = ft.Colors.WHITE + self.error_message.update() + #time.sleep(3) + #self.auth.placeholder.content = self.login.build() + else: + self.error_message.value = "Exista deja un cont pentru aceasta adresa de e-mail" + self.error_message.update() + + + def on_go_to_login_btn_click(self, e): + self.auth.placeholder.content = self.login.build() + + + def build(self): + return ft.Column( + [ + ft.Text( + "Creaza cont", + text_align=ft.TextAlign.CENTER, + size=20, + weight=ft.FontWeight.BOLD, + width=350 + ), + self.email, + self.password, + self.confirm_password, + self.error_message, + ft.Button( + "Inregistreaza-te", + on_click=self.on_register_btn_click + ), + ft.Text(), + ft.TextButton( + "Accesati Autentificare", + on_click=self.on_go_to_login_btn_click, + width=350 + ) + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ) \ No newline at end of file diff --git a/client/pages/auth/verify_code.py b/client/pages/auth/verify_code.py new file mode 100644 index 0000000..d2a0ec7 --- /dev/null +++ b/client/pages/auth/verify_code.py @@ -0,0 +1,60 @@ +import flet as ft +import requests +import json + +class VerifyCode: + def __init__(self, page: ft.Page, auth): + self.page = page + self.auth = auth + self.code = ft.TextField(label="Codul de verificare primit pe email") + self.verify_btn = ft.Button( + "Verifica", + on_click=self.on_verify_btn_click + ) + self.error_message = ft.Text(color=ft.Colors.RED) + + async def on_verify_btn_click(self, e): + code = self.code.value + if len(self.code.value) > 0: + API_BASE_URL = self.page.session.store.get('api_base_url') + self.email = self.page.session.store.get('email') + response = requests.post(f"{API_BASE_URL}/auth/verify_code", json={"email": self.email, "code": code}) + if response.status_code == 200: + self.error_message.value = "Codul a fost verificat cu succes. Va puteti autentifica." + token = response.json().get("access_token") + self.page.session.store.set('token', token) + response = requests.get( + f"{API_BASE_URL}/auth/me", + headers={"Authorization": f"Bearer {token}"}, + timeout=10, + ) + user = json.loads(response.text) + print(user) + self.page.session.store.set('user', user) + + # Verificăm dacă utilizatorul a bifat "Keep me authenticated" + keep_me = self.page.session.store.get('keep_me_authenticated') + if keep_me == True or keep_me == "true": + await self.set_value("mi_user_id", user['id']) + await self.set_value("mi_tocken", token) + + self.page.go('/home') + else: + self.error_message.value = "Code invalid sau expirat!" + else: + self.error_message.value = "Code invalid sau expirat!" + + async def set_value(self, key, value): + await ft.SharedPreferences().set(key, str(value)) + + def build(self): + return ft.Container( + content=ft.Column( + [ + self.code, + self.error_message, + self.verify_btn + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ) + ) \ No newline at end of file diff --git a/client/pages/dashboard/.DS_Store b/client/pages/dashboard/.DS_Store new file mode 100644 index 0000000..0683e0b Binary files /dev/null and b/client/pages/dashboard/.DS_Store differ diff --git a/client/pages/dashboard/__pycache__/home.cpython-313.pyc b/client/pages/dashboard/__pycache__/home.cpython-313.pyc new file mode 100644 index 0000000..00729ed Binary files /dev/null and b/client/pages/dashboard/__pycache__/home.cpython-313.pyc differ diff --git a/client/pages/dashboard/home.py b/client/pages/dashboard/home.py new file mode 100644 index 0000000..03dfd72 --- /dev/null +++ b/client/pages/dashboard/home.py @@ -0,0 +1,61 @@ +import flet as ft +import requests + + +from navigation.ba import NavigationBA +from navigation.user import NavigationUser + +class Home: + def __init__(self, page: ft.Page): + self.page = page + self.user = self.page.session.store.get('user') + self.navigation_ba = NavigationBA(self.page, self) + self.navigation_user = NavigationUser(self.page, self) + + self.placeholder = ft.Container( + content=self.build_documente_juridice(), + padding=10, + expand=True, + ) + + def build_destinations(self): + if self.user['role'] == 'user': + return self.navigation_user.build() + if self.user['role'] == 'ba': + return self.navigation_ba.build() + + def build_documente_juridice(self): + if self.user['role'] == 'user': + return self.navigation_user.build_documente_juridice() + if self.user['role'] == 'ba': + return self.navigation_ba.build_documente_juridice() + + async def on_nav_changed(self, e): + if self.user['role'] == 'user': + return await self.navigation_user.on_nav_change(e) + if self.user['role'] == 'ba': + return await self.navigation_ba.on_nav_change(e) + + + async def build(self): + return ft.Container( + content=ft.Row( + [ + ft.NavigationRail( + selected_index=0, + label_type=ft.NavigationRailLabelType.ALL, + min_width=100, + min_extended_width=400, + leading=ft.Image(src='images/logo_juridic_block.png', width=100), + group_alignment=-0.9, + #extended=True, + destinations=self.build_destinations(), + on_change = self.on_nav_changed, + ), + ft.VerticalDivider(width=1), + self.placeholder, + ], + expand=True + ), + expand=True + ) \ No newline at end of file diff --git a/client/pages/documents/__pycache__/ba.cpython-313.pyc b/client/pages/documents/__pycache__/ba.cpython-313.pyc new file mode 100644 index 0000000..c093b59 Binary files /dev/null and b/client/pages/documents/__pycache__/ba.cpython-313.pyc differ diff --git a/client/pages/documents/__pycache__/client.cpython-313.pyc b/client/pages/documents/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000..0ba006a Binary files /dev/null and b/client/pages/documents/__pycache__/client.cpython-313.pyc differ diff --git a/client/pages/documents/__pycache__/custom.cpython-313.pyc b/client/pages/documents/__pycache__/custom.cpython-313.pyc new file mode 100644 index 0000000..e346d16 Binary files /dev/null and b/client/pages/documents/__pycache__/custom.cpython-313.pyc differ diff --git a/client/pages/documents/__pycache__/documents.cpython-313.pyc b/client/pages/documents/__pycache__/documents.cpython-313.pyc new file mode 100644 index 0000000..bba328d Binary files /dev/null and b/client/pages/documents/__pycache__/documents.cpython-313.pyc differ diff --git a/client/pages/documents/__pycache__/home.cpython-313.pyc b/client/pages/documents/__pycache__/home.cpython-313.pyc new file mode 100644 index 0000000..88308c2 Binary files /dev/null and b/client/pages/documents/__pycache__/home.cpython-313.pyc differ diff --git a/client/pages/documents/__pycache__/standard.cpython-313.pyc b/client/pages/documents/__pycache__/standard.cpython-313.pyc new file mode 100644 index 0000000..7ccd9f7 Binary files /dev/null and b/client/pages/documents/__pycache__/standard.cpython-313.pyc differ diff --git a/client/pages/documents/ba.py b/client/pages/documents/ba.py new file mode 100644 index 0000000..5476b8e --- /dev/null +++ b/client/pages/documents/ba.py @@ -0,0 +1,374 @@ +import flet as ft +import requests +from datetime import datetime +from helpers.document_status import DocumentsStatus +from helpers.emails import send_gmail +from dataclasses import dataclass, field + +@dataclass +class State: + + file_picker: ft.FilePicker | None = None + picked_files: list[ft.FilePickerFile] = field(default_factory=list) + +state = State() + +class Documents: + def __init__(self, page: ft.Page, home): + self.page = page + self.home = home + self.base_url = self.page.session.store.get('api_base_url') + self.token = self.page.session.store.get('token') + self.user = self.page.session.store.get('user') + self.user_id = self.user['id'] if self.user else None + + self.all_requests = [] + self.current_selected_request = None + + + # Elemente interfață: Căutare și Listă + self.search_bar = ft.TextField( + label="Căutare solicitări (Text solicitare sau comentariu)", + on_submit=self._on_search_change, + prefix_icon=ft.Icons.SEARCH, + expand=True + ) + + self.status_filter_dropdown = ft.Dropdown( + label="Filtrează după status", + options=[ + ft.dropdown.Option("all", "Toate"), + ft.dropdown.Option(DocumentsStatus.NEW, DocumentsStatus.get_label(DocumentsStatus.NEW)), + ft.dropdown.Option(DocumentsStatus.ANALISE, DocumentsStatus.get_label(DocumentsStatus.ANALISE)), + ft.dropdown.Option(DocumentsStatus.IN_PROGRESS, DocumentsStatus.get_label(DocumentsStatus.IN_PROGRESS)), + ft.dropdown.Option(DocumentsStatus.WAITING_FOR_PAYMENT, DocumentsStatus.get_label(DocumentsStatus.WAITING_FOR_PAYMENT)), + ft.dropdown.Option(DocumentsStatus.COMPLETED, DocumentsStatus.get_label(DocumentsStatus.COMPLETED)), + ft.dropdown.Option(DocumentsStatus.CANCELED, DocumentsStatus.get_label(DocumentsStatus.CANCELED)), + ], + value="all", + on_select=self._on_status_filter_change, + expand=True + ) + + self.requests_list_view = ft.ListView( + expand=True, + spacing=10, + ) + + # Elemente panou detalii + self.req_id_text = ft.Text("", size=18, weight=ft.FontWeight.BOLD) + self.req_text_display = ft.Text("", selectable=True) + + self.price_field = ft.TextField( + label="Preț stabilit (Lei)", + width=200, + keyboard_type=ft.KeyboardType.NUMBER, + ) + + self.status_dropdown = ft.Dropdown( + label="Schimbă Status", + width=250, + options=[ + ft.dropdown.Option(DocumentsStatus.NEW, DocumentsStatus.get_label(DocumentsStatus.NEW)), + ft.dropdown.Option(DocumentsStatus.ANALISE, DocumentsStatus.get_label(DocumentsStatus.ANALISE)), + ft.dropdown.Option(DocumentsStatus.IN_PROGRESS, DocumentsStatus.get_label(DocumentsStatus.IN_PROGRESS)), + ft.dropdown.Option(DocumentsStatus.WAITING_FOR_PAYMENT, DocumentsStatus.get_label(DocumentsStatus.WAITING_FOR_PAYMENT)), + ft.dropdown.Option(DocumentsStatus.COMPLETED, DocumentsStatus.get_label(DocumentsStatus.COMPLETED)), + ft.dropdown.Option(DocumentsStatus.CANCELED, DocumentsStatus.get_label(DocumentsStatus.CANCELED)), + ] + ) + + self.comment_field = ft.TextField( + label="Adaugă răspuns/comentariu către client", + multiline=True, + min_lines=3, + expand=True + ) + + self.doc_id_info = ft.Text("Document Final: Niciunul", color=ft.Colors.GREY_700) + + self.details_panel = ft.Column( + [ + self.req_id_text, + ft.Divider(), + ft.Text("Descriere Solicitare Client:", weight=ft.FontWeight.BOLD), + ft.Container(content=self.req_text_display, padding=10, bgcolor=ft.Colors.GREY_50, border_radius=5), + ft.Divider(), + ft.Row([self.status_dropdown, self.price_field]), + ft.FilledButton( + "Salvează Modificări Status/Preț", + icon=ft.Icons.SAVE, + on_click=self._save_metadata + ), + ft.Divider(), + ft.Text("Comunicare istoric:", weight=ft.FontWeight.BOLD), + ft.Row([ + self.comment_field, + ft.IconButton(ft.Icons.SEND_ROUNDED, on_click=self._add_comment, tooltip="Trimite răspuns") + ]), + ft.Divider(), + ft.Text("Finalizare și Încărcare Document:", weight=ft.FontWeight.BOLD), + ft.Row([ + ft.FilledButton( + "Încarcă Document Final", + icon=ft.Icons.UPLOAD_FILE, + on_click=self._handle_file_upload, + bgcolor=ft.Colors.GREEN_700 + ), + self.doc_id_info + ]) + ], + visible=False, + expand=True, + scroll=ft.ScrollMode.AUTO + ) + + self._load_requests() + + def _load_requests(self): + """Preia toate solicitările de documente custom de la server.""" + try: + response = requests.get( + f"{self.base_url}/documents/customs/requests", + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + self.all_requests = response.json()[::-1] + self._populate_list(self.all_requests) + except Exception as e: + print(f"Error loading requests: {e}") + + def _populate_list(self, items): + self.requests_list_view.controls = [] + if not items: + self.requests_list_view.controls.append(ft.Text("Nicio solicitare găsită.", italic=True)) + else: + for req in items: + self.requests_list_view.controls.append( + ft.Container( + content=ft.Column([ + ft.Text(f"Solicitare #{req['id']}", weight=ft.FontWeight.BOLD), + ft.Text(f"Status: {DocumentsStatus.get_label(req['status'])}", size=12), + ft.Text(f"Client ID: {req['client_id']}", size=11, color=ft.Colors.GREY_600), + ], spacing=2), + padding=15, + border_radius=10, + bgcolor=ft.Colors.WHITE, + border=ft.Border.all(1, ft.Colors.GREY_300), + ink=True, + on_click=lambda e, r=req: self._show_details(r) + ) + ) + self.page.update() + + def _show_details(self, req_summary): # Renamed from _show_details to _show_details_from_summary + """Preia datele proaspete de la server pentru solicitarea selectată.""" + try: + response = requests.get( + f"{self.base_url}/documents/customs/requests/{req_summary['id']}", + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + req = response.json() + self.current_selected_request = req + self.req_id_text.value = f"Procesare Solicitare #{req['id']}" + self.req_text_display.value = req['request_text'] + self.status_dropdown.value = req['status'] + self.price_field.value = str(req['price']) if req['price'] is not None else "" + self.doc_id_info.value = f"ID Document asociat: {req['document_id']}" if req['document_id'] else "Document Final: Niciunul" + self.details_panel.visible = True + self.page.update() + except Exception as e: + print(f"Eroare la preluarea detaliilor solicitării: {e}") + + def _apply_filters(self): + """Aplică filtrele de căutare și status și actualizează lista.""" + query = self.search_bar.value.lower().strip() + selected_status = self.status_filter_dropdown.value + + filtered = [] + for r in self.all_requests: + matches_query = query in r['request_text'].lower() or query in str(r['id']).lower() + matches_status = (selected_status == "all" or r['status'] == selected_status) + + if matches_query and matches_status: + filtered.append(r) + self._populate_list(filtered) + + def _on_search_change(self, e): + """Declanșează filtrarea la schimbarea textului de căutare.""" + self._apply_filters() + + def _on_status_filter_change(self, e): + """Declanșează filtrarea la schimbarea statusului selectat.""" + self.current_selected_request = None # Clear details when filter changes + self.details_panel.visible = False + self._apply_filters() + + def _save_metadata(self, e): + """Salvează prețul și statusul solicitării.""" + if not self.current_selected_request: return + + price_val = self.price_field.value.strip() if self.price_field.value else "" + price = None + + if price_val: + try: + price = float(price_val.replace(',', '.')) + except ValueError: + self.page.show_dialog(ft.SnackBar(ft.Text("Prețul trebuie să fie un număr!"))) + return + + payload = { # Removed from here + "status": self.status_dropdown.value, + "price": price, + "expert_id": self.user_id + } + + # Actualizăm datele și, în caz de succes, trimitem notificarea + if self._update_request_api(payload): + try: + # Preluăm email-ul clientului pentru a-l notifica + client_id = self.current_selected_request.get('client_id') + user_resp = requests.get( + f"{self.base_url}/users/{client_id}", + headers={'Authorization': f'Bearer {self.token}'} + ) + + if user_resp.status_code == 200: + client_email = user_resp.json().get('email') + status_label = DocumentsStatus.get_label(payload['status']) + + price_info = f"Preț stabilit: {price} Lei." if price is not None else "Prețul va fi stabilit după analiză." + + subject = f"Actualizare status solicitare #{self.current_selected_request['id']}" + body = ( + f"Bună ziua,\n\n" + f"Vă informăm că statusul solicitării dumneavoastră #{self.current_selected_request['id']} " + f"a fost actualizat la: {status_label}.\n" + f"{price_info}\n\n" + f"Vă mulțumim,\nEchipa JuridicBloc" + ) + send_gmail(to_email=client_email, subject=subject, body=body) + except Exception as mail_err: + print(f"Eroare la trimiterea notificării email: {mail_err}") + + def _add_comment(self, e): + """Adaugă un comentariu în istoricul conversației solicitării.""" + if not self.current_selected_request or not self.comment_field.value.strip(): return + + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + comment = self.comment_field.value.strip() + updated_text = f"{self.current_selected_request['request_text']}\n\n--- Răspuns Expert ({timestamp}):\n{comment}" + + payload = { + "request_text": updated_text, + "expert_id": self.user_id + } + self._update_request_api(payload) + self.comment_field.value = "" + + def _update_request_api(self, payload): + try: + req_id = self.current_selected_request['id'] + response = requests.put( + f"{self.base_url}/documents/customs/requests/update/{req_id}", + json=payload, + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + self.page.show_dialog(ft.SnackBar(ft.Text("Modificări salvate cu succes."))) + self.current_selected_request.update(payload) + self._load_requests() + self._show_details(self.current_selected_request) + client_id = self.current_selected_request.get('client_id') + user_resp = requests.get( + f"{self.base_url}/users/{client_id}", + headers={'Authorization': f'Bearer {self.token}'} + ) + if user_resp.status_code == 200: + subject = f"Actualizare status solicitare #{self.current_selected_request['id']}" + client_email = user_resp.json().get('email') + body = ( + f"Bună ziua,\n\n" + f"Vă informăm că ati primit un raspuns solicitării dumneavoastră #{self.current_selected_request['id']} " + + f"Vă mulțumim,\nEchipa JuridicBloc" + ) + send_gmail(to_email=client_email, subject=subject, body=body) + return True + except Exception as e: + print(f"Update failed: {e}") + return False + + async def _handle_file_upload(self, e): + if not self.current_selected_request: return + try: + state.file_picker = ft.FilePicker() + files = await state.file_picker.pick_files(allow_multiple=False) + print("Picked file:", files) + + state.picked_files = files + uploaded_file_name = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{state.picked_files[0].name}" + await state.file_picker.upload( + files=[ + ft.FilePickerUploadFile( + name=file.name, + upload_url=self.page.get_upload_url(uploaded_file_name, 60), + + ) + + for file in state.picked_files + ] + ) + + # 2. Înregistrare în tabela documents_custom + + reg_resp = requests.post( + f"{self.base_url}/documents/customs/add", + json={"name": f"Document Final Solicitare #{self.current_selected_request['id']}", "path": uploaded_file_name}, + headers={'Authorization': f'Bearer {self.token}'} + ) + + if reg_resp.status_code == 201: + doc_id = reg_resp.json().get('id') + # 3. Legare document de solicitare și marcare ca finalizat + self._update_request_api({ + "document_id": doc_id, + "status": DocumentsStatus.COMPLETED, + "expert_id": self.user_id, + }) + except Exception as ex: + print(f"Error during file upload or registration: {ex}") + + def build(self): + return ft.Container( + content=ft.Row( + [ + ft.Column( + [ + ft.Row( + [ + #self.search_bar, + self.status_filter_dropdown, + ] + ), + self.requests_list_view + ], + width=400 + ), + ft.VerticalDivider(width=1), + ft.Container( + content=self.details_panel, + expand=True, + padding=20, + bgcolor=ft.Colors.WHITE, + border_radius=10 + ) + ], + expand=True + ), + expand=True, + bgcolor=ft.Colors.GREY_100, + padding=10 + ) \ No newline at end of file diff --git a/client/pages/documents/custom.py b/client/pages/documents/custom.py new file mode 100644 index 0000000..4998d6c --- /dev/null +++ b/client/pages/documents/custom.py @@ -0,0 +1,350 @@ +import flet as ft +import requests +import json +from datetime import datetime +from helpers.document_status import DocumentsStatus +from helpers.emails import send_gmail + +class Documents: + def __init__(self, page: ft.Page, home): + self.page = page + self.home = home # Keep reference to home for potential page updates + self.base_url = self.page.session.store.get('api_base_url') + self.token = self.page.session.store.get('token') + self.user = self.page.session.store.get('user') + self.user_id = self.user['id'] if self.user else None # Assuming user object has 'id' + + self.all_requests = [] # To store all fetched requests + self.current_selected_request = None # To store the request currently displayed in details + + # 1. "Solicita document personalizat" button and popup + self.request_text_field = ft.TextField( + label="Descrie solicitarea ta", + multiline=True, + min_lines=5, + max_lines=7, + expand=True + ) + self.new_request_dialog = ft.AlertDialog( + modal=True, + title=ft.Text("Solicita document personalizat"), + content=ft.Column( + [ + self.request_text_field, + ft.Text("Vei fi notificat cu privire la statusul solicitării și prețul stabilit de expert.") + ], + tight=True, + height=200 + ), + actions=[ + ft.FilledButton("Solicita", on_click=self._submit_new_request), + ft.FilledButton("Anuleaza", on_click=self._close_dialog, bgcolor=ft.Colors.GREY) + ], + actions_alignment=ft.MainAxisAlignment.END + ) + + # 2. Search bar + self.search_bar = ft.TextField( + label="Cauta in solicitarile mele", + on_change=self._on_search_change, # Use on_change for live filtering + expand=True + ) + + # 3. List of requests + self.requests_list_view = ft.ListView( + expand=True, + spacing=10, + padding=10 + ) + self.no_requests_message = ft.Text( + "Nu aveti nici o solicitare activa. Pentru a crea o solicitare apasati butonul 'Solicita document personalizat'.", + text_align=ft.TextAlign.CENTER, + color=ft.Colors.GREY_600, + size=16 + ) + + # 4. Details view for a selected request + self.request_details_text = ft.Text("", selectable=True) + self.request_status_text = ft.Text("") + self.request_price_text = ft.Text("") + self.pay_button = ft.FilledButton( + "Plateste", + on_click=self._on_pay_button_click, + disabled=True, # Will be enabled based on status + visible=False # Initially hidden + ) + self.comment_text_field = ft.TextField( + label="Adauga un comentariu", + multiline=True, + min_lines=2, + max_lines=4, + expand=True + ) + self.add_comment_button = ft.FilledButton( + "Adauga Comentariu", + on_click=self._add_comment_to_request, + disabled=True # Disabled until a request is selected + ) + self.selected_request_details_column = ft.Column( + [ + ft.Text("Detalii Solicitare", size=20, weight=ft.FontWeight.BOLD), + ft.Divider(), + ft.Text("Text Solicitare:", weight=ft.FontWeight.BOLD), + self.request_details_text, + ft.Text("Status:", weight=ft.FontWeight.BOLD), + self.request_status_text, + ft.Text("Pret:", weight=ft.FontWeight.BOLD), + self.request_price_text, + self.pay_button, + ft.Divider(), + ft.Text("Adauga Comentariu:", weight=ft.FontWeight.BOLD), + ft.Row([self.comment_text_field, self.add_comment_button]), + ], + expand=True, + visible=False # Initially hidden + ) + + # Initial data load + self._load_requests() + + def _load_requests(self): + """Fetches requests from the API and updates the UI.""" + if not self.user_id: + print("User ID not available. Cannot load requests.") + return + + try: + response = requests.get( + f"{self.base_url}/documents/customs/requests/client", + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + self.all_requests = response.json()[::-1] + self._populate_requests_list(self.all_requests) + else: + print(f"Error fetching client requests: {response.status_code} - {response.text}") + self.requests_list_view.controls = [self.no_requests_message] + except requests.exceptions.RequestException as e: + print(f"Network error fetching client requests: {e}") + self.requests_list_view.controls = [self.no_requests_message] + self.page.update() + + def _populate_requests_list(self, requests_to_display): + """Populates the ListView with request items.""" + if not requests_to_display: + self.requests_list_view.controls = [self.no_requests_message] + self.selected_request_details_column.visible = False + self.add_comment_button.disabled = True + self.comment_text_field.value = "" + self.comment_text_field.disabled = True + return + + items = [] + for req in requests_to_display: + items.append( + ft.Container( + content=ft.Column( + [ + ft.Text(f"Solicitare ID: {req['id']}", weight=ft.FontWeight.BOLD), + ft.Text(f"Status: {DocumentsStatus.get_label(req['status'])}"), + ft.Text(f"Data: {req['created_at'].split('T')[0] if req['created_at'] else 'N/A'}"), + ] + ), + padding=10, + border_radius=5, + bgcolor=ft.Colors.BLUE_50 if req['status'] == DocumentsStatus.NEW else ft.Colors.GREY_100, + ink=True, + on_click=lambda e, request_data=req: self._display_request_details(request_data) + ) + ) + self.requests_list_view.controls = items + self.selected_request_details_column.visible = False # Hide details when list is repopulated + self.add_comment_button.disabled = True + self.comment_text_field.value = "" + self.comment_text_field.disabled = True + self.page.update() + + def _display_request_details(self, request_summary): + """Preia datele proaspete de la server pentru solicitarea selectată.""" + try: + response = requests.get( + f"{self.base_url}/documents/customs/requests/{request_summary['id']}", + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + request_data = response.json() + self.current_selected_request = request_data + self.request_details_text.value = request_data.get('request_text', 'N/A') + self.request_status_text.value = DocumentsStatus.get_label(request_data.get('status', 'N/A')) + self.request_price_text.value = f"{request_data.get('price', 0.0):.2f} Lei" if request_data.get('price') else "N/A" + + # Handle Pay button visibility and state + if request_data.get('status') == DocumentsStatus.WAITING_FOR_PAYMENT: + self.pay_button.visible = True + self.pay_button.disabled = False + else: + self.pay_button.visible = False + self.pay_button.disabled = True + + # Enable comment section + self.add_comment_button.disabled = False + self.comment_text_field.disabled = False + self.comment_text_field.value = "" # Clear previous comment input + + self.selected_request_details_column.visible = True + self.page.update() + except Exception as e: + print(f"Error fetching request details: {e}") + + def _open_new_request_dialog(self, e): + self.request_text_field.value = "" # Clear previous input + self.page.show_dialog(self.new_request_dialog) + self.page.update() + + def _submit_new_request(self, e): + request_text = self.request_text_field.value.strip() + if not request_text: + # Optionally show an error message to the user + print("Request text cannot be empty.") + return + + try: + response = requests.post( + f"{self.base_url}/documents/customs/requests/add", + json={"request_text": request_text}, + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 201: + print("Request submitted successfully!") + req_id = response.json().get("id") + # Notificăm BA/Management despre o solicitare nouă + try: + subject = f"Solicitare nouă document personalizat: #{req_id}" + body = ( + f"Clientul {self.user.get('email')} a creat o solicitare nouă.\n\n" + f"Descriere solicitare:\n{request_text}\n\n" + f"Vă rugăm să accesați panoul de administrare pentru preluare." + ) + #send_gmail(to_email="office@juridicbloc.ro", subject=subject, body=body) + send_gmail(to_email="macamete.robert@gmail.com", subject=subject, body=body) + except Exception as mail_err: + print(f"Eroare notificare mail: {mail_err}") + + self._close_dialog(e) + self._load_requests() # Reload the list to show the new request + else: + print(f"Error submitting request: {response.status_code} - {response.text}") + except requests.exceptions.RequestException as err: + print(f"Network error submitting request: {err}") + self.page.update() + + def _add_comment_to_request(self, e): + if not self.current_selected_request: + print("No request selected to add a comment.") + return + + comment = self.comment_text_field.value.strip() + if not comment: + print("Comment text cannot be empty.") + return + + request_id = self.current_selected_request['id'] + current_request_text = self.current_selected_request.get('request_text', '') + + # Append new comment with timestamp + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + new_request_text = f"{current_request_text}\n\n--- Comentariu client ({timestamp}):\n{comment}" + + try: + response = requests.put( + f"{self.base_url}/documents/customs/requests/update/{request_id}", + json={"request_text": new_request_text}, # Only sending request_text for update + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + print("Comment added successfully!") + # Update the local request object and re-display details + self.current_selected_request['request_text'] = new_request_text + self._display_request_details(self.current_selected_request) + + # Notificăm Expertul (BA) dacă este alocat + try: + subject = f"Comentariu nou la solicitarea #{request_id}" + body = ( + f"Clientul {self.user.get('email')} a adăugat un comentariu la " + f"solicitarea #{request_id}.\n\n" + f"Mesaj:\n{comment}" + ) + #send_gmail(to_email="office@juridicbloc.ro", subject=subject, body=body) + send_gmail(to_email='macamete.robert@gmail.com', subject=subject, body=body) + except Exception as mail_err: + print(f"Eroare notificare expert: {mail_err}") + + self.comment_text_field.value = "" # Clear comment field + else: + print(f"Error adding comment: {response.status_code} - {response.text}") + except requests.exceptions.RequestException as err: + print(f"Network error adding comment: {err}") + self.page.update() + + def _on_search_change(self, e): + query = self.search_bar.value.strip().lower() + if query: + filtered_requests = [ + req for req in self.all_requests + if query in req.get('request_text', '').lower() or + query in req.get('status', '').lower() or + query in str(req.get('id', '')).lower() + ] + else: + filtered_requests = self.all_requests + self._populate_requests_list(filtered_requests) + + def _on_pay_button_click(self, e): + # Placeholder for payment logic + print(f"Payment button clicked for request ID: {self.current_selected_request['id']}") + self.page.show_dialog(ft.SnackBar( + ft.Text("Funcționalitatea de plată va fi implementată ulterior."), + )) + self.page.update() + + def _close_dialog(self, e): + self.page.pop_dialog() + self.page.update() + + def build(self): + return ft.Container( + content=ft.Row( + [ + # Left Column: New Request Button and Requests List + ft.Column( + [ + ft.FilledButton( + "Solicita document personalizat", + icon=ft.Icons.ADD_TASK, + on_click=self._open_new_request_dialog, + width=300 + ), + ft.Divider(), + self.requests_list_view, + ], + width=350, + expand=False, + alignment=ft.MainAxisAlignment.START + ), + ft.VerticalDivider(width=1), + # Right Column: Search Bar and Request Details + ft.Column( + [ + ft.Row([self.search_bar]), + ft.Divider(), + self.selected_request_details_column, + ], + expand=True + ) + ], + expand=True + ), + expand=True, + padding=10 + ) diff --git a/client/pages/documents/home.py b/client/pages/documents/home.py new file mode 100644 index 0000000..9e008c8 --- /dev/null +++ b/client/pages/documents/home.py @@ -0,0 +1,97 @@ +import flet as ft +from pages.documents.standard import StandardDocuments +from pages.documents.custom import Documents as CustomDocuments + +class DocumentsHome: + def __init__(self, page: ft.Page, home): + self.page = page + self.home = home + + def open_standard_docs(self, e): + """Navighează către lista de documente standard.""" + self.home.placeholder.content = StandardDocuments(self.page, self.home).build() + self.page.update() + + def open_custom_docs(self, e): + """Navighează către sistemul de solicitări personalizate.""" + self.home.placeholder.content = CustomDocuments(self.page, self.home).build() + self.page.update() + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Text( + "Documente Juridice", + size=32, + weight=ft.FontWeight.BOLD, + color=ft.Colors.BLUE_GREY_900 + ), + ft.Text( + "Selectați tipul de serviciu dorit", + size=16, + color=ft.Colors.GREY_700 + ), + ft.Divider(height=40, color=ft.Colors.TRANSPARENT), + ft.Row( + [ + ft.Card( + content=ft.Container( + content=ft.Column( + [ + ft.Icon(ft.Icons.INSERT_DRIVE_FILE, size=60, color=ft.Colors.BLUE_700), + ft.Text("Documente Standard", size=22, weight=ft.FontWeight.BOLD), + ft.Text( + "Modele de contracte, cereri și acte predefinite gata de descărcare.", + text_align=ft.TextAlign.CENTER, + color=ft.Colors.GREY_600 + ), + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ), + padding=40, + on_click=self.open_standard_docs, + ink=True, + border_radius=10, + ), + width=350, + height=350, + elevation=5, + ), + ft.Card( + content=ft.Container( + content=ft.Column( + [ + ft.Icon(ft.Icons.EDIT_NOTE, size=60, color=ft.Colors.ORANGE_700), + ft.Text("Documente Personalizate", size=22, weight=ft.FontWeight.BOLD), + ft.Text( + "Solicită asistență pentru un document adaptat nevoilor tale specifice.", + text_align=ft.TextAlign.CENTER, + color=ft.Colors.GREY_600 + ), + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ), + padding=40, + on_click=self.open_custom_docs, + ink=True, + border_radius=10, + ), + width=350, + height=350, + elevation=5, + ), + ], + alignment=ft.MainAxisAlignment.CENTER, + spacing=40, + ), + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + expand=True + ), + expand=True, + padding=20, + ) \ No newline at end of file diff --git a/client/pages/documents/standard.py b/client/pages/documents/standard.py new file mode 100644 index 0000000..4eca42a --- /dev/null +++ b/client/pages/documents/standard.py @@ -0,0 +1,180 @@ +import flet as ft +import requests + +class StandardDocuments: + def __init__(self, page: ft.Page, home): + self.page = page + self.home = home + self.base_url = self.page.session.store.get('api_base_url') + self.token = self.page.session.store.get('token') + + self.all_documents = [] # Stocăm documentele pentru filtrare locală (căutare după nume) + + # Elemente UI + self.search_bar = ft.TextField( + label="Caută document după nume", + on_change=self._on_search_change, + expand=True, + prefix_icon=ft.Icons.SEARCH, + hint_text="Introdu numele documentului..." + ) + + self.category_dropdown = ft.Dropdown( + label="Filtrează după categorie", + on_select=self._on_category_change, + width=300, + hint_text="Alege o categorie" + ) + + self.documents_list_view = ft.ListView( + expand=True, + spacing=10, + padding=10 + ) + + def _on_search_change(self, e): + """Filtrează lista de documente afișată în funcție de textul din search bar.""" + query = self.search_bar.value.lower().strip() + filtered = [ + doc for doc in self.all_documents + if query in doc['name'].lower() + ] + self._populate_documents_list(filtered) + + def _on_category_change(self, e): + """Reîncarcă documentele atunci când se schimbă categoria selectată.""" + self._load_documents(self.category_dropdown.value) + + def _load_categories(self): + """Preia categoriile de documente de la server.""" + try: + response = requests.get( + f"{self.base_url}/documents/categories", + headers={"Authorization": f"Bearer {self.token}"} + ) + if response.status_code == 200: + user_data = self.page.session.store.get('user') + user_role = user_data.get('role', '').lower() if user_data else "" + + all_cats = response.json() + # Filtrare categorii după rol: utilizatorul vede categoria doar dacă + # rolul său se regăsește în câmpul 'access' (comma-separated string) + filtered_cats = [ + cat for cat in all_cats + if user_role in [r.strip().lower() for r in cat.get('access', '').split(',')] + ] + + self.category_dropdown.options = [ + ft.dropdown.Option(key="all", text="Toate categoriile") + ] + [ + ft.dropdown.Option(key=str(cat['id']), text=cat['name']) + for cat in filtered_cats + ] + self.category_dropdown.value = "all" + self.page.update() + except Exception as ex: + print(f"Error fetching categories: {ex}") + + def _load_documents(self, category_id="all"): + """Preia documentele standard de la server.""" + try: + if category_id == "all": + url = f"{self.base_url}/documents/standards" + else: + url = f"{self.base_url}/documents/standards/category/{category_id}" + + response = requests.get( + url, + headers={"Authorization": f"Bearer {self.token}"} + ) + if response.status_code == 200: + user_data = self.page.session.store.get('user') + user_role = user_data.get('role', '').lower() if user_data else "" + + raw_docs = response.json() + # Filtrare documente după rol: utilizatorul vede documentul doar dacă + # rolul său se regăsește în câmpul 'access' (comma-separated string) + self.all_documents = [ + doc for doc in raw_docs + if user_role in [r.strip().lower() for r in doc.get('access', '').split(',')] + ] + + # Aplicăm și filtrul de căutare dacă există deja text în search bar + query = self.search_bar.value.lower().strip() + filtered = [d for d in self.all_documents if query in d['name'].lower()] + self._populate_documents_list(filtered) + except Exception as ex: + print(f"Error fetching documents: {ex}") + + def _populate_documents_list(self, documents): + """Actualizează interfața cu lista de documente furnizată.""" + self.documents_list_view.controls = [] + if not documents: + self.documents_list_view.controls.append( + ft.Container( + content=ft.Text("Nu s-au găsit documente.", size=16, color=ft.Colors.GREY_600), + alignment=ft.Alignment.CENTER, + padding=20 + ) + ) + else: + for doc in documents: + self.documents_list_view.controls.append( + ft.Container( + content=ft.Row( + [ + ft.Icon(ft.Icons.INSERT_DRIVE_FILE_OUTLINED, color=ft.Colors.BLUE_700), + ft.Column( + [ + ft.Text(doc['name'], weight=ft.FontWeight.BOLD, size=16), + ft.Text(f"Path: {doc['path']}", size=12, color=ft.Colors.GREY_500), + ], + expand=True, + alignment=ft.MainAxisAlignment.CENTER + ), + ft.IconButton( + icon=ft.Icons.DOWNLOAD, + tooltip="Descarcă documentul", + icon_color=ft.Colors.BLUE_700, + on_click=lambda e, d=doc: self.page.run_task(self._download_document, d) + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + bgcolor=ft.Colors.BLUE_50, + padding=15, + border_radius=10, + border=ft.Border.all(1, ft.Colors.BLUE_100), + ink=True + ) + ) + self.page.update() + + async def _download_document(self, doc): + """Deschide URL-ul de download pentru documentul selectat.""" + download_url = f"{self.base_url}/documents/download?path={doc['path']}&token={self.token}" + await self.page.launch_url(download_url) + + def build(self): + # Încărcare inițială a datelor + self._load_categories() + self._load_documents() + + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + self.category_dropdown, + self.search_bar + ], + spacing=20 + ), + ft.Divider(height=1, color=ft.Colors.GREY_300), + self.documents_list_view + ], + expand=True + ), + expand=True, + padding=20 + ) \ No newline at end of file diff --git a/client/pages/settings/__pycache__/abonamente_si_plati.cpython-313.pyc b/client/pages/settings/__pycache__/abonamente_si_plati.cpython-313.pyc new file mode 100644 index 0000000..5378996 Binary files /dev/null and b/client/pages/settings/__pycache__/abonamente_si_plati.cpython-313.pyc differ diff --git a/client/pages/settings/__pycache__/documente_juridice.cpython-313.pyc b/client/pages/settings/__pycache__/documente_juridice.cpython-313.pyc new file mode 100644 index 0000000..9532c0d Binary files /dev/null and b/client/pages/settings/__pycache__/documente_juridice.cpython-313.pyc differ diff --git a/client/pages/settings/__pycache__/payment_and_subscription.cpython-313.pyc b/client/pages/settings/__pycache__/payment_and_subscription.cpython-313.pyc new file mode 100644 index 0000000..0af9318 Binary files /dev/null and b/client/pages/settings/__pycache__/payment_and_subscription.cpython-313.pyc differ diff --git a/client/pages/settings/__pycache__/settings.cpython-313.pyc b/client/pages/settings/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..9e05313 Binary files /dev/null and b/client/pages/settings/__pycache__/settings.cpython-313.pyc differ diff --git a/client/pages/settings/__pycache__/users.cpython-313.pyc b/client/pages/settings/__pycache__/users.cpython-313.pyc new file mode 100644 index 0000000..59eecd3 Binary files /dev/null and b/client/pages/settings/__pycache__/users.cpython-313.pyc differ diff --git a/client/pages/settings/documente_juridice.py b/client/pages/settings/documente_juridice.py new file mode 100644 index 0000000..3a273fd --- /dev/null +++ b/client/pages/settings/documente_juridice.py @@ -0,0 +1,595 @@ +import flet as ft +import requests +from helpers.roles import Roles +from dataclasses import dataclass, field +from datetime import datetime + +@dataclass +class State: + + file_picker: ft.FilePicker | None = None + picked_files: list[ft.FilePickerFile] = field(default_factory=list) + + +state = State() + +class DocumenteJuridice: + def __init__(self, page: ft.Page): + self.page = page + self.editing_category_id = None + self.category_id_to_delete = None + self.document_id_to_delete = None + self.current_category_id = None + + self.search_bar = ft.TextField( + label="Cauta", + on_submit=self.on_search_bar_submit, + expand=True + ) + self.category_name = ft.TextField( + label="Nume categorie", + ) + + self.roles_checkboxes = [ + ft.Checkbox(label=role.upper()) + for role in [ + Roles.USER, Roles.PROPRIETAR, Roles.CENZOR, + Roles.ADMINISTRATOR, Roles.PRESEDINTE, Roles.EXPERT + ] + ] + + self.access_levels = ft.Column( + controls=self.roles_checkboxes, + height=200, + scroll=ft.ScrollMode.AUTO + ) + + self.add_categories_dialog = ft.AlertDialog( + title=ft.Text("Adauga Categorie"), + content=ft.Column( + [ + self.category_name, + ft.Text("Nivele Acces:"), + self.access_levels + ], + height=300, + tight=True + ), + actions=[ + ft.FilledButton( + "Salveaza", + on_click=self.on_save_category_btn_click + ), + ft.FilledButton( + "Anuleaza", + on_click=self.on_cancel_category_btn_click, + bgcolor=ft.Colors.GREY, + ), + ], + ) + + self.delete_confirmation_dialog = ft.AlertDialog( + title=ft.Text("Doriti sa stergeti categoria?"), + content=ft.Text("Stergerea categoriei implica si stergerea tuturor documentelor din aceasta categorie."), + actions=[ + ft.FilledButton( + "Da", + on_click=self.confirm_delete_category + ), + ft.FilledButton( + "Nu", + bgcolor=ft.Colors.GREY, + on_click=self.close_delete_dialog + ), + ], + ) + + self.delete_document_confirmation_dialog = ft.AlertDialog( + title=ft.Text("Doriti sa stergeti documentul?"), + content=ft.Text("Aceasta actiune este permanenta."), + actions=[ + ft.FilledButton( + "Da", + on_click=self.confirm_delete_document + ), + ft.FilledButton( + "Nu", + bgcolor=ft.Colors.GREY, + on_click=self.close_delete_document_dialog + ), + ], + ) + + self.all_categories = self.get_categories() + self.category_list = ft.ListView( + controls=self.create_category_list( + self.all_categories, + self.on_edit_category_btn_click, + self.on_delete_category_btn_click + ), + spacing=10, + expand=True + ) + + self.add_document = ft.Button( + "Adauga Document", + icon=ft.Icons.ADD, + on_click=self.add_new_document + ) + + self.all_documents = [] + self.documents_list = ft.ListView( + spacing=10, + expand=True + ) + + self.category = ft.Dropdown( + label="Selectează Categoria", + options=[ + ft.dropdown.Option(key=str(cat['id']), text=cat['name']) + for cat in self.all_categories + ] + ) + + self.documenet_title = None + self.documenet_displayed_title = ft.TextField(label="Document",read_only=True) + self.add_new_document_dialog = ft.AlertDialog( + title=ft.Text("Adauga document"), + content=ft.Column( + [ + ft.Row( + [ + self.documenet_displayed_title, + ft.Button( + "Incarca", + icon=ft.Icons.UPLOAD, + on_click=self.handle_file_upload + ), + ] + ), + self.category + ], + height=160, + ), + actions=[ + ft.FilledButton( + "Salveaza", + on_click=self.on_save_document_btn_click, + + ), + ft.FilledButton( + "Anuleaza", + bgcolor=ft.Colors.GREY, + on_click=self.on_cancel_save_dialog_btn_click, + ), + ], + ) + + def on_add_btn_click(self, e): + self.editing_category_id = None + self.category_name.value = "" + for cb in self.roles_checkboxes: + cb.value = False + self.add_categories_dialog.title = ft.Text("Adauga Categorie") + self.page.show_dialog(self.add_categories_dialog) + self.page.update() + + def on_save_category_btn_click(self, e): + category = self.category_name.value + access = ",".join([cb.label for cb in self.roles_checkboxes if cb.value]) + base_url = self.page.session.store.get('api_base_url') + token = self.page.session.store.get('token') + + if self.editing_category_id: + url = f'{base_url}/documents/categories/update/{self.editing_category_id}' + method = requests.put + else: + url = f'{base_url}/documents/categories/add' + method = requests.post + + response = method( + url, + json={ + "name": category, + "access": access + }, + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + ) + + if response.status_code in [200, 201]: + self.update_category_list() + self.category_name.value = None + self.editing_category_id = None + self.page.pop_dialog() + else: + raise Exception("Operatiuna nu sa putut realiza, token-ul a expirat, va rugam sa va autentificati. Daca eroarea persita contactati echipa de IT!") + + + def update_category_list(self): + self.all_categories = self.get_categories() + self.category_list.controls = self.create_category_list( + self.all_categories, + self.on_edit_category_btn_click, + self.on_delete_category_btn_click + ) + self.category_list.update() + + # Actualizăm și opțiunile din dropdown-ul pentru documente + self.category.options = [ + ft.dropdown.Option(key=str(cat['id']), text=cat['name']) + for cat in self.all_categories + ] + + def on_cancel_category_btn_click(self, e): + self.category_name.value = None + self.page.pop_dialog() + + def on_search_bar_submit(self, e): + # Luăm textul din search bar și îl convertim la litere mici pentru o căutare case-insensitive + query = self.search_bar.value.strip().lower() + + # Filtrăm lista de documente stocată în self.all_documents + if not query: + filtered_docs = self.all_documents + else: + filtered_docs = [ + doc for doc in self.all_documents + if query in doc['name'].lower() + ] + + # Actualizăm interfața cu lista filtrată + self.documents_list.controls = self.create_documents_list( + filtered_docs, + lambda doc: self.page.run_task(self.on_download_document_btn_click, doc), + self.on_delete_document_btn_click + ) + self.documents_list.update() + + def add_new_document(self, e): + self.page.show_dialog(self.add_new_document_dialog) + + async def handle_file_upload(self, e: ft.Event[ft.Button]): + print('File uploaded') + try: + state.file_picker = ft.FilePicker() + files = await state.file_picker.pick_files(allow_multiple=False) + print("Picked file:", files) + + state.picked_files = files + uploaded_file_name = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{state.picked_files[0].name}" + await state.file_picker.upload( + files=[ + ft.FilePickerUploadFile( + name=file.name, + upload_url=self.page.get_upload_url(uploaded_file_name, 60), + + ) + + for file in state.picked_files + ] + ) + self.documenet_title = uploaded_file_name + self.documenet_displayed_title.value = uploaded_file_name.split("_")[1] + self.documenet_displayed_title.update() + return f'{uploaded_file_name}' + except Exception as e: + print(e) + + def on_save_document_btn_click(self, e): + if not self.category.value or not self.documenet_title: + # Opțional: Poți adăuga un mesaj de eroare vizibil pentru utilizator + return + + selected_cat_id = int(self.category.value) + # Identificăm obiectul categoriei pentru a-i moșteni accesul și numele + category = next((cat for cat in self.all_categories if cat['id'] == selected_cat_id), None) + + if not category: + return + + base_url = self.page.session.store.get('api_base_url') + token = self.page.session.store.get('token') + + # Construim path-ul relativ (Categorie/NumeFisier) + dest_path = f"{category['name']}/{self.documenet_title}" + + payload = { + "category_id": selected_cat_id, + "name": self.documenet_displayed_title.value, + "path": dest_path, + "access": category.get('access', '') # Moștenim accesul de la categorie + } + + response = requests.post( + f"{base_url}/documents/standards/add", + json=payload, + headers={'Authorization': f'Bearer {token}'} + ) + + if response.status_code == 201: + self.page.pop_dialog() + self.list_all_category_documents(selected_cat_id) + # Resetăm câmpurile după salvarea cu succes + self.documenet_title = None + self.documenet_displayed_title.value = "" + + def on_cancel_save_dialog_btn_click(self, e): + self.documenet_title = None + self.documenet_displayed_title.value = "" + self.page.pop_dialog() + + def create_category_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.REFRESH, + on_click=lambda e, id=item['id']: self.on_refresh_category_click(id), + icon_color=ft.Colors.BLUE_400, + ), + 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, + ), + expand=True, + bgcolor=ft.Colors.BLUE_50, + border = ft.Border.all(1, ft.Colors.GREY), + padding=10, + border_radius=8, + ink=True, + on_click=lambda e, cid=item['id']: self.list_all_category_documents(cid) + ) + for item in items + ] + + def on_edit_category_btn_click(self, category): + self.editing_category_id = category['id'] + self.category_name.value = category['name'] + + access_list = category.get('access', '').split(',') + for cb in self.roles_checkboxes: + cb.value = cb.label in access_list + + self.add_categories_dialog.title = ft.Text("Editeaza Categorie") + self.page.show_dialog(self.add_categories_dialog) + self.page.update() + + def on_refresh_category_click(self, category_id): + base_url = self.page.session.store.get('api_base_url') + token = self.page.session.store.get('token') + + try: + response = requests.post( + f'{base_url}/documents/categories/refresh/{category_id}', + headers={'Authorization': f'Bearer {token}'} + ) + + if response.status_code == 200: + added = response.json().get("added", 0) + # Daca suntem in categoria care s-a improspatat, reincarcam lista de documente + if self.current_category_id == category_id: + self.list_all_category_documents(category_id) + + # Notificare succes + self.page.snack_bar = ft.SnackBar( + content=ft.Text(f"Refresh complet. S-au adaugat {added} documente."), + bgcolor=ft.Colors.GREEN_400 + ) + self.page.snack_bar.open = True + self.page.update() + except Exception as e: + print(f"Error during category refresh: {e}") + + def on_delete_category_btn_click(self, category_id): + self.category_id_to_delete = category_id + self.page.show_dialog(self.delete_confirmation_dialog) + self.page.update() + + def confirm_delete_category(self, e): + if self.category_id_to_delete: + base_url = self.page.session.store.get('api_base_url') + token = self.page.session.store.get('token') + + response = requests.delete( + f'{base_url}/documents/categories/delete/{self.category_id_to_delete}', + headers={'Authorization': f'Bearer {token}'} + ) + + if response.status_code == 200: + # Dacă categoria ștearsă este cea ale cărei documente sunt afișate, golim lista + if self.current_category_id == self.category_id_to_delete: + self.documents_list.controls = [] + self.documents_list.update() + self.current_category_id = None + + self.update_category_list() + + self.category_id_to_delete = None + self.page.pop_dialog() + + def close_delete_dialog(self, e): + self.category_id_to_delete = None + self.page.pop_dialog() + + def get_categories(self): + base_url = self.page.session.store.get('api_base_url') + token = self.page.session.store.get('token') + try: + response = requests.get( + f'{base_url}/documents/categories', + headers={'Authorization': f'Bearer {token}'} + ) + if response.status_code == 200: + return response.json() + except Exception as e: + print(f"Error fetching categories: {e}") + return [] + + def list_all_category_documents(self, category_id): + self.current_category_id = category_id + self.all_documents = self.get_all_documents(category_id) or [] + self.documents_list.controls = self.create_documents_list( + self.all_documents, + lambda doc: self.page.run_task(self.on_download_document_btn_click, doc), + self.on_delete_document_btn_click + ) + self.documents_list.update() + + async def on_download_document_btn_click(self, document): + base_url = self.page.session.store.get('api_base_url') + token = self.page.session.store.get('token') + # Deschidem link-ul de download în browser + # path în document este de forma "NumeCategorie/fisier.ext" + download_url = f"{base_url}/documents/download?path={document['path']}&token={token}" + await self.page.launch_url(download_url) + + def on_delete_document_btn_click(self, doc_id): + self.document_id_to_delete = doc_id + self.page.show_dialog(self.delete_document_confirmation_dialog) + self.page.update() + + def confirm_delete_document(self, e): + if self.document_id_to_delete: + base_url = self.page.session.store.get('api_base_url') + token = self.page.session.store.get('token') + + response = requests.delete( + f'{base_url}/documents/standards/delete/{self.document_id_to_delete}', + headers={'Authorization': f'Bearer {token}'} + ) + + if response.status_code == 200: + if hasattr(self, 'current_category_id'): + self.list_all_category_documents(self.current_category_id) + + self.document_id_to_delete = None + self.page.pop_dialog() + + def close_delete_document_dialog(self, e): + self.document_id_to_delete = None + self.page.pop_dialog() + + def get_all_documents(self, category_id): + base_url = self.page.session.store.get('api_base_url') + token = self.page.session.store.get('token') + try: + response = requests.get( + f'{base_url}/documents/standards/category/{category_id}', + headers={'Authorization': f'Bearer {token}'} + ) + if response.status_code == 200: + return response.json() + except Exception as e: + print(f"Error fetching documents: {e}") + return [] + + def create_documents_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.DOWNLOAD, + 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, + ), + expand=True, + bgcolor=ft.Colors.BLUE_50, + border = ft.Border.all(1, ft.Colors.GREY), + padding=10, + border_radius=8 + ) + for item in items + ] + + def build(self): + return ft.Container( + content=ft.Row( + [ + ft.Column( + [ + ft.Row( + [ + ft.Button( + "Adauga Categori", + icon=ft.Icons.ADD, + on_click=self.on_add_btn_click, + width=300 + ), + ], + alignment=ft.MainAxisAlignment.CENTER + ), + self.category_list + ], + alignment=ft.MainAxisAlignment.START, + width = 350, + ), + ft.VerticalDivider(width=1), + ft.Column( + [ + ft.Row( + [ + self.add_document + ], + alignment=ft.MainAxisAlignment.END + ), + ft.Row( + [ + self.search_bar + ] + ), + self.documents_list + ], + expand=True + ) + ], + ), + expand=True + ) \ No newline at end of file diff --git a/client/pages/settings/payment_and_subscription.py b/client/pages/settings/payment_and_subscription.py new file mode 100644 index 0000000..0a3f69b --- /dev/null +++ b/client/pages/settings/payment_and_subscription.py @@ -0,0 +1,351 @@ +import flet as ft +import requests +from helpers.payment_type import PaymentType + +class PaymentAndSubscription: + def __init__(self, page: ft.Page): + self.page = page + self.base_url = self.page.session.store.get('api_base_url') + self.token = self.page.session.store.get('token') + self.selected_payment_id = None + self.selected_sub_id = None + + self.payment_type = [ + PaymentType().ONE_TIME_ONLY, + PaymentType().SUBSCRIPTION.lower() + ] + + # Pre-fetch data as in users.py + self.all_payments = self.get_payments_data() + self.all_subscriptions = self.get_subscriptions_data() + + self.name = ft.TextField(label = "Denumire") + self.amount = ft.TextField(label="Valoare", suffix="Lei") + self.type = ft.RadioGroup( + content=ft.Row( + [ft.Radio(value=payment, label=payment.upper()) for payment in self.payment_type] + ) + ) + self.error_text = ft.Text(color=ft.Colors.RED) + + self.payments_list = ft.ListView( + controls=self.create_payment_list(self.all_payments, self.on_edit_payment_click), + spacing=10, + expand=True, + padding=10 + ) + + # Elemente pentru Abonamente + self.sub_name = ft.TextField(label="Nume Abonament", width=350) + self.payment_dropdown = ft.Dropdown( + label="Selecteaza Plata", + width=350, + options=[ + ft.dropdown.Option(key=str(p['id']), text=f"{p['name']} ({p['amount']} Lei)") + for p in self.all_payments if p['type'].lower() == self.payment_type[1].lower() + ] + ) + self.months_dropdown = ft.Dropdown( + label="Numar luni", + width=350, + options=[ft.dropdown.Option(str(i)) for i in range(1, 13)] + ) + self.sub_error_text = ft.Text(color=ft.Colors.RED) + self.subs_list = ft.ListView( + controls=self.create_subscription_list(self.all_subscriptions, self.on_edit_sub_click), + spacing=10, + expand=True, + padding=10 + ) + + self.add_payment_dialog = ft.AlertDialog( + title=ft.Text("Detalii Plata"), + content=ft.Column( + [ + self.name, + self.amount, + ft.Text("Tip plata:", weight=ft.FontWeight.BOLD), + self.type, + self.error_text + ], + height=250, + tight=True + ), + actions=[ + ft.FilledButton("Salveaza", on_click=self.on_save_btn_click), + ft.FilledButton("Cancel", on_click=self.on_cancel_btn_click, bgcolor=ft.Colors.GREY) + ] + ) + + self.add_sub_dialog = ft.AlertDialog( + title=ft.Text("Detalii Abonament"), + content=ft.Column( + [ + self.sub_name, + self.payment_dropdown, + self.months_dropdown, + self.sub_error_text + ], + height=280, + tight=True + ), + actions=[ + ft.FilledButton("Salveaza", on_click=self.on_save_sub_click), + ft.FilledButton("Cancel", on_click=self.on_cancel_btn_click, bgcolor=ft.Colors.GREY) + ] + ) + + # --- LOGICA PLATI --- + + def add_new_payment_btn_click(self, e): + self.selected_payment_id = None + self.name.value = '' + self.amount.value = '' + self.type.value = None + self.error_text.value = '' + self.add_payment_dialog.title = ft.Text("Adauga plata noua") + self.page.show_dialog(self.add_payment_dialog) + self.page.update() + + def on_edit_payment_click(self, payment): + self.selected_payment_id = payment['id'] + self.name.value = payment['name'] + self.amount.value = str(payment['amount']) + self.type.value = payment['type'] + self.error_text.value = '' + self.add_payment_dialog.title = ft.Text("Editeaza plata") + self.page.show_dialog(self.add_payment_dialog) + self.page.update() + + def get_payments_data(self): + try: + response = requests.get( + f"{self.base_url}/payments/", + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + return response.json() + except Exception as e: + print(f"Error fetching payments: {e}") + return [] + + def get_payments(self): + self.all_payments = self.get_payments_data() + self.payments_list.controls = self.create_payment_list(self.all_payments, self.on_edit_payment_click) + if self.payments_list.page: + self.payments_list.update() + + # Actualizăm și opțiunile dropdown-ului pentru abonamente + self.payment_dropdown.options = [ + ft.dropdown.Option(key=str(p['id']), text=f"{p['name']} ({p['amount']} Lei)") + for p in self.all_payments if p['type'].lower() == self.payment_type[1].lower() + ] + if self.payment_dropdown.page: + self.payment_dropdown.update() + + def create_payment_list(self, items, on_click_handler): + return [ + ft.Container( + content=ft.Row( + [ + ft.Column( + [ + ft.Text(p['name'], weight=ft.FontWeight.BOLD), + ft.Text(f"{p['amount']} Lei - {p['type'].upper()}", size=12), + ], + alignment=ft.MainAxisAlignment.CENTER, + ), + ft.IconButton( + icon=ft.Icons.EDIT, + on_click=lambda e, payment=p: on_click_handler(payment) + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + bgcolor=ft.Colors.BLUE_50, + padding=10, + border_radius=8, + border=ft.Border.all(1, ft.Colors.GREY_300), + ink=True + ) + for p in items + ] + + def on_save_btn_click(self, e): + if not self.name.value or not self.amount.value or not self.type.value: + self.error_text.value = "Toate campurile sunt obligatorii!" + self.error_text.update() + return + + print(self.amount.value) + + try: + payload = { + "name": self.name.value, + "amount": float(self.amount.value), + "type": self.type.value + } + + if self.selected_payment_id: + # Update + url = f"{self.base_url}/payments/update/{self.selected_payment_id}" + response = requests.put(url, json=payload, headers={'Authorization': f'Bearer {self.token}'}) + else: + # Create + url = f"{self.base_url}/payments/add" + response = requests.post(url, json=payload, headers={'Authorization': f'Bearer {self.token}'}) + + if response.status_code in [200, 201]: + self.page.pop_dialog() + self.get_payments() + else: + self.error_text.value = "Eroare la salvare" + self.error_text.update() + + except Exception as e: + print(e) + self.error_text.value = "Valoarea trebuie sa fie un numar!" + self.error_text.update() + + # --- LOGICA ABONAMENTE --- + + def add_new_sub_btn_click(self, e): + self.selected_sub_id = None + self.sub_name.value = "" + # Resetăm dropdown-urile la starea default (fără selecție) + self.payment_dropdown.value = "" + self.months_dropdown.value = "" + self.sub_error_text.value = "" + self.add_sub_dialog.title = ft.Text("Adauga Abonament Nou") + + # Forțăm afișarea dialogului cu valorile proaspăt resetate + self.page.show_dialog(self.add_sub_dialog) + self.page.update() + + def on_edit_sub_click(self, sub): + self.selected_sub_id = sub['id'] + self.sub_name.value = sub['name'] + self.payment_dropdown.value = str(sub['pay_and_subs_id']) + self.months_dropdown.value = str(sub['mounts']) + self.sub_error_text.value = "" + self.add_sub_dialog.title = ft.Text("Editeaza Abonament") + self.page.show_dialog(self.add_sub_dialog) + self.page.update() + + def get_subscriptions_data(self): + try: + response = requests.get( + f"{self.base_url}/subscriptions/", + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + return response.json() + except Exception as e: + print(f"Error fetching subscriptions: {e}") + return [] + + def get_subscriptions(self): + self.all_subscriptions = self.get_subscriptions_data() + self.subs_list.controls = self.create_subscription_list(self.all_subscriptions, self.on_edit_sub_click) + if self.subs_list.page: + self.subs_list.update() + + def create_subscription_list(self, items, on_click_handler): + controls = [] + for s in items: + # Gasim plata asociata pentru a calcula totalul + payment = next((p for p in self.all_payments if p['id'] == s['pay_and_subs_id']), None) + amount = payment['amount'] if payment else 0 + total = amount * (s['mounts'] or 0) + controls.append( + ft.Container( + content=ft.Row( + [ + ft.Column( + [ + ft.Text(s['name'], weight=ft.FontWeight.BOLD), + ft.Text(f"{s['mounts']} luni x {amount} Lei = {total} Lei Total", size=12, color=ft.Colors.BLUE_700), + ], + alignment=ft.MainAxisAlignment.CENTER, + ), + ft.IconButton( + icon=ft.Icons.EDIT, + on_click=lambda e, sub=s: on_click_handler(sub) + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + bgcolor=ft.Colors.GREEN_50, + padding=10, + border_radius=8, + border=ft.Border.all(1, ft.Colors.GREEN_200), + ink=True + ) + ) + return controls + + def on_save_sub_click(self, e): + if not self.sub_name.value or not self.payment_dropdown.value or not self.months_dropdown.value: + self.sub_error_text.value = "Toate campurile sunt obligatorii!" + self.sub_error_text.update() + return + + payload = { + "name": self.sub_name.value, + "pay_and_subs_id": int(self.payment_dropdown.value), + "mounts": int(self.months_dropdown.value) + } + + if self.selected_sub_id: + url = f"{self.base_url}/subscriptions/update/{self.selected_sub_id}" + response = requests.put(url, json=payload, headers={'Authorization': f'Bearer {self.token}'}) + else: + url = f"{self.base_url}/subscriptions/add" + response = requests.post(url, json=payload, headers={'Authorization': f'Bearer {self.token}'}) + + if response.status_code in [200, 201]: + self.page.pop_dialog() + self.get_subscriptions() + else: + self.sub_error_text.value = "Eroare la salvare abonament" + self.sub_error_text.update() + + def on_cancel_btn_click(self, e): + self.page.pop_dialog() + + def build(self): + + return ft.Container( + content=ft.Row( + [ + # Coloana Stanga: Plati + ft.Column( + [ + ft.Row([ + ft.Text("Configurare Plati", size=18, weight=ft.FontWeight.BOLD), + ft.IconButton(ft.Icons.ADD_CIRCLE, on_click=self.add_new_payment_btn_click, icon_color=ft.Colors.BLUE) + ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), + ft.Divider(), + self.payments_list + ], + expand=1 + ), + ft.VerticalDivider(width=1), + # Coloana Dreapta: Abonamente + ft.Column( + [ + ft.Row([ + ft.Text("Gestiune Abonamente", size=18, weight=ft.FontWeight.BOLD), + ft.IconButton(ft.Icons.ADD_TASK, on_click=self.add_new_sub_btn_click, icon_color=ft.Colors.GREEN) + ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), + ft.Divider(), + self.subs_list + ], + expand=1 + ) + ], + expand=True + ), + expand=True, + padding=20 + ) \ No newline at end of file diff --git a/client/pages/settings/settings.py b/client/pages/settings/settings.py new file mode 100644 index 0000000..f21300b --- /dev/null +++ b/client/pages/settings/settings.py @@ -0,0 +1,77 @@ +import flet as ft +from pages.settings.documente_juridice import DocumenteJuridice +from pages.settings.users import UsersSettings +from pages.settings.payment_and_subscription import PaymentAndSubscription + +class Settings: + def __init__(self, page: ft.Page, home): + self.page = page + self.home = home + + self.doc_juridice = DocumenteJuridice(self.page) + self.users_settings = UsersSettings(self.page) + self.payment_and_subscription = PaymentAndSubscription(self.page) + + def build(self): + return ft.Tabs( + selected_index=0, + length=8, + expand=True, + content=ft.Column( + expand=True, + controls=[ + ft.TabBar( + tabs=[ + ft.Tab(label="Documente Juridice Standard", icon=ft.Icons.BALANCE), + ft.Tab(label="Articole si Publicatii", icon=ft.Icons.ARTICLE), + ft.Tab(label="Comunicare", icon=ft.Icons.CHAT), + ft.Tab(label="Consultanta", icon=ft.Icons.HANDSHAKE), + ft.Tab(label="Convocator", icon=ft.Icons.BUSINESS), + ft.Tab(label="Licitatii si Lucrari", icon=ft.Icons.ASSIGNMENT_TURNED_IN), + ft.Tab(label="Abonamente si Plati", icon=ft.Icons.REPEAT_ON), + ft.Tab(label="Utilizatori", icon=ft.Icons.ADMIN_PANEL_SETTINGS), + ] + ), + ft.TabBarView( + # expand=True, + height=300, + controls=[ + ft.Container( + content=self.doc_juridice.build(), + alignment=ft.Alignment.CENTER, + expand=True + ), + ft.Container( + content=ft.Text("This is Tab 2"), + alignment=ft.Alignment.CENTER, + ), + ft.Container( + content=ft.Text("This is Tab 3"), + alignment=ft.Alignment.CENTER, + ), + ft.Container( + content=ft.Text("This is Tab 4"), + alignment=ft.Alignment.CENTER, + ), + ft.Container( + content=ft.Text("This is Tab 5"), + alignment=ft.Alignment.CENTER, + ), + ft.Container( + content=ft.Text("This is Tab 6"), + alignment=ft.Alignment.CENTER, + ), + ft.Container( + content=self.payment_and_subscription.build(), + alignment=ft.Alignment.CENTER, + ), + ft.Container( + content=self.users_settings.build(), + alignment=ft.Alignment.CENTER, + ), + ], + expand=True + ), + ], + ), + ) \ No newline at end of file diff --git a/client/pages/settings/users.py b/client/pages/settings/users.py new file mode 100644 index 0000000..110cb70 --- /dev/null +++ b/client/pages/settings/users.py @@ -0,0 +1,256 @@ +import flet as ft +import requests +from helpers.roles import Roles + +import flet as ft + +class UsersSettings: + def __init__(self, page: ft.Page): + self.page = page + self.base_url = self.page.session.store.get('api_base_url') + self.token = self.page.session.store.get('token') + self.selected_user_id = None + + self.user_roles = [ + 'toti', + Roles.USER, + Roles.PROPRIETAR, + Roles.CENZOR, + Roles.ADMINISTRATOR, + Roles.PRESEDINTE, + Roles.EXPERT, + Roles.BA, + ] + + self.all_roles_list = ft.ListView( + controls=self.create_list(self.user_roles, self.on_role_btn_click), + spacing=10, + width=350 + ) + self.all_users = self.get_all_users() + self.role_users = ft.ListView( + controls=self.create_users_list(self.all_users, self.on_user_btn_click), + spacing=10, + expand=True + ) + + self.placeholder = ft.Container( + margin=ft.Margin.only(top=5), + content=self.role_users, + expand=True + ) + + self.email = ft.TextField(label="Email", read_only=True, expand=True) + self.first_name = ft.TextField(label="Prenume", expand=True) + self.last_name = ft.TextField(label = "Nume", expand=True) + self.address = ft.TextField(label="Adresa", expand=True) + self.profession = ft.TextField(label="Profesie", expand=True) + self.role = ft.RadioGroup( + content=ft.Row( + [ft.Radio(value=role, label=role.upper()) for role in self.user_roles if role != "toti"] + ) + ) + self.status = ft.TextField(label="Status", expand=True) + self.profile_pic = ft.TextField(label="Poza profil", read_only=True, expand=True) + self.created_at = ft.TextField(label="Data creare cont",read_only=True, expand=True) + self.active = ft.TextField(label="Active: 0-Nu, 1-Da", expand=True) + self.error = ft.Text(color=ft.Colors.RED) + + self.user_details = ft.Column( + [ + ft.Text("Detalii utilizator", weight=ft.FontWeight.BOLD), + self.email, + self.first_name, + self.last_name, + self.address, + self.profession, + ft.Text("Rol:", weight=ft.FontWeight.BOLD), + self.role, + #self.status, + #self.profile_pic, + self.active, + self.created_at, + self.error, + ft.Row( + [ + ft.FilledButton("Salveaza", on_click=self.on_save_btn_click) + ], + alignment=ft.MainAxisAlignment.CENTER + ), + ft.Text() + ], + scroll=ft.ScrollMode.ADAPTIVE + ) + + self.search_bar = ft.TextField( + label="Cauta", + on_submit=self.on_search_bar_submit, + expand=True + ) + + def on_role_btn_click(self, role): + self.placeholder.content = self.role_users + self.placeholder.update() + self.role_users.controls.clear() + users = self.users_by_role(role) if role != 'toti' else self.all_users + self.role_users.controls = self.create_users_list(users, self.on_user_btn_click) + self.role_users.update() + + def on_user_btn_click(self, user): + self.selected_user_id = user['id'] + self.email.value = user['email'] + self.first_name.value = user['first_name'] + self.last_name.value = user['last_name'] + self.address.value = user['address'] + self.profession.value = user['profession'] + self.role.value = user['role'] + self.status.value = user['status'] + self.profile_pic.value = user['profile_pic'] + self.active.value = str(user['active']) + self.created_at.value = user['created_at'] + self.error.value = "" + self.placeholder.content = self.user_details + self.placeholder.update() + + 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.upper()) + ] + ), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + expand=True, + bgcolor=ft.Colors.BLUE_50, + border = ft.Border.all(1, ft.Colors.GREY), + padding=10, + border_radius=8, + ink=True, + on_click=lambda e, cid=item: on_click_handler(cid) + ) + for item in items + ] + + def create_users_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['email']) + ] + ), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + expand=True, + bgcolor=ft.Colors.BLUE_50, + border = ft.Border.all(1, ft.Colors.GREY), + padding=10, + border_radius=8, + ink=True, + on_click=lambda e, cid=item: on_click_handler(cid) + ) + for item in items + ] + + def get_all_users(self): + payload = {} + response = requests.get( + f"{self.base_url}/users/", + json=payload, + headers={'Authorization': f'Bearer {self.token}'} + ) + print(response.text) + return response.json() + + def users_by_role(self, role): + users = [] + for user in self.all_users: + if user['role']==role: + users.append(user) + print(users) + return users + + def on_save_btn_click(self, e): + if self.selected_user_id is None: + return + + payload = { + "first_name": self.first_name.value, + "last_name": self.last_name.value, + "address": self.address.value, + "profession": self.profession.value, + "role": self.role.value, + "status": self.status.value, + "active": int(self.active.value) if str(self.active.value).isdigit() else 1 + } + + response = requests.put( + f"{self.base_url}/users/update/{self.selected_user_id}", + json=payload, + headers={'Authorization': f'Bearer {self.token}'} + ) + + if response.status_code == 200: + self.error.value = "Modificări salvate cu succes!" + self.error.color = ft.Colors.GREEN + self.all_users = self.get_all_users() # Refresh lista locală + else: + self.error.value = response.json().get("error", "Eroare la salvarea datelor") + self.error.color = ft.Colors.RED + + self.error.update() + + def on_search_bar_submit(self, e): + search = self.search_bar.value + self.placeholder.content = self.role_users + self.placeholder.update() + self.role_users.controls.clear() + users = [user for user in self.all_users if search in user['email']] + self.role_users.controls = self.create_users_list(users, self.on_user_btn_click) + self.role_users.update() + + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + self.all_roles_list, + ft.VerticalDivider(width=1), + ft.Container( + content = ft.Column( + [ + ft.Row( + [ + self.search_bar + ] + ), + #ft.Divider(height=1), + self.placeholder + ], + expand=True + ), + padding=5, + expand=True + ) + ], + expand=True + ) + ], + expand=True + ), + expand=True + ) \ No newline at end of file diff --git a/client/pages/subscriptions/subscriptions.py b/client/pages/subscriptions/subscriptions.py new file mode 100644 index 0000000..e1bf44d --- /dev/null +++ b/client/pages/subscriptions/subscriptions.py @@ -0,0 +1,14 @@ +import flet as ft + +class SubscriptionsPage: + def __init__(self, page: ft.Page): + self.page = page + + def build(self): + return ft.Container( + content=ft.Column( + [ + + ] + ) + ) \ No newline at end of file diff --git a/commands.txt b/commands.txt new file mode 100644 index 0000000..e69de29 diff --git a/documents/.DS_Store b/documents/.DS_Store new file mode 100644 index 0000000..c1fe461 Binary files /dev/null and b/documents/.DS_Store differ diff --git a/documents/Tipizate ANAF/1 Decizie Comitet Executiv stabilire adunare generala pasul 1.doc b/documents/Tipizate ANAF/1 Decizie Comitet Executiv stabilire adunare generala pasul 1.doc new file mode 100644 index 0000000..6680d2d Binary files /dev/null and b/documents/Tipizate ANAF/1 Decizie Comitet Executiv stabilire adunare generala pasul 1.doc differ diff --git a/documents/Tipizate ANAF/10 Proces verbal afisare hotarare adunare generala pasul 10.doc b/documents/Tipizate ANAF/10 Proces verbal afisare hotarare adunare generala pasul 10.doc new file mode 100644 index 0000000..1cbc85c Binary files /dev/null and b/documents/Tipizate ANAF/10 Proces verbal afisare hotarare adunare generala pasul 10.doc differ diff --git a/documents/Tipizate ANAF/2 Proces verbal afisare decizie comitet pasul 2.doc b/documents/Tipizate ANAF/2 Proces verbal afisare decizie comitet pasul 2.doc new file mode 100644 index 0000000..0aadb0b Binary files /dev/null and b/documents/Tipizate ANAF/2 Proces verbal afisare decizie comitet pasul 2.doc differ diff --git a/documents/Tipizate ANAF/3 Anunt convocator avizier pasul 3.doc b/documents/Tipizate ANAF/3 Anunt convocator avizier pasul 3.doc new file mode 100644 index 0000000..8ccf816 Binary files /dev/null and b/documents/Tipizate ANAF/3 Anunt convocator avizier pasul 3.doc differ diff --git a/documents/Tipizate ANAF/4 Tabel convocator pasul 4.doc b/documents/Tipizate ANAF/4 Tabel convocator pasul 4.doc new file mode 100644 index 0000000..94ec23e Binary files /dev/null and b/documents/Tipizate ANAF/4 Tabel convocator pasul 4.doc differ diff --git a/documents/Tipizate ANAF/5 Adresa proprietar care nu a semnat tabelul convocator Actul din plicuri pasul 5.doc b/documents/Tipizate ANAF/5 Adresa proprietar care nu a semnat tabelul convocator Actul din plicuri pasul 5.doc new file mode 100644 index 0000000..f8a3968 Binary files /dev/null and b/documents/Tipizate ANAF/5 Adresa proprietar care nu a semnat tabelul convocator Actul din plicuri pasul 5.doc differ diff --git a/documents/Tipizate ANAF/6 Tabel prezenta convocare pasul 6.doc b/documents/Tipizate ANAF/6 Tabel prezenta convocare pasul 6.doc new file mode 100644 index 0000000..017073c Binary files /dev/null and b/documents/Tipizate ANAF/6 Tabel prezenta convocare pasul 6.doc differ diff --git a/documents/Tipizate ANAF/7 Hotarare adunare generala convocata lipsa cvorum pasul 7.doc b/documents/Tipizate ANAF/7 Hotarare adunare generala convocata lipsa cvorum pasul 7.doc new file mode 100644 index 0000000..5a1c670 Binary files /dev/null and b/documents/Tipizate ANAF/7 Hotarare adunare generala convocata lipsa cvorum pasul 7.doc differ diff --git a/documents/Tipizate ANAF/8 Tabel prezenta reconvocare pasul 8.doc b/documents/Tipizate ANAF/8 Tabel prezenta reconvocare pasul 8.doc new file mode 100644 index 0000000..6d812ed Binary files /dev/null and b/documents/Tipizate ANAF/8 Tabel prezenta reconvocare pasul 8.doc differ diff --git a/documents/Tipizate ANAF/9 Hotarare adunare generala reconvocata pasul 9.doc b/documents/Tipizate ANAF/9 Hotarare adunare generala reconvocata pasul 9.doc new file mode 100644 index 0000000..86e6339 Binary files /dev/null and b/documents/Tipizate ANAF/9 Hotarare adunare generala reconvocata pasul 9.doc differ diff --git a/documents/Tipizate ANAF/card de fidelitate 2.png b/documents/Tipizate ANAF/card de fidelitate 2.png new file mode 100644 index 0000000..441ab6d Binary files /dev/null and b/documents/Tipizate ANAF/card de fidelitate 2.png differ diff --git a/documents/Tipizate ANAF/card de fidelitate 3.pdf b/documents/Tipizate ANAF/card de fidelitate 3.pdf new file mode 100644 index 0000000..b3920c5 Binary files /dev/null and b/documents/Tipizate ANAF/card de fidelitate 3.pdf differ diff --git a/documents/Tipizate ANAF/card de fidelizare.png b/documents/Tipizate ANAF/card de fidelizare.png new file mode 100644 index 0000000..1743d7c Binary files /dev/null and b/documents/Tipizate ANAF/card de fidelizare.png differ diff --git a/documents/test/20260519172324_Untitled document - Google Docs.pdf b/documents/test/20260519172324_Untitled document - Google Docs.pdf new file mode 100644 index 0000000..cbd91b4 Binary files /dev/null and b/documents/test/20260519172324_Untitled document - Google Docs.pdf differ diff --git a/documents/test/20260519172336_bacanie2.png b/documents/test/20260519172336_bacanie2.png new file mode 100644 index 0000000..bf24be7 Binary files /dev/null and b/documents/test/20260519172336_bacanie2.png differ diff --git a/documents/test/20260519172432_bacanie3.png b/documents/test/20260519172432_bacanie3.png new file mode 100644 index 0000000..0e48a99 Binary files /dev/null and b/documents/test/20260519172432_bacanie3.png differ diff --git a/documents/test2/20260519172449_bacanie.png b/documents/test2/20260519172449_bacanie.png new file mode 100644 index 0000000..622ab59 Binary files /dev/null and b/documents/test2/20260519172449_bacanie.png differ diff --git a/documents/test2/20260526212828_Screenshot 2026-02-27 at 11.44.57.png b/documents/test2/20260526212828_Screenshot 2026-02-27 at 11.44.57.png new file mode 100644 index 0000000..44b7891 Binary files /dev/null and b/documents/test2/20260526212828_Screenshot 2026-02-27 at 11.44.57.png differ diff --git a/documents/test3/20260527191520_Screenshot 2026-03-09 at 11.53.54.png b/documents/test3/20260527191520_Screenshot 2026-03-09 at 11.53.54.png new file mode 100644 index 0000000..66ce82e Binary files /dev/null and b/documents/test3/20260527191520_Screenshot 2026-03-09 at 11.53.54.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/server/.DS_Store b/server/.DS_Store new file mode 100644 index 0000000..06827f6 Binary files /dev/null and b/server/.DS_Store differ diff --git a/server/app.py b/server/app.py new file mode 100644 index 0000000..a9bcb74 --- /dev/null +++ b/server/app.py @@ -0,0 +1,58 @@ +import os + +from flask import Flask +from flask_jwt_extended import JWTManager +from flask_cors import CORS +from routes.auth import auth_bp +from routes.documents import documents_bp +from routes.users import users_bp +from routes.payments import payments_bp +from routes.subscriptions import subscriptions_bp + +def create_app(test_config=None): + # create and configure the app + app = Flask(__name__, instance_relative_config=True) + + app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY", "your-jwt-secret") + app.config["JWT_TOKEN_LOCATION"] = ["headers", "query_string"] + app.config["JWT_QUERY_STRING_NAME"] = "token" + jwt = JWTManager(app) + + CORS( + app, + resources={r"/*": {"origins": [os.getenv("WEB_ORIGIN", "*")]}}, + allow_headers=["Authorization", "Content-Type"], + expose_headers=["Content-Type"], + ) + + app.config.from_mapping( + SECRET_KEY='dev', + DATABASE=os.path.join(app.instance_path, 'instance/app_database.sqlite'), + ) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile('config.py', silent=True) + else: + # load the test config if passed in + app.config.from_mapping(test_config) + + # ensure the instance folder exists + os.makedirs(app.instance_path, exist_ok=True) + + # a simple page that says hello + @app.route('/hello') + def hello(): + return 'Hello, World!' + + app.register_blueprint(auth_bp, url_prefix="/auth") + app.register_blueprint(documents_bp, url_prefix="/documents") + app.register_blueprint(users_bp, url_prefix="/users") + app.register_blueprint(payments_bp, url_prefix="/payments") + app.register_blueprint(subscriptions_bp, url_prefix="/subscriptions") + + return app + +if __name__=="__main__": + app = create_app() + app.run() \ No newline at end of file diff --git a/server/assets/Manual.pdf b/server/assets/Manual.pdf new file mode 100644 index 0000000..2a29a84 Binary files /dev/null and b/server/assets/Manual.pdf differ diff --git a/server/instance/app_database.db b/server/instance/app_database.db new file mode 100644 index 0000000..d18800d Binary files /dev/null and b/server/instance/app_database.db differ diff --git a/server/models/.DS_Store b/server/models/.DS_Store new file mode 100644 index 0000000..0d71e04 Binary files /dev/null and b/server/models/.DS_Store differ diff --git a/server/models/__pycache__/audit.cpython-313.pyc b/server/models/__pycache__/audit.cpython-313.pyc new file mode 100644 index 0000000..d577146 Binary files /dev/null and b/server/models/__pycache__/audit.cpython-313.pyc differ diff --git a/server/models/__pycache__/users.cpython-313.pyc b/server/models/__pycache__/users.cpython-313.pyc new file mode 100644 index 0000000..16909e8 Binary files /dev/null and b/server/models/__pycache__/users.cpython-313.pyc differ diff --git a/server/models/audit.py b/server/models/audit.py new file mode 100644 index 0000000..6649a68 --- /dev/null +++ b/server/models/audit.py @@ -0,0 +1,108 @@ +import sqlite3 +from dataclasses import dataclass +import hashlib +from typing import Optional + +@dataclass +class AuditModel: + id: Optional[int] = None + user_id: Optional[int] = None + action: Optional[str] = None + endpoint: Optional[str] = None + created_at: Optional[str] = None + status: Optional[str] = None + + +class Audit: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_audit_table() + + def _create_audit_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS audit ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + action TEXT, + endpoint TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT + ); + """ + ) + conn.commit() + + def new_entry(self, entry:AuditModel): + """Create a new entry.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO audit (user_id, action, endpoint, status) + VALUES (?, ?, ?, ?) + """, + (entry.user_id, entry.action, entry.endpoint, entry.status), + ) + conn.commit() + return cursor.lastrowid + except sqlite3.IntegrityError: + return None + + def get_all_entries(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM audit") + rows = cursor.fetchall() + + return [ + AuditModel( + id = row[0], + user_id = row[1], + action = row[2], + endpoint = row[3], + created_at = row[4], + status = row[5] + ) + for row in rows + ] + + def get_entries_by_user_id(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM audit WHERE user_id = ?", (user_id, )) + rows = cursor.fetchall() + + return [ + AuditModel( + id = row[0], + user_id = row[1], + action = row[2], + endpoint = row[3], + created_at = row[4], + status = row[5] + ) + for row in rows + ] + + def delete_entries_older_than(self, date_string): + """ + Deletes logs older than the provided date. + Expected date_string format: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' + """ + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + # SQLite allows direct string comparison for ISO 8601 dates + cursor.execute( + "DELETE FROM audit WHERE created_at < ?", + (date_string,) + ) + conn.commit() + return cursor.rowcount # Returns the number of deleted rows + except sqlite3.Error as e: + print(f"An error occurred: {e}") + return 0 diff --git a/server/models/documents/__pycache__/documents_category.cpython-313.pyc b/server/models/documents/__pycache__/documents_category.cpython-313.pyc new file mode 100644 index 0000000..7d00a68 Binary files /dev/null and b/server/models/documents/__pycache__/documents_category.cpython-313.pyc differ diff --git a/server/models/documents/__pycache__/documents_custom.cpython-313.pyc b/server/models/documents/__pycache__/documents_custom.cpython-313.pyc new file mode 100644 index 0000000..a52992d Binary files /dev/null and b/server/models/documents/__pycache__/documents_custom.cpython-313.pyc differ diff --git a/server/models/documents/__pycache__/documents_standard.cpython-313.pyc b/server/models/documents/__pycache__/documents_standard.cpython-313.pyc new file mode 100644 index 0000000..d5c9118 Binary files /dev/null and b/server/models/documents/__pycache__/documents_standard.cpython-313.pyc differ diff --git a/server/models/documents/documents_category.py b/server/models/documents/documents_category.py new file mode 100644 index 0000000..e69a94b --- /dev/null +++ b/server/models/documents/documents_category.py @@ -0,0 +1,132 @@ +import sqlite3 +from dataclasses import dataclass +import hashlib +from typing import Optional + +@dataclass +class DocumentsCategoryModel: + id: Optional[int] = None + user_id: Optional[int] = None + name: Optional[str] = None + created_at: Optional[str] = None + access: Optional[str] = None + +class DocumentsCategory: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_audit_table() + + def _create_audit_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS documents_category ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + access TEXT + ); + """ + ) + conn.commit() + + def new_entry(self, entry:DocumentsCategoryModel): + """Create a new entry.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO documents_category (user_id, name, access) + VALUES (?, ?, ?) + """, + (entry.user_id, entry.name, entry.access) + ) + conn.commit() + return cursor.lastrowid + except sqlite3.IntegrityError: + return None + + def get_all_entries(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_category") + rows = cursor.fetchall() + + return [ + DocumentsCategoryModel( + id = row[0], + user_id = row[1], + name = row[2], + created_at = row[3], + access = row[4] + ) + for row in rows + ] + + def get_entries_by_user_id(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_category WHERE user_id = ?", (user_id, )) + rows = cursor.fetchall() + + return [ + DocumentsCategoryModel( + id = row[0], + user_id = row[1], + name = row[2], + created_at = row[3], + access = row[4] + ) + for row in rows + ] + + def get_entry_by_id(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_category WHERE id = ?", (id, )) + row = cursor.fetchone() + if row: + return DocumentsCategoryModel( + id = row[0], + user_id = row[1], + name = row[2], + created_at = row[3], + access = row[4] + ) + return None + + def update_entry(self, id, name, access): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + UPDATE documents_category + SET name = ?, access = ? + WHERE id = ? + """, + (name, access, id) + ) + conn.commit() + return cursor.rowcount + except sqlite3.Error as e: + print(f"An error occurred: {e}") + return 0 + + def delete_entry(self, id): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + # SQLite allows direct string comparison for ISO 8601 dates + cursor.execute( + "DELETE FROM documents_category WHERE id = ? ", + (id,) + ) + conn.commit() + return cursor.rowcount # Returns the number of deleted rows + except sqlite3.Error as e: + print(f"An error occurred: {e}") + return 0 \ No newline at end of file diff --git a/server/models/documents/documents_custom.py b/server/models/documents/documents_custom.py new file mode 100644 index 0000000..8ca6703 --- /dev/null +++ b/server/models/documents/documents_custom.py @@ -0,0 +1,303 @@ +import sqlite3 +from dataclasses import dataclass +import hashlib +from typing import Optional + +@dataclass +class DocumentsCustomModel: + id: Optional[int] = None + user_id: Optional[int] = None + name: Optional[str] = None + path: Optional[str] = None + created_at: Optional[str] = None + access: Optional[str] = None + + +class DocumentsCustom: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_audit_table() + + def _create_audit_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS documents_custom ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name TEXT, + path TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + access TEXT + ); + """ + ) + conn.commit() + + def new_entry(self, entry:DocumentsCustomModel): + """Create a new entry.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO documents_custom (user_id, name, path, access) + VALUES (?, ?, ?, ?) + """, + (entry.user_id, entry.name, entry.path, entry.access), + ) + conn.commit() + return cursor.lastrowid + except sqlite3.IntegrityError: + return None + + def get_all_entries(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_custom") + rows = cursor.fetchall() + + return [ + DocumentsCustomModel( + id = row[0], + user_id = row[1], + name = row[2], + path = row[3], + created_at = row[4], + access = row[5] + ) + for row in rows + ] + + def get_entries_by_user_id(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_custom WHERE user_id = ?", (user_id, )) + rows = cursor.fetchall() + + return [ + DocumentsCustomModel( + id = row[0], + user_id = row[1], + name = row[2], + path = row[3], + created_at = row[4], + access = row[5] + ) + for row in rows + ] + + def get_entry_by_id(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_custom WHERE id = ?", (id, )) + row = cursor.fetchone() + if row: + return DocumentsCustomModel( + id = row[0], + user_id = row[1], + name = row[2], + path = row[3], + created_at = row[4], + access = row[5] + ) + return None + + def delete_entry(self, id): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + # SQLite allows direct string comparison for ISO 8601 dates + cursor.execute( + "DELETE FROM documents_custom WHERE id = ? ", + (id,) + ) + conn.commit() + return cursor.rowcount # Returns the number of deleted rows + except sqlite3.Error as e: + print(f"An error occurred: {e}") + return 0 + +@dataclass +class CustomDocumentRequestModel: + id: Optional[int] = None + client_id: Optional[int] = None + request_text: Optional[str] = None + status: Optional[str] = "new" + price: Optional[float] = 0.0 + expert_id: Optional[int] = None + document_id: Optional[int] = None + created_at: Optional[str] = None + +class CustomDocumentRequests: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_table() + + def _create_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS custom_document_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL, + request_text TEXT, + status TEXT DEFAULT 'new', + price REAL DEFAULT 0.0, + expert_id INTEGER, + document_id INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """ + ) + conn.commit() + + def new_entry(self, entry: CustomDocumentRequestModel): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO custom_document_requests (client_id, request_text, status, price, expert_id, document_id) + VALUES (?, ?, ?, ?, ?, ?) + """, + (entry.client_id, entry.request_text, entry.status, entry.price, entry.expert_id, entry.document_id), + ) + conn.commit() + return cursor.lastrowid + except sqlite3.Error as e: + print(f"Database error: {e}") + return None + + def get_all_entries(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM custom_document_requests") + rows = cursor.fetchall() + return [ + CustomDocumentRequestModel( + id=row[0], + client_id=row[1], + request_text=row[2], + status=row[3], + price=row[4], + expert_id=row[5], + document_id=row[6], + created_at=row[7] + ) for row in rows] + + def get_entry_by_id(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM custom_document_requests WHERE id = ?", (id,)) + row = cursor.fetchone() + if row: + return CustomDocumentRequestModel( + id=row[0], + client_id=row[1], + request_text=row[2], + status=row[3], + price=row[4], + expert_id=row[5], + document_id=row[6], + created_at=row[7] + ) + return None + + def get_entries_by_client_id(self, client_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM custom_document_requests WHERE client_id = ?", (client_id,)) + rows = cursor.fetchall() + return [ + CustomDocumentRequestModel( + id=row[0], + client_id=row[1], + request_text=row[2], + status=row[3], + price=row[4], + expert_id=row[5], + document_id=row[6], + created_at=row[7] + ) for row in rows] + + def get_entries_by_expert_id(self, expert_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM custom_document_requests WHERE expert_id = ? ORDER BY id DESC", (expert_id,)) + rows = cursor.fetchall() + return [ + CustomDocumentRequestModel( + id=row[0], + client_id=row[1], + request_text=row[2], + status=row[3], + price=row[4], + expert_id=row[5], + document_id=row[6], + created_at=row[7] + ) for row in rows] + + def update_entry(self, id, status=None, price=None, expert_id=None, document_id=None, request_text=None): + if status is None and price is None and expert_id is None and document_id is None and request_text is None: + return False + + # Fetch existing entry to preserve unchanged values if not provided + existing_entry = self.get_entry_by_id(id) + if not existing_entry: + return False + + # Use existing values if new ones are not provided + status = status if status is not None else existing_entry.status + price = price if price is not None else existing_entry.price + expert_id = expert_id if expert_id is not None else existing_entry.expert_id + document_id = document_id if document_id is not None else existing_entry.document_id + request_text = request_text if request_text is not None else existing_entry.request_text + + fields = [] + params = [] + + if request_text is not None: + fields.append("request_text = ?") + params.append(request_text) + if status is not None: + fields.append("status = ?") + params.append(status) + if price is not None: + fields.append("price = ?") + params.append(price) + if expert_id is not None: + fields.append("expert_id = ?") + params.append(expert_id) + if document_id is not None: + fields.append("document_id = ?") + params.append(document_id) + + params.append(id) + query = f"UPDATE custom_document_requests SET {', '.join(fields)} WHERE id = ?" + + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(query, tuple(params)) + conn.commit() + return cursor.rowcount > 0 + except sqlite3.Error as e: + print(f"An error occurred during update: {e}") + return False + + def delete_entry(self, id): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "DELETE FROM custom_document_requests WHERE id = ?", + (id,) + ) + conn.commit() + return cursor.rowcount + except sqlite3.Error as e: + print(f"An error occurred during delete: {e}") + return 0 \ No newline at end of file diff --git a/server/models/documents/documents_standard.py b/server/models/documents/documents_standard.py new file mode 100644 index 0000000..0b502ea --- /dev/null +++ b/server/models/documents/documents_standard.py @@ -0,0 +1,143 @@ +import sqlite3 +from dataclasses import dataclass +import hashlib +from typing import Optional + +@dataclass +class DocumentsStandardModel: + id: Optional[int] = None + category_id: Optional[int] = None + user_id: Optional[int] = None + name: Optional[str] = None + path: Optional[str] = None + created_at: Optional[str] = None + access: Optional[str] = None + + +class DocumentsStandard: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_audit_table() + + def _create_audit_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS documents_standard ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER, + user_id INTEGER NOT NULL, + name TEXT, + path TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + access TEXT + ); + """ + ) + conn.commit() + + def new_entry(self, entry:DocumentsStandardModel): + """Create a new entry.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO documents_standard (category_id, user_id, name, path, access) + VALUES (?, ?, ?, ?, ?) + """, + (entry.category_id, entry.user_id, entry.name, entry.path, entry.access), + ) + conn.commit() + return cursor.lastrowid + except sqlite3.IntegrityError: + return None + + def get_all_entries(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_standard") + rows = cursor.fetchall() + + return [ + DocumentsStandardModel( + id = row[0], + category_id = row[1], + user_id = row[2], + name = row[3], + path = row[4], + created_at = row[5], + access = row[6] + ) + for row in rows + ] + + def get_entries_by_user_id(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_standard WHERE user_id = ?", (user_id, )) + rows = cursor.fetchall() + + return [ + DocumentsStandardModel( + id = row[0], + category_id = row[1], + user_id = row[2], + name = row[3], + path = row[4], + created_at = row[5], + access = row[6] + ) + for row in rows + ] + + def get_entries_by_category(self, category_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_standard WHERE category_id = ?", (category_id, )) + rows = cursor.fetchall() + return [ + DocumentsStandardModel( + id = row[0], + category_id = row[1], + user_id = row[2], + name = row[3], + path = row[4], + created_at = row[5], + access = row[6] + ) + for row in rows + ] + + def get_entry_by_id(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM documents_standard WHERE id = ?", (id, )) + row = cursor.fetchone() + if row: + return DocumentsStandardModel( + id = row[0], + category_id = row[1], + user_id = row[2], + name = row[3], + path = row[4], + created_at = row[5], + access = row[6] + ) + return None + + def delete_entry(self, id): + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + # SQLite allows direct string comparison for ISO 8601 dates + cursor.execute( + "DELETE FROM documents_standard WHERE id = ? ", + (id,) + ) + conn.commit() + return cursor.rowcount # Returns the number of deleted rows + except sqlite3.Error as e: + print(f"An error occurred: {e}") + return 0 \ No newline at end of file diff --git a/server/models/payments/__pycache__/payments.cpython-313.pyc b/server/models/payments/__pycache__/payments.cpython-313.pyc new file mode 100644 index 0000000..1fb3668 Binary files /dev/null and b/server/models/payments/__pycache__/payments.cpython-313.pyc differ diff --git a/server/models/payments/__pycache__/subscriptions.cpython-313.pyc b/server/models/payments/__pycache__/subscriptions.cpython-313.pyc new file mode 100644 index 0000000..79f9bc6 Binary files /dev/null and b/server/models/payments/__pycache__/subscriptions.cpython-313.pyc differ diff --git a/server/models/payments/__pycache__/subscriptions_and_payments.cpython-313.pyc b/server/models/payments/__pycache__/subscriptions_and_payments.cpython-313.pyc new file mode 100644 index 0000000..2580d2d Binary files /dev/null and b/server/models/payments/__pycache__/subscriptions_and_payments.cpython-313.pyc differ diff --git a/server/models/payments/payments.py b/server/models/payments/payments.py new file mode 100644 index 0000000..608f883 --- /dev/null +++ b/server/models/payments/payments.py @@ -0,0 +1,124 @@ +import sqlite3 +from dataclasses import dataclass +from typing import Optional + +@dataclass +class PaymentsModel: + id: Optional[int] = None + user_id: Optional[int] = None + name: Optional[str] = None + amount: Optional[float] = None + type: Optional[str] = None + created_at: Optional[str] = None + +class Payments: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_payment_table() + + def _create_payment_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS payments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name TEXT, + amount REAL, + type TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """ + ) + conn.commit() + + def add_payment(self, payment: PaymentsModel): + """Adds a new payment entry to the database.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO subscriptions_and_payments (user_id, name, amount, type) + VALUES (?, ?, ?, ?) + """, + (payment.user_id, payment.name, payment.amount, payment.type), + ) + conn.commit() + return cursor.lastrowid + except sqlite3.IntegrityError: + return None + + def get_payment(self, payment_id: int) -> PaymentsModel | None: + """Retrieves a single payment entry by its ID.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM subscriptions_and_payments WHERE id = ?", (payment_id,)) + row = cursor.fetchone() + + if not row: + return None + + return PaymentsModel( + id=row[0], + user_id=row[1], + name=row[2], + amount=row[3], + type=row[4], + created_at=row[5] + ) + + def get_all_payments(self): + """Retrieves all payment entries from the database.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM subscriptions_and_payments") + rows = cursor.fetchall() + + return [ + PaymentsModel( + id=row[0], + user_id=row[1], + name=row[2], + amount=row[3], + type=row[4], + created_at=row[5] + ) + for row in rows + ] + + def update_payment(self, payment_id: int, name: Optional[str] = None, amount: Optional[float] = None, type: Optional[str] = None): + """Updates an existing payment entry.""" + fields = [] + params = [] + + if name is not None: + fields.append("name = ?") + params.append(name) + if amount is not None: + fields.append("amount = ?") + params.append(amount) + if type is not None: + fields.append("type = ?") + params.append(type) + + if not fields: + return False # No fields to update + + params.append(payment_id) + query = f"UPDATE subscriptions_and_payments SET {', '.join(fields)} WHERE id = ?" + + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(query, tuple(params)) + conn.commit() + return cursor.rowcount > 0 + + def delete_payment(self, payment_id: int): + """Deletes a payment entry by its ID.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM subscriptions_and_payments WHERE id = ?", (payment_id,)) + conn.commit() + return cursor.rowcount > 0 \ No newline at end of file diff --git a/server/models/payments/subscriptions.py b/server/models/payments/subscriptions.py new file mode 100644 index 0000000..32f6cb7 --- /dev/null +++ b/server/models/payments/subscriptions.py @@ -0,0 +1,112 @@ +import sqlite3 +from dataclasses import dataclass +from typing import Optional + +@dataclass +class SubscriptionsModel: + id: Optional[int] = None + user_id: Optional[int] = None + name: Optional[str] = None # Numele abonamentului (ex: "Abonament Lunar", "Abonament Anual") + pay_and_subs_id: Optional[int] = None # ID-ul plății asociate din tabela subscriptions_and_payments + mounts: Optional[int] = None + created_at: Optional[str] = None + +class Subscriptions: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_subscription_table() + + def _create_subscription_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name TEXT, + pay_and_subs_id INTEGER, + mounts INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (pay_and_subs_id) REFERENCES subscriptions_and_payments(id) + ); + """ + ) + conn.commit() + + def add_subscription(self, subscription: SubscriptionsModel): + """Adds a new subscription entry to the database.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO subscriptions (user_id, name, pay_and_subs_id, mounts) + VALUES (?, ?, ?, ?) + """, + (subscription.user_id, subscription.name, subscription.pay_and_subs_id, subscription.mounts), + ) + conn.commit() + return cursor.lastrowid + except sqlite3.Error: + return None + + def get_subscription(self, subscription_id: int) -> Optional[SubscriptionsModel]: + """Retrieves a single subscription entry by its ID.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM subscriptions WHERE id = ?", (subscription_id,)) + row = cursor.fetchone() + if row: + return SubscriptionsModel(*row) + return None + + def get_all_subscriptions(self): + """Retrieves all subscription entries from the database.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM subscriptions") + rows = cursor.fetchall() + return [SubscriptionsModel(*row) for row in rows] + + def get_subscriptions_by_user_id(self, user_id: int): + """Retrieves all subscriptions for a specific user.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM subscriptions WHERE user_id = ?", (user_id,)) + rows = cursor.fetchall() + return [SubscriptionsModel(*row) for row in rows] + + def update_subscription(self, subscription_id: int, name: Optional[str] = None, pay_and_subs_id: Optional[int] = None, mounts: Optional[int] = None): + """Updates an existing subscription entry.""" + fields = [] + params = [] + if name is not None: + fields.append("name = ?") + params.append(name) + if pay_and_subs_id is not None: + fields.append("pay_and_subs_id = ?") + params.append(pay_and_subs_id) + if mounts is not None: + fields.append("mounts = ?") + params.append(mounts) + + if not fields: + return False + + params.append(subscription_id) + query = f"UPDATE subscriptions SET {', '.join(fields)} WHERE id = ?" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(query, tuple(params)) + conn.commit() + return cursor.rowcount > 0 + + def delete_subscription(self, subscription_id: int): + """Deletes a subscription entry by its ID.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM subscriptions WHERE id = ?", (subscription_id,)) + conn.commit() + return cursor.rowcount > 0 \ No newline at end of file diff --git a/server/models/users.py b/server/models/users.py new file mode 100644 index 0000000..f42bf06 --- /dev/null +++ b/server/models/users.py @@ -0,0 +1,323 @@ +import sqlite3 +from dataclasses import dataclass +import hashlib +from typing import Optional + +@dataclass +class UserModel: + id: Optional[int] = None + workspace_id: Optional[int] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + email: Optional[str] = None + password: Optional[str] = None + address: Optional[str] = None + profession: Optional[str] = None + role: Optional[str] = None + status: Optional[str] = None + profile_pic: Optional[str] = None + created_at: Optional[str] = None + otp_code: Optional[str] = None + otp_expiration: Optional[str] = None + active: Optional[int] = None + +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): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + workspace_id INTEGER NOT NULL, + first_name TEXT, + last_name TEXT, + email TEXT UNIQUE, + password TEXT, + address TEXT, + profession TEXT, + role TEXT DEFAULT 'user', + status TEXT, + profile_pic TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + otp_code TEXT, + otp_expiration TIMESTAMPTZ, + active INTEGER DEFAULT 1 + ); + """ + ) + conn.commit() + + def update_user_otp(self, user_id, otp_code, expiration): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + f""" + UPDATE users + SET otp_code = ?, otp_expiration = ? + WHERE id = ? + """, + (otp_code, expiration.isoformat(), user_id) + ) + conn.commit() + + def clear_user_otp(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + f""" + UPDATE users + SET otp_code = NULL, otp_expiration = NULL + WHERE id = ? + """, + (user_id,) + ) + if hasattr(conn, "commit"): + conn.commit() + + def update_password(self, email, password): + '''Update user password''' + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE users SET password = ? + WHERE email = ? + ''', (self.hash_password(password), email)) + conn.commit() + + def hash_password(self, password: str) -> bytes: + return hashlib.md5(password.encode('utf-8')).hexdigest() + + def authenticate(self, email, password): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM users + WHERE email = ? AND password = ? + """, (email, self.hash_password(password))) + row = cursor.fetchone() + if not row: + return None + + return UserModel( + id=row[0], + workspace_id=row[1], + first_name=row[2], + last_name=row[3], + email=row[4], + address=row[6], + profession=row[7], + role=row[8], + status=row[9], + profile_pic=row[10], + created_at=row[11], + otp_code=row[12], + otp_expiration=row[13], + active=row[14] + ) + + def register_user(self, email, password, workspace_id): + """Register a new user.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO users (workspace_id, email, password) + VALUES (?, ?, ?) + """, (workspace_id, email, self.hash_password(password))) + conn.commit() + return True + except sqlite3.IntegrityError: + return False # Username already exist + + def add_user(self, user:UserModel): + """Create a new post.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO users (workspace_id, first_name, last_name, email, password, address, profession, role, status, profile_pic) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (user.workspace_id, user.first_name, user.last_name, user.email, user.password, user.address, user.profession, user.role, user.status, user.profile_pic), + ) + conn.commit() + return cursor.lastrowid + except sqlite3.IntegrityError: + return None + + def get_user(self, user_id: int) -> UserModel | None: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + row = cursor.fetchone() + + if not row: + return None + + return UserModel( + id=row[0], + workspace_id=row[1], + first_name=row[2], + last_name=row[3], + email=row[4], + address=row[6], + profession=row[7], + role=row[8], + status=row[9], + profile_pic=row[10], + created_at=row[11], + otp_code=row[12], + otp_expiration=row[13], + active=row[14] + ) + + def get_user_by_email(self, email: str) -> UserModel | None: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM users WHERE email = ?", (email,)) + row = cursor.fetchone() + + if not row: + return None + + return UserModel( + id=row[0], + workspace_id=row[1], + first_name=row[2], + last_name=row[3], + email=row[4], + password=row[5], + address=row[6], + profession=row[7], + role=row[8], + status=row[9], + profile_pic=row[10], + created_at=row[11], + otp_code=row[12], + otp_expiration=row[13], + active=row[14] + ) + + def get_users_by_workspace_id(self, workspace_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM users WHERE workspace_id = ?", (workspace_id,)) + rows = cursor.fetchall() + + return [ + UserModel( + id=row[0], + workspace_id=row[1], + first_name=row[2], + last_name=row[3], + email=row[4], + address=row[6], + profession=row[7], + role=row[8], + status=row[9], + profile_pic=row[10], + created_at=row[11], + otp_code=row[12], + otp_expiration=row[13], + active=row[14] + ) + for row in rows + ] + + def get_all_users(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM users") + rows = cursor.fetchall() + + return [ + UserModel( + id=row[0], + workspace_id=row[1], + first_name=row[2], + last_name=row[3], + email=row[4], + address=row[6], + profession=row[7], + role=row[8], + status=row[9], + profile_pic=row[10], + created_at=row[11], + otp_code=row[12], + otp_expiration=row[13], + active=row[14] + ) + for row in rows + ] + + def update_user(self, user_id, first_name=None, last_name=None, email=None, password = None, address = None, profession = None, role = None, status = None, profile_pic=None, active=None): + if first_name is None and last_name is None and email is None and password is None and address is None and profession is None and role is None and status is None and profile_pic is None and active is None: + return False + + fields = [] + params = [] + + if first_name is not None: + fields.append("first_name = ?") + params.append(first_name) + if last_name is not None: + fields.append("last_name = ?") + params.append(last_name) + if email is not None: + fields.append("email = ?") + params.append(email) + if password is not None: + fields.append("password = ?") + params.append(password) + if address is not None: + fields.append("address = ?") + params.append(address) + if profession is not None: + fields.append("profession = ?") + params.append(profession) + if role is not None: + fields.append("role = ?") + params.append(role) + if status is not None: + fields.append("status = ?") + params.append(status) + if profile_pic is not None: + fields.append("profile_pic = ?") + params.append(profile_pic) + if active is not None: + fields.append("active = ?") + params.append(active) + + params.append(user_id) + query = f"UPDATE users SET {', '.join(fields)} WHERE id = ?" + + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(query, tuple(params)) + conn.commit() + return cursor.rowcount > 0 + + #Do not use this method if you do not delete first in cascade, better use inactivate + def delete_user(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + cursor.execute("DELETE FROM users WHERE id = ?", (user_id,)) + + conn.commit() + return cursor.rowcount > 0 + + def inactivate_user(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + cursor.execute("UPDATE users SET status = ? WHERE id = ?", ('inactive',user_id,)) + + conn.commit() + return cursor.rowcount > 0 \ No newline at end of file diff --git a/server/routes/__pycache__/auth.cpython-313.pyc b/server/routes/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..210a6cb Binary files /dev/null and b/server/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/server/routes/__pycache__/documents.cpython-313.pyc b/server/routes/__pycache__/documents.cpython-313.pyc new file mode 100644 index 0000000..c76978f Binary files /dev/null and b/server/routes/__pycache__/documents.cpython-313.pyc differ diff --git a/server/routes/__pycache__/payments.cpython-313.pyc b/server/routes/__pycache__/payments.cpython-313.pyc new file mode 100644 index 0000000..7cafdb6 Binary files /dev/null and b/server/routes/__pycache__/payments.cpython-313.pyc differ diff --git a/server/routes/__pycache__/subscriptions.cpython-313.pyc b/server/routes/__pycache__/subscriptions.cpython-313.pyc new file mode 100644 index 0000000..a763aff Binary files /dev/null and b/server/routes/__pycache__/subscriptions.cpython-313.pyc differ diff --git a/server/routes/__pycache__/subscriptions_and_payments.cpython-313.pyc b/server/routes/__pycache__/subscriptions_and_payments.cpython-313.pyc new file mode 100644 index 0000000..1aca5f4 Binary files /dev/null and b/server/routes/__pycache__/subscriptions_and_payments.cpython-313.pyc differ diff --git a/server/routes/__pycache__/users.cpython-313.pyc b/server/routes/__pycache__/users.cpython-313.pyc new file mode 100644 index 0000000..50860cc Binary files /dev/null and b/server/routes/__pycache__/users.cpython-313.pyc differ diff --git a/server/routes/auth.py b/server/routes/auth.py new file mode 100644 index 0000000..2858567 --- /dev/null +++ b/server/routes/auth.py @@ -0,0 +1,295 @@ +from flask import Blueprint, request, jsonify +#from werkzeug.security import generate_password_hash, check_password_hash +from utils.email import send_email, send_gmail +from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity +from flask_jwt_extended import decode_token +import datetime +import random +import os +from datetime import timezone +import hashlib +from models.users import Users +from utils.welcome_email import WelcomeMessage +from models.audit import Audit, AuditModel + +auth_bp = Blueprint("auth", __name__) +audit = Audit() + +def hash_password(password: str) -> bytes: + return hashlib.md5(password.encode('utf-8')).hexdigest() + +@auth_bp.route("/register", methods=["POST"]) +def register(): + users = Users() + data = request.get_json() + email = data.get("email") + password = data.get("password") + workspace_id = data.get("workspace_id") + + if not email or not password: + return jsonify({"error": "Missing required fields"}), 400 + + existing_user = users.get_user_by_email(email) + if existing_user: + entry = AuditModel(action=f"Attempt to register an existing email:{email}", status='409 - User already exists') + audit.new_entry(entry) + return jsonify({"error": "User already exists"}), 409 + + users.register_user(email, password, workspace_id) + + welcome_message = WelcomeMessage(email) + welcome_message.send_email() + entry = AuditModel(action=f"User registered successfully:{email}", status='201 - User created') + audit.new_entry(entry) + return jsonify({"message": "User registered successfully!"}), 201 + +@auth_bp.route("/login", methods=["POST"]) +def login(): + users = Users() + data = request.get_json() + email = data.get("email", "").strip().lower() + password = data.get("password", "") + + if not email or not password: + entry = AuditModel(user_id=user.id, action=f"Attempt to login: {email}", status='400 - Missing email or password') + audit.new_entry(entry) + return jsonify({"error": "Missing email or password"}), 400 + + user = users.get_user_by_email(email) + if not user or not hash_password(password)==user.password: + print(user.password, password) + entry = AuditModel(user_id=user.id ,action=f"Attempt to login: {email}", status='401 - Invalid credentials') + audit.new_entry(entry) + return jsonify({"error": "Invalid credentials"}), 401 + + if user.active != 1: + entry = AuditModel(user_id=user.id, action=f"Attempt to login: {email}", status='401 - User inactive') + audit.new_entry(entry) + return jsonify({"error": "Inactive user"}), 401 + + otp_code = str(random.randint(100000, 999999)) + expiration = datetime.datetime.now(timezone.utc) + datetime.timedelta(minutes=10) + users.update_user_otp(user.id, otp_code, expiration) + + send_gmail( + to_email=user.email, + subject="Your Login Verification Code", + body=f"Your login verification code is: {otp_code}" + ) + entry = AuditModel(user_id=user.id, action=f"Attempt to login: {email}", status='200 - Verification code send!') + audit.new_entry(entry) + return jsonify({"message": "Verification code sent to your email."}), 200 + + +@auth_bp.route("/verify_code", methods=["POST"]) +def verify_code(): + users = Users() + data = request.get_json() + email = data.get("email", "").strip().lower() + code = data.get("code", "") + + if not email or not code: + entry = AuditModel(action=f"Attempt to verify code: {email}", status='400 - Missing email or verification code!') + audit.new_entry(entry) + return jsonify({"error": "Missing email or verification code"}), 400 + + user = users.get_user_by_email(email) + #-----------------------------------------------> for testing only remove in prod + #if email != 'test@test.com': + #-----------------------------------------------> for testing only remove in prod + if not user or user.otp_code != code: + entry = AuditModel(user_id=user.id, action=f"Attempt to verify code: {email}", status='401 - Invalid code!') + audit.new_entry(entry) + return jsonify({"error": "Invalid code"}), 401 + + exp = user.otp_expiration + # Normalize to aware UTC datetime for safe comparison across SQLite (string) and Postgres (datetime) + now_utc = datetime.datetime.now(timezone.utc) + if isinstance(exp, str): + try: + exp_dt = datetime.datetime.fromisoformat(exp) + except Exception: + return jsonify({"error": "Invalid expiration format"}), 500 + if exp_dt.tzinfo is None: + exp_dt = exp_dt.replace(tzinfo=timezone.utc) + else: + # Assume a datetime object from DB driver + exp_dt = exp + if exp_dt is None: + entry = AuditModel(user_id=user.id, action=f"Attempt to verify code:{email}", status='500 - Missing expiration!') + audit.new_entry(entry) + return jsonify({"error": "Missing expiration"}), 500 + if exp_dt.tzinfo is None: + exp_dt = exp_dt.replace(tzinfo=timezone.utc) + if now_utc > exp_dt: + entry = AuditModel(user_id=user.id, action=f"Attempt to verify code:{email}", status='403 - Verification code has expired!') + audit.new_entry(entry) + return jsonify({"error": "Verification code has expired"}), 403 + + users.clear_user_otp(user.id) + + access_token = create_access_token( + identity=str(user.id), + expires_delta=datetime.timedelta(hours=12) + ) + entry = AuditModel(user_id=user.id, action=f"Attempt to verify code:{email}", status='200 Ok - Login successful!') + audit.new_entry(entry) + return jsonify({ + "message": "Login successful", + "access_token": access_token + }), 200 + + +@auth_bp.route("/forgot_password", methods=["POST"]) +def forgot_password(): + users = Users() + data = request.get_json() + email = data.get("email", "").strip().lower() + + if not email: + entry = AuditModel(action=f"Attempt to recover password", status='400 - Email is required!') + audit.new_entry(entry) + return jsonify({"error": "Email is required"}), 400 + + user = users.get_user_by_email(email) + if user: + reset_token = create_access_token( + identity=user["id"], + expires_delta=datetime.timedelta(minutes=15), + additional_claims={"purpose": "password_reset"} + ) + + send_gmail( + to_email=user["email"], + subject="Password Reset Request", + body=( + "Click the link to reset your password: " + f"{os.getenv('FRONTEND_BASE_URL', 'http://127.0.0.1:5100')}/reset_password?token={reset_token}" + ) + ) + + entry = AuditModel(user_id=user.id, action=f"Attempt to recover password: {email}", status='200 Ok - If this email is registered, a reset link has been sent!') + audit.new_entry(entry) + return jsonify({"message": "If this email is registered, a reset link has been sent."}), 200 + + +@auth_bp.route("/reset_password", methods=["POST"]) +def reset_password(): + users = Users() + data = request.get_json() + token = data.get("token", "") + new_password = data.get("new_password", "") + + if not token or not new_password: + entry = AuditModel( action=f"Attempt to recover password", status='400 - Missing token or new password!') + audit.new_entry(entry) + return jsonify({"error": "Missing token or new password"}), 400 + + try: + decoded_token = decode_token(token) + if decoded_token.get("purpose") != "password_reset": + entry = AuditModel( action=f"Attempt to recover password", status='403 - Invalid token purpose!') + audit.new_entry(entry) + return jsonify({"error": "Invalid token purpose"}), 403 + except Exception: + entry = AuditModel( action=f"Attempt to recover password", status='403 - Invalid or expired token!') + audit.new_entry(entry) + return jsonify({"error": "Invalid or expired token"}), 403 + + user_id = decoded_token["sub"] + user = users.get_user(user_id) + if not user: + entry = AuditModel( action=f"Attempt to recover password", status='404 - User not found!') + audit.new_entry(entry) + return jsonify({"error": "User not found"}), 404 + + users.update_password(user_id, new_password) + entry = AuditModel(user_id=user.id, action=f"Attempt to recover password:{user.email}", status='200 ok - Password has been reset successfully.') + audit.new_entry(entry) + return jsonify({"message": "Password has been reset successfully."}), 200 + + +@auth_bp.route("/me", methods=["GET"]) +@jwt_required() +def me(): + users = Users() + user_id = get_jwt_identity() + user = users.get_user(user_id) + if not user: + entry = AuditModel( action=f"Get user data: ", status='404 - User not found.') + audit.new_entry(entry) + return jsonify({"error": "User not found"}), 404 + + entry = AuditModel(user_id=user.id, action=f"Get user data: {user.email}", status='200 Ok.') + audit.new_entry(entry) + return jsonify({ + 'id': user.id, + 'workspace_id': user.workspace_id, + 'first_name':user.first_name, + 'last_name': user.last_name, + 'email':user.email, + 'address':user.address, + 'profession':user.profession, + 'role':user.role, + 'status': user.status, + 'profile_pic': user.profile_pic, + 'created_at': user.created_at, + 'otp_code': user.otp_code, + 'otp_expiration': user.otp_expiration + }), 200 + + +# Validate token endpoint +@auth_bp.route("/validate_token", methods=["GET"]) +@jwt_required() +def validate_token(): + users = Users() + user_id = get_jwt_identity() + user = users.get_user(user_id) + if not user: + entry = AuditModel(action=f"Get access token:", status='404 - User not found.') + audit.new_entry(entry) + return jsonify({"error": "User not found"}), 404 + entry = AuditModel(user_id=user.id, action=f"Get access token: {user.email}", status='200 Ok - Token is valid') + audit.new_entry(entry) + return jsonify({"message": "Token is valid"}), 200 + +# @auth_bp.route("/temporary_password", methods=["POST"]) +# @jwt_required() +# def change_passwd(): +# data = request.get_json() +# if not data: +# entry = AuditModel(action=f"Get temporary password token:", status='404 - Password not found.') +# audit.new_entry(entry) +# return jsonify({"error": "Password not found"}), 404 +# users = Users() +# user_id = get_jwt_identity() +# new_password_hash = generate_password_hash(data['password']) +# users.update_user_password(user_id, new_password_hash) +# users.update_temp_pass(user_id) +# entry = AuditModel(user_id=user_id, action=f"Get temporary password:", status='200 Ok - Password has been updated successfully.') +# audit.new_entry(entry) +# return jsonify({"message": "Password has been updated successfully."}), 200 + + +@auth_bp.route("/update_passwrod", methods=["POST"]) +def update_passwrod(): + data = request.get_json() + email = data.get("email", "").strip().lower() + password = data.get('password') + token = data.get('token') + env_token = os.getenv('PASSWORD_TOKEN') + if not email and not password and not token: + entry = AuditModel(action=f"Update Password:", status='403 - Data not provided.') + audit.new_entry(entry) + return jsonify({"error": "Data not provided"}), 403 + if env_token != env_token: + entry = AuditModel(action=f"Update Password:", status='401 - Invalid token.') + audit.new_entry(entry) + return jsonify({"error": "Data not provided"}), 401 + + users = Users() + users.update_password(email, password) + entry = AuditModel(action=f"Update Password:", status='200 - Password has been updated successfully.') + audit.new_entry(entry) + return jsonify({"message": "Password has been updated successfully."}), 200 \ No newline at end of file diff --git a/server/routes/documents.py b/server/routes/documents.py new file mode 100644 index 0000000..8857a39 --- /dev/null +++ b/server/routes/documents.py @@ -0,0 +1,417 @@ +import os +import shutil +from flask import Blueprint, request, jsonify, send_from_directory +from flask_jwt_extended import jwt_required, get_jwt_identity +from models.documents.documents_category import DocumentsCategory, DocumentsCategoryModel +from models.documents.documents_standard import DocumentsStandard, DocumentsStandardModel +from models.documents.documents_custom import DocumentsCustom, DocumentsCustomModel, CustomDocumentRequests, CustomDocumentRequestModel +from models.audit import Audit, AuditModel + +documents_bp = Blueprint("documents", __name__) +audit = Audit() + +# Definirea căii către folderul principal de documente (la nivelul server/client) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +DOCUMENTS_ROOT = os.path.join(BASE_DIR, "documents") +os.makedirs(DOCUMENTS_ROOT, exist_ok=True) + +# --- Document Categories --- + +@documents_bp.route("/categories/add", methods=["POST"]) +@jwt_required() +def add_category(): + user_id = get_jwt_identity() + data = request.get_json() + name = data.get("name") + access = data.get("access") + + if not name: + entry = AuditModel(user_id=user_id, action="Attempt to create category", status="400 - Missing name") + audit.new_entry(entry) + return jsonify({"error": "Missing name"}), 400 + + docs_cat = DocumentsCategory() + # Filesafe: Verificăm dacă există deja o categorie cu acest nume (global) + # pentru a preveni duplicatele în DB și conflictele în sistemul de fișiere. + existing_categories = docs_cat.get_all_entries() + for cat in existing_categories: + if cat.name and cat.name.lower() == name.lower(): + return jsonify({"message": "Category already exists", "id": cat.id}), 200 + + category = DocumentsCategoryModel(user_id=user_id, name=name, access=access) + result = docs_cat.new_entry(category) + + if result: + # Creare folder fizic pentru categorie + category_path = os.path.join(DOCUMENTS_ROOT, name) + os.makedirs(category_path, exist_ok=True) + + entry = AuditModel(user_id=user_id, action=f"Category created: {name}", status="201 - Created") + audit.new_entry(entry) + return jsonify({"message": "Category created successfully", "id": result}), 201 + + return jsonify({"error": "Failed to create category"}), 500 + +@documents_bp.route("/categories", methods=["GET"]) +@jwt_required() +def get_categories(): + docs_cat = DocumentsCategory() + categories = docs_cat.get_all_entries() + return jsonify([vars(c) for c in categories]), 200 + +@documents_bp.route("/categories/update/", methods=["PUT"]) +@jwt_required() +def update_category(id): + user_id = get_jwt_identity() + data = request.get_json() + name = data.get("name") + access = data.get("access") + + if not name: + return jsonify({"error": "Missing name"}), 400 + + docs_cat = DocumentsCategory() + old_category = docs_cat.get_entry_by_id(id) + + if not old_category: + return jsonify({"error": "Category not found"}), 404 + + updated_count = docs_cat.update_entry(id, name, access) + + if updated_count: + # Redenumire folder dacă numele s-a schimbat + if old_category.name != name: + old_path = os.path.join(DOCUMENTS_ROOT, old_category.name) + new_path = os.path.join(DOCUMENTS_ROOT, name) + if os.path.exists(old_path): + os.rename(old_path, new_path) + else: + os.makedirs(new_path, exist_ok=True) + + entry = AuditModel(user_id=user_id, action=f"Category updated: {name} (ID: {id})", status="200 - OK") + audit.new_entry(entry) + return jsonify({"message": "Category updated successfully"}), 200 + + return jsonify({"error": "Category not found"}), 404 + +@documents_bp.route("/categories/delete/", methods=["DELETE"]) +@jwt_required() +def delete_category(id): + user_id = get_jwt_identity() + docs_cat = DocumentsCategory() + + # Preluăm categoria înainte de ștergere pentru a cunoaște numele folderului + category = docs_cat.get_entry_by_id(id) + if not category: + return jsonify({"error": "Category not found"}), 404 + + deleted_count = docs_cat.delete_entry(id) + if deleted_count: + # Ștergere folder fizic și tot conținutul său (documente încărcate) + category_path = os.path.join(DOCUMENTS_ROOT, category.name) + if os.path.exists(category_path): + shutil.rmtree(category_path) + + entry = AuditModel(user_id=user_id, action=f"Deleted Category: {category.name} (ID: {id})", status="200 - OK") + audit.new_entry(entry) + return jsonify({"message": "Category deleted successfully"}), 200 + + return jsonify({"error": "Category not found"}), 404 + +@documents_bp.route("/download", methods=["GET"]) +@jwt_required() +def download_file(): + # Path-ul vine ca parametru (ex: NumeCategorie/fisier.pdf) + file_path = request.args.get("path") + if not file_path: + return jsonify({"error": "Missing path"}), 400 + + # Securitate: prevenim ieșirea din DOCUMENTS_ROOT + abs_path = os.path.abspath(os.path.join(DOCUMENTS_ROOT, file_path)) + if not abs_path.startswith(os.path.abspath(DOCUMENTS_ROOT)): + return jsonify({"error": "Unauthorized access"}), 403 + + directory = os.path.dirname(abs_path) + filename = os.path.basename(abs_path) + return send_from_directory(directory, filename, as_attachment=True) + +@documents_bp.route("/categories/refresh/", methods=["POST"]) +@jwt_required() +def refresh_category_files(category_id): + user_id = get_jwt_identity() + docs_cat = DocumentsCategory() + docs_std = DocumentsStandard() + + category = docs_cat.get_entry_by_id(category_id) + if not category: + return jsonify({"error": "Category not found"}), 404 + + category_path = os.path.join(DOCUMENTS_ROOT, category.name) + if not os.path.exists(category_path): + return jsonify({"error": "Category folder does not exist on disk"}), 404 + + # Obținem lista fișierelor de pe disc + files_on_disk = [f for f in os.listdir(category_path) if os.path.isfile(os.path.join(category_path, f))] + + # Obținem lista documentelor din DB pentru această categorie + db_entries = docs_std.get_entries_by_category(category_id) + db_file_paths = {entry.path for entry in db_entries} + + added_count = 0 + for filename in files_on_disk: + # Reconstruim path-ul așa cum este stocat în DB: "NumeCategorie/fisier.ext" + relative_path = f"{category.name}/{filename}" + + if relative_path not in db_file_paths: + new_doc = DocumentsStandardModel( + category_id=category_id, + user_id=0, # User ID 0 pentru fișiere adăugate extern + name=filename, + path=relative_path, + access=category.access + ) + if docs_std.new_entry(new_doc): + added_count += 1 + + if added_count > 0: + audit.new_entry(AuditModel(user_id=user_id, action=f"Refreshed category {category.name}: added {added_count} files", status="200 - OK")) + + return jsonify({"message": f"Refresh complete. Added {added_count} new documents.", "added": added_count}), 200 + +# --- Document Standards Category --- +@documents_bp.route("/standards/category/", methods=["GET"]) +@jwt_required() +def get_standards_by_category(category_id): + docs_std = DocumentsStandard() + items = docs_std.get_entries_by_category(category_id) + return jsonify([vars(i) for i in items]), 200 + +# --- Document Standards --- + +@documents_bp.route("/standards/add", methods=["POST"]) +@jwt_required() +def add_standard(): + user_id = get_jwt_identity() + data = request.get_json() + name = data.get("name") + path = data.get("path") + access = data.get("access") + category_id = data.get("category_id") + + if not name or not path or not category_id: + entry = AuditModel(user_id=user_id, action="Attempt to create standard", status="400 - Missing fields") + audit.new_entry(entry) + return jsonify({"error": "Missing name, path or category_id"}), 400 + + # Mutare fizică a fișierului din folderul de upload în folderul categoriei + # Sursa este folderul unde Flet salvează implicit upload-urile + source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) + dest_path = os.path.join(DOCUMENTS_ROOT, path) + + if os.path.exists(source_path): + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + shutil.move(source_path, dest_path) + + docs_std = DocumentsStandard() + standard = DocumentsStandardModel(user_id=user_id, category_id=category_id, name=name, path=path, access=access) + result = docs_std.new_entry(standard) + + if result: + entry = AuditModel(user_id=user_id, action=f"Standard created: {name}", status="201 - Created") + audit.new_entry(entry) + return jsonify({"message": "Standard created successfully", "id": result}), 201 + + return jsonify({"error": "Failed to create standard"}), 500 + +@documents_bp.route("/standards", methods=["GET"]) +@jwt_required() +def get_standards(): + user_id = get_jwt_identity() + docs_std = DocumentsStandard() + standards = docs_std.get_all_entries() + return jsonify([vars(s) for s in standards]), 200 + +@documents_bp.route("/standards/delete/", methods=["DELETE"]) +@jwt_required() +def delete_standard(id): + user_id = get_jwt_identity() + docs_std = DocumentsStandard() + + # Preluăm documentul înainte de ștergere pentru a cunoaște calea fișierului + standard = docs_std.get_entry_by_id(id) + if not standard: + return jsonify({"error": "Standard document not found"}), 404 + + deleted_count = docs_std.delete_entry(id) + if deleted_count: + # Ștergere fișier fizic de pe disc + file_path = os.path.join(DOCUMENTS_ROOT, standard.path) + if os.path.exists(file_path): + try: + os.remove(file_path) + except Exception as e: + print(f"Error deleting physical file: {e}") + + entry = AuditModel(user_id=user_id, action=f"Deleted Standard Document: {standard.name} (ID: {id})", status="200 - OK") + audit.new_entry(entry) + return jsonify({"message": "Standard document deleted successfully"}), 200 + + return jsonify({"error": "Failed to delete standard document"}), 500 + +# --- Document Custom --- + +@documents_bp.route("/customs/add", methods=["POST"]) +@jwt_required() +def add_custom(): + user_id = get_jwt_identity() + data = request.get_json() + name = data.get("name") + path = data.get("path") + access = data.get("access") + + if not name or not path: + entry = AuditModel(user_id=user_id, action="Attempt to create custom document", status="400 - Missing fields") + audit.new_entry(entry) + return jsonify({"error": "Missing name or path"}), 400 + + # Physical file move from upload folder to documents root + source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) + dest_path = os.path.join(DOCUMENTS_ROOT, path) + + if os.path.exists(source_path): + # Ensure the destination directory exists (for custom docs we usually put them in root or a 'custom' folder) + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + shutil.move(source_path, dest_path) + + docs_custom = DocumentsCustom() + custom = DocumentsCustomModel(user_id=user_id, name=name, path=path, access=access) + result = docs_custom.new_entry(custom) + + if result: + entry = AuditModel(user_id=user_id, action=f"Custom document created: {name}", status="201 - Created") + audit.new_entry(entry) + return jsonify({"message": "Custom document created successfully", "id": result}), 201 + + return jsonify({"error": "Failed to create custom document"}), 500 + +@documents_bp.route("/customs", methods=["GET"]) +@jwt_required() +def get_customs(): + user_id = get_jwt_identity() + docs_custom = DocumentsCustom() + customs = docs_custom.get_entries_by_user_id(user_id) + return jsonify([vars(c) for c in customs]), 200 + +@documents_bp.route("/customs/", methods=["GET"]) +@jwt_required() +def get_custom_by_id(id): + docs_custom = DocumentsCustom() + document = docs_custom.get_entry_by_id(id) + if not document: + return jsonify({"error": "Custom document not found"}), 404 + return jsonify(vars(document)), 200 + +@documents_bp.route("/customs/delete/", methods=["DELETE"]) +@jwt_required() +def delete_custom(id): + user_id = get_jwt_identity() + docs_custom = DocumentsCustom() + deleted_count = docs_custom.delete_entry(id) + if deleted_count: + entry = AuditModel(user_id=user_id, action=f"Deleted Custom Document ID: {id}", status="200 - OK") + audit.new_entry(entry) + return jsonify({"message": "Custom document deleted successfully"}), 200 + + return jsonify({"error": "Custom document not found"}), 404 + +# --- Custom Document Requests --- + +@documents_bp.route("/customs/requests/add", methods=["POST"]) +@jwt_required() +def add_custom_request(): + user_id = get_jwt_identity() + data = request.get_json() + request_text = data.get("request_text") + + if not request_text: + return jsonify({"error": "Missing request text"}), 400 + + repo = CustomDocumentRequests() + new_request = CustomDocumentRequestModel( + client_id=user_id, + request_text=request_text, + status="new" + ) + result = repo.new_entry(new_request) + + if result: + audit.new_entry(AuditModel(user_id=user_id, action=f"Custom request created: {result}", status="201 - Created")) + return jsonify({"message": "Request submitted successfully", "id": result}), 201 + + return jsonify({"error": "Failed to submit request"}), 500 + +@documents_bp.route("/customs/requests", methods=["GET"]) +@jwt_required() +def get_all_custom_requests(): + repo = CustomDocumentRequests() + requests_list = repo.get_all_entries() + return jsonify([vars(r) for r in requests_list]), 200 + +@documents_bp.route("/customs/requests/", methods=["GET"]) +@jwt_required() +def get_custom_request_by_id(id): + repo = CustomDocumentRequests() + req = repo.get_entry_by_id(id) + if not req: + return jsonify({"error": "Request not found"}), 404 + return jsonify(vars(req)), 200 + +@documents_bp.route("/customs/requests/client", methods=["GET"]) +@jwt_required() +def get_client_custom_requests(): + user_id = get_jwt_identity() + repo = CustomDocumentRequests() + requests_list = repo.get_entries_by_client_id(user_id) + return jsonify([vars(r) for r in requests_list]), 200 + +@documents_bp.route("/customs/requests/expert", methods=["GET"]) +@jwt_required() +def get_expert_custom_requests(): + user_id = get_jwt_identity() + repo = CustomDocumentRequests() + requests_list = repo.get_entries_by_expert_id(user_id) + return jsonify([vars(r) for r in requests_list]), 200 + +@documents_bp.route("/customs/requests/update/", methods=["PUT"]) +@jwt_required() +def update_custom_request(id): + user_id = get_jwt_identity() + data = request.get_json() + + repo = CustomDocumentRequests() + success = repo.update_entry( + id=id, + request_text=data.get("request_text"), + status=data.get("status"), + price=data.get("price"), + expert_id=data.get("expert_id"), + document_id=data.get("document_id") + ) + + if success: + audit.new_entry(AuditModel(user_id=user_id, action=f"Updated custom request ID: {id}", status="200 - OK")) + return jsonify({"message": "Request updated successfully"}), 200 + + return jsonify({"error": "Request not found or no updates provided"}), 404 + +@documents_bp.route("/customs/requests/delete/", methods=["DELETE"]) +@jwt_required() +def delete_custom_request(id): + user_id = get_jwt_identity() + repo = CustomDocumentRequests() + + if repo.delete_entry(id): + audit.new_entry(AuditModel(user_id=user_id, action=f"Deleted custom request ID: {id}", status="200 - OK")) + return jsonify({"message": "Request deleted successfully"}), 200 + + return jsonify({"error": "Request not found"}), 404 \ No newline at end of file diff --git a/server/routes/payments.py b/server/routes/payments.py new file mode 100644 index 0000000..c517825 --- /dev/null +++ b/server/routes/payments.py @@ -0,0 +1,85 @@ +import os +import shutil +from flask import Blueprint, request, jsonify, send_from_directory +from flask_jwt_extended import jwt_required, get_jwt_identity +from models.payments.payments import Payments, PaymentsModel +from models.audit import Audit, AuditModel + +payments_bp = Blueprint("payments", __name__) +audit = Audit() + +@payments_bp.route("/add", methods=["POST"]) +@jwt_required() +def add_payment(): + current_user_id = get_jwt_identity() + data = request.get_json() + + name = data.get("name") + amount = data.get("amount") + payment_type = data.get("type") + + if not name or amount is None or not payment_type: + return jsonify({"error": "Missing required fields (name, amount, type)"}), 400 + + payment_repo = Payments() + new_payment = PaymentsModel( + user_id=current_user_id, + name=name, + amount=float(amount), + type=payment_type + ) + + payment_id = payment_repo.add_payment(new_payment) + if payment_id: + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Added payment: {name}", status="201 - Created")) + return jsonify({"message": "Payment added successfully", "id": payment_id}), 201 + + return jsonify({"error": "Failed to add payment"}), 500 + +@payments_bp.route("/", methods=["GET"]) +@jwt_required() +def get_payments(): + payment_repo = Payments() + payments = payment_repo.get_all_payments() + return jsonify([vars(p) for p in payments]), 200 + +@payments_bp.route("/", methods=["GET"]) +@jwt_required() +def get_payment(payment_id): + payment_repo = Payments() + payment = payment_repo.get_payment(payment_id) + if not payment: + return jsonify({"error": "Payment not found"}), 404 + return jsonify(vars(payment)), 200 + +@payments_bp.route("/update/", methods=["PUT"]) +@jwt_required() +def update_payment(payment_id): + current_user_id = get_jwt_identity() + data = request.get_json() + payment_repo = Payments() + + success = payment_repo.update_payment( + payment_id, + name=data.get("name"), + amount=float(data.get("amount")) if data.get("amount") is not None else None, + type=data.get("type") + ) + + if success: + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Updated payment ID: {payment_id}", status="200 - OK")) + return jsonify({"message": "Payment updated successfully"}), 200 + + return jsonify({"error": "Payment not found or no valid fields to update"}), 404 + +@payments_bp.route("/delete/", methods=["DELETE"]) +@jwt_required() +def delete_payment(payment_id): + current_user_id = get_jwt_identity() + payment_repo = Payments() + + if payment_repo.delete_payment(payment_id): + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Deleted payment ID: {payment_id}", status="200 - OK")) + return jsonify({"message": "Payment deleted successfully"}), 200 + + return jsonify({"error": "Payment not found"}), 404 \ No newline at end of file diff --git a/server/routes/settings.py b/server/routes/settings.py new file mode 100644 index 0000000..f6f8bf6 --- /dev/null +++ b/server/routes/settings.py @@ -0,0 +1,21 @@ +from flask import Blueprint, request, jsonify +#from werkzeug.security import generate_password_hash, check_password_hash +from utils.email import send_email, send_gmail +from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity + +from models.audit import Audit, AuditModel + +import os + +auth_bp = Blueprint("settings", __name__) +audit = Audit() + +@auth_bp.route("/documente_juridice/add", methods=["POST"]) +@jwt_required() +def documente_juridice_add(): + data = request.get_json() + user_id = get_jwt_identity() + if 'category' not in data: + entry = AuditModel(user_id=user_id, action=f"Attempt to create a new category", status='400 - Missing category') + audit.new_entry(entry) + \ No newline at end of file diff --git a/server/routes/subscriptions.py b/server/routes/subscriptions.py new file mode 100644 index 0000000..755e2bf --- /dev/null +++ b/server/routes/subscriptions.py @@ -0,0 +1,71 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from models.payments.subscriptions import Subscriptions, SubscriptionsModel +from models.audit import Audit, AuditModel + +subscriptions_bp = Blueprint("subscriptions", __name__) +audit = Audit() + +@subscriptions_bp.route("/add", methods=["POST"]) +@jwt_required() +def add_subscription(): + current_user_id = get_jwt_identity() + data = request.get_json() + + name = data.get("name") + pay_and_subs_id = data.get("pay_and_subs_id") + mounts = data.get("mounts") + + if not name or not pay_and_subs_id: + return jsonify({"error": "Missing required fields (name, pay_and_subs_id)"}), 400 + + subs_repo = Subscriptions() + new_sub = SubscriptionsModel( + user_id=current_user_id, + name=name, + pay_and_subs_id=pay_and_subs_id, + mounts=mounts + ) + + sub_id = subs_repo.add_subscription(new_sub) + if sub_id: + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Added subscription: {name}", status="201 - Created")) + return jsonify({"message": "Subscription added successfully", "id": sub_id}), 201 + + return jsonify({"error": "Failed to add subscription"}), 500 + +@subscriptions_bp.route("/", methods=["GET"]) +@jwt_required() +def get_all_subscriptions(): + subs_repo = Subscriptions() + subscriptions = subs_repo.get_all_subscriptions() + return jsonify([vars(s) for s in subscriptions]), 200 + +@subscriptions_bp.route("/", methods=["GET"]) +@jwt_required() +def get_subscription(subscription_id): + subs_repo = Subscriptions() + subscription = subs_repo.get_subscription(subscription_id) + if not subscription: + return jsonify({"error": "Subscription not found"}), 404 + return jsonify(vars(subscription)), 200 + +@subscriptions_bp.route("/update/", methods=["PUT"]) +@jwt_required() +def update_subscription(subscription_id): + current_user_id = get_jwt_identity() + data = request.get_json() + subs_repo = Subscriptions() + + success = subs_repo.update_subscription( + subscription_id, + name=data.get("name"), + pay_and_subs_id=data.get("pay_and_subs_id"), + mounts=data.get("mounts") + ) + + if success: + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Updated subscription ID: {subscription_id}", status="200 - OK")) + return jsonify({"message": "Subscription updated successfully"}), 200 + + return jsonify({"error": "Subscription not found or no valid fields to update"}), 404 \ No newline at end of file diff --git a/server/routes/users.py b/server/routes/users.py new file mode 100644 index 0000000..83dc802 --- /dev/null +++ b/server/routes/users.py @@ -0,0 +1,108 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from models.users import Users, UserModel +from models.audit import Audit, AuditModel + +users_bp = Blueprint("users", __name__) +audit = Audit() + +@users_bp.route("/add", methods=["POST"]) +@jwt_required() +def add_user(): + current_admin_id = get_jwt_identity() + data = request.get_json() + + email = data.get("email") + workspace_id = data.get("workspace_id") + + if not email or not workspace_id: + return jsonify({"error": "Missing required fields (email, workspace_id)"}), 400 + + user_repo = Users() + + if user_repo.get_user_by_email(email): + return jsonify({"error": "User already exists"}), 409 + + new_user = UserModel( + workspace_id=workspace_id, + first_name=data.get("first_name"), + last_name=data.get("last_name"), + email=email, + password=user_repo.hash_password(data.get("password")) if data.get("password") else None, + address=data.get("address"), + profession=data.get("profession"), + role=data.get("role", "user"), + status=data.get("status", "active"), + profile_pic=data.get("profile_pic"), + active=1 + ) + + user_id = user_repo.add_user(new_user) + if user_id: + audit.new_entry(AuditModel(user_id=current_admin_id, action=f"Added user: {email}", status="201 - Created")) + return jsonify({"message": "User added successfully", "id": user_id}), 201 + + return jsonify({"error": "Failed to add user"}), 500 + +@users_bp.route("/", methods=["GET"]) +@jwt_required() +def get_user(user_id): + user_repo = Users() + user = user_repo.get_user(user_id) + if not user: + return jsonify({"error": "User not found"}), 404 + + # Convertim obiectul dataclass în dicționar pentru JSON + return jsonify(vars(user)), 200 + +@users_bp.route("/", methods=["GET"]) +@jwt_required() +def get_all_users(): + user_repo = Users() + users = user_repo.get_all_users() + + # Mapăm lista de obiecte UserModel la o listă de dicționare + return jsonify([vars(u) for u in users]), 200 + +@users_bp.route("/update/", methods=["PUT"]) +@jwt_required() +def update_user(user_id): + current_admin_id = get_jwt_identity() + data = request.get_json() + user_repo = Users() + + # Dacă se dorește actualizarea parolei, o hash-uim înainte de salvare + password = data.get("password") + hashed_password = user_repo.hash_password(password) if password else None + + success = user_repo.update_user( + user_id, + first_name=data.get("first_name"), + last_name=data.get("last_name"), + email=data.get("email"), + password=hashed_password, + address=data.get("address"), + profession=data.get("profession"), + role=data.get("role"), + status=data.get("status"), + profile_pic=data.get("profile_pic"), + active=data.get("active") + ) + + if success: + audit.new_entry(AuditModel(user_id=current_admin_id, action=f"Updated user ID: {user_id}", status="200 - OK")) + return jsonify({"message": "User updated successfully"}), 200 + + return jsonify({"error": "User not found or no valid fields to update"}), 404 + +@users_bp.route("/delete/", methods=["DELETE"]) +@jwt_required() +def delete_user(user_id): + current_admin_id = get_jwt_identity() + user_repo = Users() + + if user_repo.delete_user(user_id): + audit.new_entry(AuditModel(user_id=current_admin_id, action=f"Deleted user ID: {user_id}", status="200 - OK")) + return jsonify({"message": "User deleted successfully"}), 200 + + return jsonify({"error": "User not found"}), 404 \ No newline at end of file diff --git a/server/utils/__pycache__/email.cpython-313.pyc b/server/utils/__pycache__/email.cpython-313.pyc new file mode 100644 index 0000000..d53baf2 Binary files /dev/null and b/server/utils/__pycache__/email.cpython-313.pyc differ diff --git a/server/utils/__pycache__/welcome_email.cpython-313.pyc b/server/utils/__pycache__/welcome_email.cpython-313.pyc new file mode 100644 index 0000000..308624c Binary files /dev/null and b/server/utils/__pycache__/welcome_email.cpython-313.pyc differ diff --git a/server/utils/email.py b/server/utils/email.py new file mode 100644 index 0000000..86a5c77 --- /dev/null +++ b/server/utils/email.py @@ -0,0 +1,139 @@ +import smtplib +from email.message import EmailMessage +import os + +def send_email(to_email, subject, body): + smtp_host = os.environ.get("SMTP_HOST") + smtp_port = int(os.environ.get("SMTP_PORT", 587)) + smtp_user = os.environ.get("SMTP_USER") + smtp_pass = os.environ.get("SMTP_PASS") + sender_email = os.environ.get("SMTP_FROM", smtp_user) + + if not all([smtp_host, smtp_port, smtp_user, smtp_pass]): + raise ValueError("SMTP config incomplete in environment variables.") + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = to_email + msg.set_content(body) + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg) + + +# Send email with attachment +def send_email_with_attachment(to_email, subject, body, attachment_path): + smtp_host = os.environ.get("SMTP_HOST") + smtp_port = int(os.environ.get("SMTP_PORT", 587)) + smtp_user = os.environ.get("SMTP_USER") + smtp_pass = os.environ.get("SMTP_PASS") + sender_email = os.environ.get("SMTP_FROM", smtp_user) + + if not all([smtp_host, smtp_port, smtp_user, smtp_pass]): + raise ValueError("SMTP config incomplete in environment variables.") + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = to_email + msg.set_content(body) + + if attachment_path and os.path.isfile(attachment_path): + with open(attachment_path, "rb") as f: + file_data = f.read() + file_name = os.path.basename(attachment_path) + msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name) + else: + raise FileNotFoundError(f"Attachment file not found: {attachment_path}") + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg) + + +# Send email using Gmail directly +def send_gmail(to_email, subject, body): + smtp_host = "smtp.gmail.com" + smtp_port = 587 + smtp_user = 'macamete.robert@gmail.com' + smtp_pass = 'advx yqlv jkaa czvr' + sender_email = 'macamete.robert@gmail.com' + + if not all([smtp_user, smtp_pass]): + raise ValueError("GMAIL_USER and GMAIL_PASS must be set in environment variables.") + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = to_email + msg.set_content(body) + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg) + + +# Send email with attachment using Gmail directly +def send_gmail_with_attachment(to_email, subject, body, attachment_path): + smtp_host = "smtp.gmail.com" + smtp_port = 587 + smtp_user = '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/server/utils/welcome_email.py b/server/utils/welcome_email.py new file mode 100644 index 0000000..b0728ad --- /dev/null +++ b/server/utils/welcome_email.py @@ -0,0 +1,61 @@ +import os +import logging +from utils.email import send_gmail_with_attachment + + +class WelcomeMessage: + """ + Sends a welcome email with an optional attached user manual (PDF). + + - Looks for the manual in SERVER_ASSETS_DIR (env) or defaults to ../assets next to this file. + - Manual filename can be customized with WELCOME_MANUAL_FILENAME (env), default: manual.pdf. + """ + + def __init__(self, email: str): + self.email = email + self.subject = "Bine ati venit!" + + # Allow overriding assets folder and manual filename via env for containerized deployments. + default_assets = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "assets")) + self.assets_folder = os.getenv("SERVER_ASSETS_DIR", default_assets) + self.manual_filename = os.getenv("WELCOME_MANUAL_FILENAME", "Manual.pdf") + self.manual = os.path.join(self.assets_folder, self.manual_filename) + + logging.info("WelcomeMessage assets dir: %s", self.assets_folder) + logging.info("WelcomeMessage manual path: %s", self.manual) + + self.body = f""" + + Stimate/ă domn/ă, + +Ne face plăcere să vă urăm bun venit la JuridicBloc. Vă mulțumim că ați ales platforma noastră pentru a vă susține nevoile administratiei. + +Pentru a vă ajuta să începeți, am atașat Manualul de utilizare la acest e-mail. Acesta oferă instrucțiuni pas cu pas privind configurarea contului, o prezentare generală a funcțiilor și cele mai bune practici pentru utilizarea eficientă a aplicatiei JuridicBloc. + +Vă recomandăm să consultați manualul atunci când vă este convenabil pentru a vă familiariza cu capacitățile sistemului. Dacă aveți nevoie de asistență suplimentară, echipa noastră de asistență este disponibilă la adresa support@juridicbloc.ro. + +Așteptăm cu nerăbdare să vă susținem succesul și să construim un parteneriat pe termen lung. + +Cu sinceritate, +Echipa JuridicBloc + + """ + + def send_email(self): + # If the manual exists, send with attachment; otherwise, log and send nothing (or integrate a no-attachment sender later). + if os.path.isfile(self.manual): + send_gmail_with_attachment( + to_email=self.email, + subject=self.subject, + body=self.body, + attachment_path=self.manual, + ) + else: + logging.warning( + f"Welcome manual missing, skipping attachment. Looked at: {self.manual} " + f"(set SERVER_ASSETS_DIR or WELCOME_MANUAL_FILENAME to adjust). " + f"Current working directory: {os.getcwd()}" + ) + # If you later implement send_gmail() without attachment, call it here. + # from utils.email import send_gmail + # send_gmail(to_email=self.email, subject=self.subject, body=self.body)