422 lines
18 KiB
Python
422 lines
18 KiB
Python
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
|
|
import asyncio
|
|
|
|
@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.documenet_title = 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.download_button = ft.IconButton(
|
|
icon=ft.Icons.DOWNLOAD,
|
|
on_click=self._on_download_button_click,
|
|
)
|
|
|
|
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.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,
|
|
self.download_button
|
|
]),
|
|
ft.FilledButton("Salveaza si trimite", on_click=self._on_file_saved)
|
|
],
|
|
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
|
|
)
|
|
],
|
|
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: 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.doc_id_info.value = f"ID Document asociat: {self.documenet_title}"
|
|
return f'{uploaded_file_name}'
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
def _on_file_saved(self, e):
|
|
|
|
reg_resp = requests.post(
|
|
f"{self.base_url}/documents/customs/add",
|
|
json={"name": f"Document Final Solicitare #{self.current_selected_request['id']}", "path": self.documenet_title},
|
|
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,
|
|
})
|
|
|
|
async def _on_download_button_click(self, e):
|
|
"""Handles the download of the final document."""
|
|
if not self.current_selected_request or not self.current_selected_request.get('document_id'):
|
|
self.page.show_dialog(ft.SnackBar(ft.Text("Nu există un document final asociat acestei solicitări."), open=True))
|
|
self.page.update()
|
|
return
|
|
|
|
document_id = self.current_selected_request['document_id']
|
|
try:
|
|
# Fetch document details to get the path
|
|
response = requests.get(
|
|
f"{self.base_url}/documents/customs/{document_id}",
|
|
headers={'Authorization': f'Bearer {self.token}'}
|
|
)
|
|
if response.status_code == 200:
|
|
document_data = response.json()
|
|
document_path = document_data.get('path')
|
|
if document_path:
|
|
download_url = f"{self.base_url}/documents/download?path={document_path}&token={self.token}"
|
|
await self.page.launch_url(download_url)
|
|
else:
|
|
self.page.show_dialog(ft.SnackBar(ft.Text("Calea documentului nu a putut fi găsită."), open=True))
|
|
else:
|
|
self.page.show_dialog(ft.SnackBar(ft.Text(f"Eroare la preluarea detaliilor documentului: {response.status_code}"), open=True))
|
|
except requests.exceptions.RequestException as err:
|
|
self.page.show_dialog(ft.SnackBar(ft.Text(f"Eroare de rețea la descărcarea documentului: {err}"), open=True))
|
|
except Exception as ex:
|
|
self.page.show_dialog(ft.SnackBar(ft.Text(f"A apărut o eroare neașteptată: {ex}"), open=True))
|
|
self.page.update()
|
|
|
|
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
|
|
) |