first commit
This commit is contained in:
BIN
client/pages/documents/__pycache__/ba.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/ba.cpython-313.pyc
Normal file
Binary file not shown.
BIN
client/pages/documents/__pycache__/client.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/client.cpython-313.pyc
Normal file
Binary file not shown.
BIN
client/pages/documents/__pycache__/custom.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/custom.cpython-313.pyc
Normal file
Binary file not shown.
BIN
client/pages/documents/__pycache__/documents.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/documents.cpython-313.pyc
Normal file
Binary file not shown.
BIN
client/pages/documents/__pycache__/home.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/home.cpython-313.pyc
Normal file
Binary file not shown.
BIN
client/pages/documents/__pycache__/standard.cpython-313.pyc
Normal file
BIN
client/pages/documents/__pycache__/standard.cpython-313.pyc
Normal file
Binary file not shown.
374
client/pages/documents/ba.py
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
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
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
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
|
||||
)
|
||||
Reference in New Issue
Block a user