From bb564809ea4514b4464a62bb27feb55e34660a86 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Tue, 8 Jul 2025 09:55:25 -0500 Subject: [PATCH] Implement work log creation and update API; add new worklog entry route and enhance worklog template with JavaScript functionality --- models/work_log.py | 20 ++++++- routes/helpers.py | 11 ---- routes/worklog.py | 87 +++++++++++++++++++++++++++-- templates/inventory.html | 6 +- templates/worklog.html | 118 ++++++++++++++++++++++++++++++++++----- 5 files changed, 209 insertions(+), 33 deletions(-) diff --git a/models/work_log.py b/models/work_log.py index c45ba6c..72de3c5 100644 --- a/models/work_log.py +++ b/models/work_log.py @@ -1,4 +1,4 @@ -from typing import Optional, TYPE_CHECKING +from typing import Optional, Any, TYPE_CHECKING if TYPE_CHECKING: from .inventory import Inventory from .users import User @@ -57,4 +57,20 @@ class WorkLog(db.Model): 'contact_id': self.contact_id, 'analysis': self.analysis, 'work_item_id': self.work_item_id - } \ No newline at end of file + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "WorkLog": + start_time_str = data.get("start_time") + end_time_str = data.get("end_time") + + return cls( + start_time=datetime.datetime.fromisoformat(str(start_time_str)) if start_time_str else datetime.datetime.now(), + end_time=datetime.datetime.fromisoformat(str(end_time_str)) if end_time_str else None, + notes=data.get("notes"), + complete=bool(data.get("complete", False)), + followup=bool(data.get("followup", False)), + analysis=bool(data.get("analysis", False)), + contact_id=data.get("contact_id"), + work_item_id=data.get("work_item_id") + ) \ No newline at end of file diff --git a/routes/helpers.py b/routes/helpers.py index 5139f20..90c8fe9 100644 --- a/routes/helpers.py +++ b/routes/helpers.py @@ -61,16 +61,5 @@ worklog_headers = { "Quick Analysis?": lambda i: {"text": i.analysis, "type": "bool", "html": checked_box if i.analysis else unchecked_box}, } -worklog_form_fields = { - "start": lambda log: {"label": "Start Timestamp", "type": "date", "value": log.start_time.date().isoformat() if log.start_time else ""}, - "end": lambda log: {"label": "End Timestamp", "type": "date", "value": log.end_time.date().isoformat() if log.end_time else ""}, - "contact": lambda log: {"label": "Contact", "type": "datalist", "value": log.contact.full_name if log.contact else "", "list": "contactList"}, - "item": lambda log: {"label": "Work Item", "type": "datalist", "value": log.work_item.identifier if log.work_item else "", "list": "itemList"}, - "complete": lambda log: {"label": "Complete?", "type": "checkbox", "value": log.complete}, - "followup": lambda log: {"label": "Follow Up?", "type": "checkbox", "value": log.followup}, - "analysis": lambda log: {"label": "Quick Analysis?", "type": "checkbox", "value": log.analysis}, - "notes": lambda log: {"label": "Notes", "type": "textarea", "value": log.notes or "", "rows": 15} -} - def link(text, endpoint, **values): return {"text": text, "url": url_for(endpoint, **values)} diff --git a/routes/worklog.py b/routes/worklog.py index 1eb4c46..dab39af 100644 --- a/routes/worklog.py +++ b/routes/worklog.py @@ -1,7 +1,9 @@ -from flask import request, render_template +import datetime + +from flask import request, render_template, jsonify from . import main -from .helpers import worklog_headers, worklog_form_fields +from .helpers import worklog_headers from .. import db from ..models import WorkLog, User, Inventory from ..utils.load import eager_load_worklog_relationships, eager_load_user_relationships, eager_load_inventory_relationships @@ -46,6 +48,83 @@ def worklog_entry(id): title=title, log=log, users=users, - items=items, - form_fields=worklog_form_fields + items=items ) + +@main.route("/worklog_entry/new", methods=["GET"]) +def new_worklog(): + items = eager_load_inventory_relationships(db.session.query(Inventory)).all() + users = eager_load_user_relationships(db.session.query(User)).all() + + log = WorkLog( + start_time=datetime.datetime.now(), + followup=True + ) + + return render_template( + "worklog.html", + title="New Entry", + log=log, + users=users, + items=items + ) + +@main.route("/api/worklog", methods=["POST"]) +def create_worklog(): + try: + data = request.get_json(force=True) + + new_worklog = WorkLog.from_dict(data) + + db.session.add(new_worklog) + db.session.commit() + + return jsonify({"success": True, "id": new_worklog.id}), 201 + + except Exception as e: + db.session.rollback() + return jsonify({"success": False, "error": str(e)}), 400 + +@main.route("/api/worklog/", methods=["PUT"]) +def update_worklog(id): + try: + data = request.get_json(force=True) + print(data) + log = db.session.query(WorkLog).get(id) + + if not log: + return jsonify({"success": False, "error": f"Work Log with ID {id} not found."}), 404 + + log.start_time = datetime.datetime.fromisoformat(data.get("start_time")) if data.get("start_time") else log.start_time + log.end_time = datetime.datetime.fromisoformat(data.get("end_time")) if data.get("end_time") else log.end_time + log.notes = data.get("notes", log.notes) + log.complete = bool(data.get("complete", log.complete)) + log.followup = bool(data.get("followup", log.followup)) + log.analysis = bool(data.get("analysis", log.analysis)) + log.contact_id = data.get("contact_id", log.contact_id) + log.work_item_id = data.get("work_item_id", log.work_item_id) + + db.session.commit() + + return jsonify({"success": True, "id": log.id}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"success": False, "error": str(e)}), 400 + +@main.route("/api/worklog/", methods=["DELETE"]) +def delete_worklog(id): + try: + log = db.session.query(WorkLog).get(id) + + if not log: + return jsonify({"success": False, "errpr": f"Item with ID {id} not found!"}), 404 + + db.session.delete(log) + db.session.commit() + + return jsonify({"success": True}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"success": False, "error": str(e)}), 400 diff --git a/templates/inventory.html b/templates/inventory.html index e09b90e..74baa89 100644 --- a/templates/inventory.html +++ b/templates/inventory.html @@ -181,6 +181,7 @@ } } catch (err) { console.error(err); + renderToast({ message: `Error: ${err}`, type: "danger" }); } }); } @@ -190,7 +191,7 @@ const id = document.querySelector("#inventoryId").value; if (!id || id === "None") { - renderToast({ message: "No item ID found to delete." }); + renderToast({ message: "No item ID found to delete.", type: "danger }"); return; } @@ -216,7 +217,8 @@ renderToast({ message: `Error: ${result.error}`, type: "danger" }); } } catch (err) { - renderToast({ message: `Error: ${result.error}`, type: "danger" }); + console.error(err); + renderToast({ message: `Error: ${err}`, type: "danger" }); } }); } diff --git a/templates/worklog.html b/templates/worklog.html index 41e03a6..e8b8cd9 100644 --- a/templates/worklog.html +++ b/templates/worklog.html @@ -5,15 +5,16 @@ {% block content %} - +{% endblock %} + +{% block script %} + const saveButton = document.getElementById("saveButton"); + const deleteButton = document.getElementById("deleteButton"); + + if (saveButton) { + saveButton.addEventListener("click", async (e) => { + e.preventDefault(); + + const payload = { + start_time: document.querySelector("input[name='start']").value, + end_time: document.querySelector("input[name='end']").value, + notes: document.querySelector("textarea[name='notes']").value, + complete: document.querySelector("input[name='complete']").checked, + analysis: document.querySelector("input[name='analysis']").checked, + followup: document.querySelector("input[name='followup']").checked, + contact_id: document.querySelector("select[name='contact']").value || null, + work_item_id: document.querySelector("select[name='item']").value || null, + }; + + try { + const id = document.querySelector("#logId").value; + const isEdit = id && id !== "None"; + + const endpoint = isEdit ? `/api/worklog/${id}` : "/api/worklog"; + const method = isEdit ? "PUT" : "POST"; + + const response = await fetch(endpoint, { + method, + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (result.success) { + localStorage.setItem("toastMessage", JSON.stringify({ + message: isEdit ? "Work Log entry updated!" : "Work Log entry created!", + type: "success" + })); + + window.location.href = `/worklog/${result.id}`; + } else { + renderToast({ message: `Error: ${result.error}`, type: "danger" }); + } + } catch (err) { + console.error(err) + renderToast({ message: `Error: ${err}`, type: "danger" }); + } + }); + } + + if (deleteButton) { + deleteButton.addEventListener("click", async () => { + const id = document.querySelector("#logId").value; + + if (!id || id === "None") { + renderToast({ message: "No item ID found to delete.", type: "danger" }); + return; + } + + if (!confirm("Are you sure you want to delete this work log entry? This action cannot be undone.")) { + return; + } + + try { + const response = await fetch(`/api/worklog/${id}`, { + method: "DELETE" + }); + + const result = await response.json(); + + if (result.success) { + localStorage.setItem("toastMessage", JSON.stringify({ + message: "Work log entry deleted.", + type: "success" + })); + + window.location.href = "/worklog"; + } else { + renderToast({ message: `Error: ${result.error}`, type: "danger" }); + } + } catch (err) { + console.log(err); + renderToast({ message: `Error: ${err}`, type: "danger" }); + } + }); + } {% endblock %} \ No newline at end of file