first commit
BIN
client/.DS_Store
vendored
Normal file
BIN
client/assets/.DS_Store
vendored
Normal file
BIN
client/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
client/assets/icons/.DS_Store
vendored
Normal file
BIN
client/assets/icons/loading-animation.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
client/assets/images/.DS_Store
vendored
Normal file
BIN
client/assets/images/eng.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
client/assets/images/logo_juridic_block.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
client/assets/images/rom.png
Normal file
|
After Width: | Height: | Size: 88 B |
BIN
client/assets/uploads/20260613183323_panel.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 64 KiB |
BIN
client/assets/uploads/20260613190652_taina_gustuli_brown_bg.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
client/assets/uploads/20260613190736_beef cow_0.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 22 KiB |
BIN
client/assets/uploads/20260613191751_1.PNG
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
client/assets/uploads/20260613205856_taina_gustuli_brown_bg.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
client/assets/uploads/20260613210201_taina_gustuli_brown_bg.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
client/helpers/__pycache__/document_status.cpython-313.pyc
Normal file
BIN
client/helpers/__pycache__/emails.cpython-313.pyc
Normal file
BIN
client/helpers/__pycache__/payment_type.cpython-313.pyc
Normal file
BIN
client/helpers/__pycache__/roles.cpython-313.pyc
Normal file
BIN
client/helpers/__pycache__/translate.cpython-313.pyc
Normal file
BIN
client/helpers/__pycache__/validations.cpython-313.pyc
Normal file
24
client/helpers/document_status.py
Normal file
@@ -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)
|
||||||
|
|
||||||
139
client/helpers/emails.py
Normal file
@@ -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)
|
||||||
6
client/helpers/payment_type.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PaymentType:
|
||||||
|
ONE_TIME_ONLY = 'o singura data'
|
||||||
|
SUBSCRIPTION = 'abonament'
|
||||||
25
client/helpers/roles.py
Normal file
@@ -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"
|
||||||
66
client/helpers/validations.py
Normal file
@@ -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
|
||||||
88
client/main.py
Normal file
@@ -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"
|
||||||
|
)
|
||||||
BIN
client/models/chats/__pycache__/conversation.cpython-313.pyc
Normal file
BIN
client/models/chats/__pycache__/message.cpython-313.pyc
Normal file
BIN
client/models/chats/__pycache__/participant.cpython-313.pyc
Normal file
49
client/models/chats/conversation.py
Normal file
@@ -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,))
|
||||||
57
client/models/chats/message.py
Normal file
@@ -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,))
|
||||||
49
client/models/chats/participant.py
Normal file
@@ -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)
|
||||||
|
)
|
||||||
BIN
client/navigation/__pycache__/ba.cpython-313.pyc
Normal file
BIN
client/navigation/__pycache__/user.cpython-313.pyc
Normal file
96
client/navigation/ba.py
Normal file
@@ -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')
|
||||||
93
client/navigation/user.py
Normal file
@@ -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')
|
||||||
BIN
client/pages/.DS_Store
vendored
Normal file
BIN
client/pages/auth/__pycache__/auth.cpython-313.pyc
Normal file
BIN
client/pages/auth/__pycache__/forgot_password.cpython-313.pyc
Normal file
BIN
client/pages/auth/__pycache__/login.cpython-313.pyc
Normal file
BIN
client/pages/auth/__pycache__/register.cpython-313.pyc
Normal file
BIN
client/pages/auth/__pycache__/verify_code.cpython-313.pyc
Normal file
62
client/pages/auth/auth.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
116
client/pages/auth/forgot_password.py
Normal file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
85
client/pages/auth/login.py
Normal file
@@ -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,
|
||||||
|
)
|
||||||
81
client/pages/auth/register.py
Normal file
@@ -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,
|
||||||
|
)
|
||||||
60
client/pages/auth/verify_code.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
BIN
client/pages/dashboard/.DS_Store
vendored
Normal file
BIN
client/pages/dashboard/__pycache__/home.cpython-313.pyc
Normal file
61
client/pages/dashboard/home.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
BIN
client/pages/documents/__pycache__/ba.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/client.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/custom.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/documents.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/home.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/standard.cpython-313.pyc
Normal file
374
client/pages/documents/ba.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
350
client/pages/documents/custom.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
97
client/pages/documents/home.py
Normal file
@@ -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,
|
||||||
|
)
|
||||||
180
client/pages/documents/standard.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
BIN
client/pages/settings/__pycache__/settings.cpython-313.pyc
Normal file
BIN
client/pages/settings/__pycache__/users.cpython-313.pyc
Normal file
595
client/pages/settings/documente_juridice.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
351
client/pages/settings/payment_and_subscription.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
77
client/pages/settings/settings.py
Normal file
@@ -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
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
256
client/pages/settings/users.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
14
client/pages/subscriptions/subscriptions.py
Normal file
@@ -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(
|
||||||
|
[
|
||||||
|
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
0
commands.txt
Normal file
BIN
documents/.DS_Store
vendored
Normal file
BIN
documents/Tipizate ANAF/3 Anunt convocator avizier pasul 3.doc
Normal file
BIN
documents/Tipizate ANAF/4 Tabel convocator pasul 4.doc
Normal file
BIN
documents/Tipizate ANAF/6 Tabel prezenta convocare pasul 6.doc
Normal file
BIN
documents/Tipizate ANAF/8 Tabel prezenta reconvocare pasul 8.doc
Normal file
BIN
documents/Tipizate ANAF/card de fidelitate 2.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
documents/Tipizate ANAF/card de fidelitate 3.pdf
Normal file
BIN
documents/Tipizate ANAF/card de fidelizare.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
documents/test/20260519172336_bacanie2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
documents/test/20260519172432_bacanie3.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
documents/test2/20260519172449_bacanie.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 500 KiB |
|
After Width: | Height: | Size: 135 KiB |
0
requirements.txt
Normal file
BIN
server/.DS_Store
vendored
Normal file
58
server/app.py
Normal file
@@ -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()
|
||||||