import os import shutil from flask import Blueprint, request, jsonify, send_from_directory from flask_jwt_extended import jwt_required, get_jwt_identity from models.documents.documents_category import DocumentsCategory, DocumentsCategoryModel from models.documents.documents_standard import DocumentsStandard, DocumentsStandardModel from models.documents.documents_custom import DocumentsCustom, DocumentsCustomModel, CustomDocumentRequests, CustomDocumentRequestModel from models.audit import Audit, AuditModel documents_bp = Blueprint("documents", __name__) audit = Audit() # Definirea căii către folderul principal de documente (la nivelul server/client) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) DOCUMENTS_ROOT = os.path.join(BASE_DIR, "documents") os.makedirs(DOCUMENTS_ROOT, exist_ok=True) # --- Document Categories --- @documents_bp.route("/categories/add", methods=["POST"]) @jwt_required() def add_category(): user_id = get_jwt_identity() data = request.get_json() name = data.get("name") access = data.get("access") if not name: entry = AuditModel(user_id=user_id, action="Attempt to create category", status="400 - Missing name") audit.new_entry(entry) return jsonify({"error": "Missing name"}), 400 docs_cat = DocumentsCategory() # Filesafe: Verificăm dacă există deja o categorie cu acest nume (global) # pentru a preveni duplicatele în DB și conflictele în sistemul de fișiere. existing_categories = docs_cat.get_all_entries() for cat in existing_categories: if cat.name and cat.name.lower() == name.lower(): return jsonify({"message": "Category already exists", "id": cat.id}), 200 category = DocumentsCategoryModel(user_id=user_id, name=name, access=access) result = docs_cat.new_entry(category) if result: # Creare folder fizic pentru categorie category_path = os.path.join(DOCUMENTS_ROOT, name) os.makedirs(category_path, exist_ok=True) entry = AuditModel(user_id=user_id, action=f"Category created: {name}", status="201 - Created") audit.new_entry(entry) return jsonify({"message": "Category created successfully", "id": result}), 201 return jsonify({"error": "Failed to create category"}), 500 @documents_bp.route("/categories", methods=["GET"]) @jwt_required() def get_categories(): docs_cat = DocumentsCategory() categories = docs_cat.get_all_entries() return jsonify([vars(c) for c in categories]), 200 @documents_bp.route("/categories/update/", methods=["PUT"]) @jwt_required() def update_category(id): user_id = get_jwt_identity() data = request.get_json() name = data.get("name") access = data.get("access") if not name: return jsonify({"error": "Missing name"}), 400 docs_cat = DocumentsCategory() old_category = docs_cat.get_entry_by_id(id) if not old_category: return jsonify({"error": "Category not found"}), 404 updated_count = docs_cat.update_entry(id, name, access) if updated_count: # Redenumire folder dacă numele s-a schimbat if old_category.name != name: old_path = os.path.join(DOCUMENTS_ROOT, old_category.name) new_path = os.path.join(DOCUMENTS_ROOT, name) if os.path.exists(old_path): os.rename(old_path, new_path) else: os.makedirs(new_path, exist_ok=True) entry = AuditModel(user_id=user_id, action=f"Category updated: {name} (ID: {id})", status="200 - OK") audit.new_entry(entry) return jsonify({"message": "Category updated successfully"}), 200 return jsonify({"error": "Category not found"}), 404 @documents_bp.route("/categories/delete/", methods=["DELETE"]) @jwt_required() def delete_category(id): user_id = get_jwt_identity() docs_cat = DocumentsCategory() # Preluăm categoria înainte de ștergere pentru a cunoaște numele folderului category = docs_cat.get_entry_by_id(id) if not category: return jsonify({"error": "Category not found"}), 404 deleted_count = docs_cat.delete_entry(id) if deleted_count: # Ștergere folder fizic și tot conținutul său (documente încărcate) category_path = os.path.join(DOCUMENTS_ROOT, category.name) if os.path.exists(category_path): shutil.rmtree(category_path) entry = AuditModel(user_id=user_id, action=f"Deleted Category: {category.name} (ID: {id})", status="200 - OK") audit.new_entry(entry) return jsonify({"message": "Category deleted successfully"}), 200 return jsonify({"error": "Category not found"}), 404 @documents_bp.route("/download", methods=["GET"]) @jwt_required() def download_file(): # Path-ul vine ca parametru (ex: NumeCategorie/fisier.pdf) file_path = request.args.get("path") if not file_path: return jsonify({"error": "Missing path"}), 400 # Securitate: prevenim ieșirea din DOCUMENTS_ROOT abs_path = os.path.abspath(os.path.join(DOCUMENTS_ROOT, file_path)) if not abs_path.startswith(os.path.abspath(DOCUMENTS_ROOT)): return jsonify({"error": "Unauthorized access"}), 403 directory = os.path.dirname(abs_path) filename = os.path.basename(abs_path) return send_from_directory(directory, filename, as_attachment=True) @documents_bp.route("/categories/refresh/", methods=["POST"]) @jwt_required() def refresh_category_files(category_id): user_id = get_jwt_identity() docs_cat = DocumentsCategory() docs_std = DocumentsStandard() category = docs_cat.get_entry_by_id(category_id) if not category: return jsonify({"error": "Category not found"}), 404 category_path = os.path.join(DOCUMENTS_ROOT, category.name) if not os.path.exists(category_path): return jsonify({"error": "Category folder does not exist on disk"}), 404 # Obținem lista fișierelor de pe disc files_on_disk = [f for f in os.listdir(category_path) if os.path.isfile(os.path.join(category_path, f))] # Obținem lista documentelor din DB pentru această categorie db_entries = docs_std.get_entries_by_category(category_id) db_file_paths = {entry.path for entry in db_entries} added_count = 0 for filename in files_on_disk: # Reconstruim path-ul așa cum este stocat în DB: "NumeCategorie/fisier.ext" relative_path = f"{category.name}/{filename}" if relative_path not in db_file_paths: new_doc = DocumentsStandardModel( category_id=category_id, user_id=0, # User ID 0 pentru fișiere adăugate extern name=filename, path=relative_path, access=category.access ) if docs_std.new_entry(new_doc): added_count += 1 if added_count > 0: audit.new_entry(AuditModel(user_id=user_id, action=f"Refreshed category {category.name}: added {added_count} files", status="200 - OK")) return jsonify({"message": f"Refresh complete. Added {added_count} new documents.", "added": added_count}), 200 # --- Document Standards Category --- @documents_bp.route("/standards/category/", methods=["GET"]) @jwt_required() def get_standards_by_category(category_id): docs_std = DocumentsStandard() items = docs_std.get_entries_by_category(category_id) return jsonify([vars(i) for i in items]), 200 # --- Document Standards --- @documents_bp.route("/standards/add", methods=["POST"]) @jwt_required() def add_standard(): user_id = get_jwt_identity() data = request.get_json() name = data.get("name") path = data.get("path") access = data.get("access") category_id = data.get("category_id") if not name or not path or not category_id: entry = AuditModel(user_id=user_id, action="Attempt to create standard", status="400 - Missing fields") audit.new_entry(entry) return jsonify({"error": "Missing name, path or category_id"}), 400 # Mutare fizică a fișierului din folderul de upload în folderul categoriei # Sursa este folderul unde Flet salvează implicit upload-urile source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) dest_path = os.path.join(DOCUMENTS_ROOT, path) print(source_path) print(dest_path) if os.path.exists(source_path): os.makedirs(os.path.dirname(dest_path), exist_ok=True) shutil.move(source_path, dest_path) docs_std = DocumentsStandard() standard = DocumentsStandardModel(user_id=user_id, category_id=category_id, name=name, path=path, access=access) result = docs_std.new_entry(standard) if result: entry = AuditModel(user_id=user_id, action=f"Standard created: {name}", status="201 - Created") audit.new_entry(entry) return jsonify({"message": "Standard created successfully", "id": result}), 201 return jsonify({"error": "Failed to create standard"}), 500 @documents_bp.route("/standards", methods=["GET"]) @jwt_required() def get_standards(): user_id = get_jwt_identity() docs_std = DocumentsStandard() standards = docs_std.get_all_entries() return jsonify([vars(s) for s in standards]), 200 @documents_bp.route("/standards/delete/", methods=["DELETE"]) @jwt_required() def delete_standard(id): user_id = get_jwt_identity() docs_std = DocumentsStandard() # Preluăm documentul înainte de ștergere pentru a cunoaște calea fișierului standard = docs_std.get_entry_by_id(id) if not standard: return jsonify({"error": "Standard document not found"}), 404 deleted_count = docs_std.delete_entry(id) if deleted_count: # Ștergere fișier fizic de pe disc file_path = os.path.join(DOCUMENTS_ROOT, standard.path) if os.path.exists(file_path): try: os.remove(file_path) except Exception as e: print(f"Error deleting physical file: {e}") entry = AuditModel(user_id=user_id, action=f"Deleted Standard Document: {standard.name} (ID: {id})", status="200 - OK") audit.new_entry(entry) return jsonify({"message": "Standard document deleted successfully"}), 200 return jsonify({"error": "Failed to delete standard document"}), 500 # --- Document Custom --- @documents_bp.route("/customs/add", methods=["POST"]) @jwt_required() def add_custom(): user_id = get_jwt_identity() data = request.get_json() name = data.get("name", "").strip() path = data.get("path", "").strip() access = data.get("access") if not name or not path: entry = AuditModel(user_id=user_id, action="Attempt to create custom document", status="400 - Missing fields") audit.new_entry(entry) return jsonify({"error": "Missing name or path"}), 400 # Physical file move from upload folder to documents/Custom folder filename = os.path.basename(path) # The 'path' variable here is the filename from the client, not the full path. # We construct the full path for the database entry later. source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) dest_dir = os.path.normpath(os.path.join(DOCUMENTS_ROOT, "Custom")) dest_path = os.path.join(dest_dir, filename) # This is the actual destination path for shutil.move print(source_path) print(dest_path) if os.path.exists(source_path): os.makedirs(dest_dir, exist_ok=True) # Ensure the destination directory exists shutil.move(source_path, dest_path) # Update the 'path' variable to be stored in the database to reflect its new location db_path = f"Custom/{filename}" else: # Log an error and return a response if the source file doesn't exist error_message = f"Source file not found for custom document: {source_path}" print(f"ERROR: {error_message}") # For immediate debug visibility entry = AuditModel(user_id=user_id, action=f"Failed to create custom document (file not found): {name}", status="404 - File not found") audit.new_entry(entry) return jsonify({"error": "Uploaded file not found on server."}), 404 docs_custom = DocumentsCustom() custom = DocumentsCustomModel(user_id=user_id, name=name, path=db_path, access=access) # Use db_path here result = docs_custom.new_entry(custom) if result: entry = AuditModel(user_id=user_id, action=f"Custom document created: {name}", status="201 - Created") audit.new_entry(entry) return jsonify({"message": "Custom document created successfully", "id": result}), 201 # If we reach here, it means the DB entry failed after the file was moved. # This is a potential issue, as the file is moved but not recorded. entry = AuditModel(user_id=user_id, action=f"Failed to create custom document (DB entry failed): {name}", status="500 - DB error") audit.new_entry(entry) return jsonify({"error": "Failed to create custom document"}), 500 @documents_bp.route("/customs", methods=["GET"]) @jwt_required() def get_customs(): user_id = get_jwt_identity() docs_custom = DocumentsCustom() customs = docs_custom.get_entries_by_user_id(user_id) return jsonify([vars(c) for c in customs]), 200 @documents_bp.route("/customs/", methods=["GET"]) @jwt_required() def get_custom_by_id(id): docs_custom = DocumentsCustom() document = docs_custom.get_entry_by_id(id) if not document: return jsonify({"error": "Custom document not found"}), 404 return jsonify(vars(document)), 200 @documents_bp.route("/customs/delete/", methods=["DELETE"]) @jwt_required() def delete_custom(id): user_id = get_jwt_identity() docs_custom = DocumentsCustom() deleted_count = docs_custom.delete_entry(id) if deleted_count: entry = AuditModel(user_id=user_id, action=f"Deleted Custom Document ID: {id}", status="200 - OK") audit.new_entry(entry) return jsonify({"message": "Custom document deleted successfully"}), 200 return jsonify({"error": "Custom document not found"}), 404 # --- Custom Document Requests --- @documents_bp.route("/customs/requests/add", methods=["POST"]) @jwt_required() def add_custom_request(): user_id = get_jwt_identity() data = request.get_json() request_text = data.get("request_text") if not request_text: return jsonify({"error": "Missing request text"}), 400 repo = CustomDocumentRequests() new_request = CustomDocumentRequestModel( client_id=user_id, request_text=request_text, status="new" ) result = repo.new_entry(new_request) if result: audit.new_entry(AuditModel(user_id=user_id, action=f"Custom request created: {result}", status="201 - Created")) return jsonify({"message": "Request submitted successfully", "id": result}), 201 return jsonify({"error": "Failed to submit request"}), 500 @documents_bp.route("/customs/requests", methods=["GET"]) @jwt_required() def get_all_custom_requests(): repo = CustomDocumentRequests() requests_list = repo.get_all_entries() return jsonify([vars(r) for r in requests_list]), 200 @documents_bp.route("/customs/requests/", methods=["GET"]) @jwt_required() def get_custom_request_by_id(id): repo = CustomDocumentRequests() req = repo.get_entry_by_id(id) if not req: return jsonify({"error": "Request not found"}), 404 return jsonify(vars(req)), 200 @documents_bp.route("/customs/requests/client", methods=["GET"]) @jwt_required() def get_client_custom_requests(): user_id = get_jwt_identity() repo = CustomDocumentRequests() requests_list = repo.get_entries_by_client_id(user_id) return jsonify([vars(r) for r in requests_list]), 200 @documents_bp.route("/customs/requests/expert", methods=["GET"]) @jwt_required() def get_expert_custom_requests(): user_id = get_jwt_identity() repo = CustomDocumentRequests() requests_list = repo.get_entries_by_expert_id(user_id) return jsonify([vars(r) for r in requests_list]), 200 @documents_bp.route("/customs/requests/update/", methods=["PUT"]) @jwt_required() def update_custom_request(id): user_id = get_jwt_identity() data = request.get_json() repo = CustomDocumentRequests() success = repo.update_entry( id=id, request_text=data.get("request_text"), status=data.get("status"), price=data.get("price"), expert_id=data.get("expert_id"), document_id=data.get("document_id") ) if success: audit.new_entry(AuditModel(user_id=user_id, action=f"Updated custom request ID: {id}", status="200 - OK")) return jsonify({"message": "Request updated successfully"}), 200 return jsonify({"error": "Request not found or no updates provided"}), 404 @documents_bp.route("/customs/requests/delete/", methods=["DELETE"]) @jwt_required() def delete_custom_request(id): user_id = get_jwt_identity() repo = CustomDocumentRequests() if repo.delete_entry(id): audit.new_entry(AuditModel(user_id=user_id, action=f"Deleted custom request ID: {id}", status="200 - OK")) return jsonify({"message": "Request deleted successfully"}), 200 return jsonify({"error": "Request not found"}), 404