Add WorkNote model and integrate updates into WorkLog functionality
This commit is contained in:
parent
58f8a040b7
commit
39a8e64c90
6 changed files with 123 additions and 18 deletions
|
@ -10,6 +10,7 @@ from .room_functions import RoomFunction
|
||||||
from .users import User
|
from .users import User
|
||||||
from .work_log import WorkLog
|
from .work_log import WorkLog
|
||||||
from .rooms import Room
|
from .rooms import Room
|
||||||
|
from .work_note import WorkNote
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"db",
|
"db",
|
||||||
|
@ -21,4 +22,5 @@ __all__ = [
|
||||||
"User",
|
"User",
|
||||||
"WorkLog",
|
"WorkLog",
|
||||||
"Room",
|
"Room",
|
||||||
|
"WorkNote"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
from typing import Optional, Any, TYPE_CHECKING
|
from typing import Optional, Any, List, TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .inventory import Inventory
|
from .inventory import Inventory
|
||||||
from .users import User
|
from .users import User
|
||||||
|
from .work_note import WorkNote
|
||||||
|
|
||||||
from sqlalchemy import Boolean, ForeignKeyConstraint, Identity, Integer, ForeignKey, Unicode, DateTime, text
|
from sqlalchemy import Boolean, Identity, Integer, ForeignKey, Unicode, DateTime, text
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
|
from .work_note import WorkNote
|
||||||
|
|
||||||
class WorkLog(db.Model):
|
class WorkLog(db.Model):
|
||||||
__tablename__ = 'work_log'
|
__tablename__ = 'work_log'
|
||||||
|
@ -24,11 +26,25 @@ class WorkLog(db.Model):
|
||||||
|
|
||||||
work_item: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='work_logs')
|
work_item: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='work_logs')
|
||||||
contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs')
|
contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs')
|
||||||
|
updates: Mapped[list['WorkNote']] = relationship(
|
||||||
|
'WorkNote',
|
||||||
|
back_populates='work_log',
|
||||||
|
cascade='all, delete-orphan',
|
||||||
|
order_by='WorkNote.timestamp'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, start_time: Optional[datetime.datetime] = None, end_time: Optional[datetime.datetime] = None,
|
def __init__(
|
||||||
notes: Optional[str] = None, complete: Optional[bool] = False,
|
self,
|
||||||
followup: Optional[bool] = False, contact_id: Optional[int] = None,
|
start_time: Optional[datetime.datetime] = None,
|
||||||
analysis: Optional[bool] = False, work_item_id: Optional[int] = None):
|
end_time: Optional[datetime.datetime] = None,
|
||||||
|
notes: Optional[str] = None,
|
||||||
|
complete: Optional[bool] = False,
|
||||||
|
followup: Optional[bool] = False,
|
||||||
|
contact_id: Optional[int] = None,
|
||||||
|
analysis: Optional[bool] = False,
|
||||||
|
work_item_id: Optional[int] = None,
|
||||||
|
updates: Optional[List[WorkNote]] = None
|
||||||
|
) -> None:
|
||||||
self.start_time = start_time
|
self.start_time = start_time
|
||||||
self.end_time = end_time
|
self.end_time = end_time
|
||||||
self.notes = notes
|
self.notes = notes
|
||||||
|
@ -37,6 +53,7 @@ class WorkLog(db.Model):
|
||||||
self.contact_id = contact_id
|
self.contact_id = contact_id
|
||||||
self.analysis = analysis
|
self.analysis = analysis
|
||||||
self.work_item_id = work_item_id
|
self.work_item_id = work_item_id
|
||||||
|
self.updates = updates or []
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<WorkLog(id={self.id}, start_time={self.start_time}, end_time={self.end_time}, " \
|
return f"<WorkLog(id={self.id}, start_time={self.start_time}, end_time={self.end_time}, " \
|
||||||
|
@ -49,6 +66,7 @@ class WorkLog(db.Model):
|
||||||
'start_time': self.start_time.isoformat() if self.start_time else None,
|
'start_time': self.start_time.isoformat() if self.start_time else None,
|
||||||
'end_time': self.end_time.isoformat() if self.end_time else None,
|
'end_time': self.end_time.isoformat() if self.end_time else None,
|
||||||
'notes': self.notes,
|
'notes': self.notes,
|
||||||
|
'updates': [note.serialize() for note in self.updates or []],
|
||||||
'complete': self.complete,
|
'complete': self.complete,
|
||||||
'followup': self.followup,
|
'followup': self.followup,
|
||||||
'contact_id': self.contact_id,
|
'contact_id': self.contact_id,
|
||||||
|
@ -61,13 +79,26 @@ class WorkLog(db.Model):
|
||||||
start_time_str = data.get("start_time")
|
start_time_str = data.get("start_time")
|
||||||
end_time_str = data.get("end_time")
|
end_time_str = data.get("end_time")
|
||||||
|
|
||||||
|
updates_raw = data.get("updates", [])
|
||||||
|
updates: list[WorkNote] = []
|
||||||
|
|
||||||
|
for u in updates_raw:
|
||||||
|
if isinstance(u, dict):
|
||||||
|
content = u.get("content", "").strip()
|
||||||
|
else:
|
||||||
|
content = str(u).strip()
|
||||||
|
|
||||||
|
if content:
|
||||||
|
updates.append(WorkNote(content=content))
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
start_time=datetime.datetime.fromisoformat(str(start_time_str)) if start_time_str else datetime.datetime.now(),
|
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,
|
end_time=datetime.datetime.fromisoformat(str(end_time_str)) if end_time_str else None,
|
||||||
notes=data.get("notes"),
|
notes=data.get("notes"), # Soon to be removed and sent to a farm upstate
|
||||||
complete=bool(data.get("complete", False)),
|
complete=bool(data.get("complete", False)),
|
||||||
followup=bool(data.get("followup", False)),
|
followup=bool(data.get("followup", False)),
|
||||||
analysis=bool(data.get("analysis", False)),
|
analysis=bool(data.get("analysis", False)),
|
||||||
contact_id=data.get("contact_id"),
|
contact_id=data.get("contact_id"),
|
||||||
work_item_id=data.get("work_item_id")
|
work_item_id=data.get("work_item_id"),
|
||||||
|
updates=updates
|
||||||
)
|
)
|
35
inventory/models/work_note.py
Normal file
35
inventory/models/work_note.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, DateTime, UnicodeText, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
class WorkNote(db.Model):
|
||||||
|
__tablename__ = 'work_note'
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('ix_work_note_work_log_id', 'work_log_id'),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
work_log_id: Mapped[int] = mapped_column(ForeignKey('work_log.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, default=func.now(), server_default=func.now())
|
||||||
|
content: Mapped[str] = mapped_column(UnicodeText, nullable=False)
|
||||||
|
|
||||||
|
work_log = relationship('WorkLog', back_populates='updates')
|
||||||
|
|
||||||
|
def __init__(self, content: str, timestamp: datetime.datetime | None = None) -> None:
|
||||||
|
self.content = content
|
||||||
|
self.timestamp = timestamp or datetime.datetime.now()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
preview = self.content[:30].replace("\n", " ") + "..." if len(self.content) > 30 else self.content
|
||||||
|
return f"<WorkNote(id={self.id}), log_id={self.work_log_id}, ts={self.timestamp}, content={preview!r}>"
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'work_log_id': self.work_log_id,
|
||||||
|
'timestamp': self.timestamp.isoformat(),
|
||||||
|
'content': self.content
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ from flask import request, render_template, jsonify
|
||||||
from . import main
|
from . import main
|
||||||
from .helpers import worklog_headers
|
from .helpers import worklog_headers
|
||||||
from .. import db
|
from .. import db
|
||||||
from ..models import WorkLog, User, Inventory
|
from ..models import WorkLog, User, Inventory, WorkNote
|
||||||
from ..utils.load import eager_load_worklog_relationships, eager_load_user_relationships, eager_load_inventory_relationships
|
from ..utils.load import eager_load_worklog_relationships, eager_load_user_relationships, eager_load_inventory_relationships
|
||||||
|
|
||||||
@main.route("/worklog")
|
@main.route("/worklog")
|
||||||
|
@ -27,12 +27,13 @@ def worklog_entry(id):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='worklog_entry', endpoint_args={'id': -1})
|
return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='worklog_entry', endpoint_args={'id': -1})
|
||||||
|
|
||||||
log = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.id == id).first()
|
log = eager_load_worklog_relationships(db.session.query(WorkLog)).get(id)
|
||||||
user_query = db.session.query(User).order_by(User.first_name)
|
user_query = db.session.query(User).order_by(User.first_name)
|
||||||
users = eager_load_user_relationships(user_query).all()
|
users = eager_load_user_relationships(user_query).all()
|
||||||
item_query = db.session.query(Inventory)
|
item_query = db.session.query(Inventory)
|
||||||
items = eager_load_inventory_relationships(item_query).all()
|
items = eager_load_inventory_relationships(item_query).all()
|
||||||
items = sorted(items, key=lambda i: i.identifier)
|
items = sorted(items, key=lambda i: i.identifier)
|
||||||
|
print(log)
|
||||||
|
|
||||||
if log:
|
if log:
|
||||||
title = f'Work Log - Entry #{id}'
|
title = f'Work Log - Entry #{id}'
|
||||||
|
@ -92,20 +93,32 @@ def create_worklog():
|
||||||
def update_worklog(id):
|
def update_worklog(id):
|
||||||
try:
|
try:
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
print(data)
|
log = eager_load_worklog_relationships(db.session.query(WorkLog)).get(id)
|
||||||
log = db.session.query(WorkLog).get(id)
|
|
||||||
|
|
||||||
if not log:
|
if not log:
|
||||||
return jsonify({"success": False, "error": f"Work Log with ID {id} not found."}), 404
|
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.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.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.complete = bool(data.get("complete", log.complete))
|
||||||
log.followup = bool(data.get("followup", log.followup))
|
log.followup = bool(data.get("followup", log.followup))
|
||||||
log.analysis = bool(data.get("analysis", log.analysis))
|
log.analysis = bool(data.get("analysis", log.analysis))
|
||||||
log.contact_id = data.get("contact_id", log.contact_id)
|
log.contact_id = data.get("contact_id", log.contact_id)
|
||||||
log.work_item_id = data.get("work_item_id", log.work_item_id)
|
log.work_item_id = data.get("work_item_id", log.work_item_id)
|
||||||
|
existing = {str(note.id): note for note in log.updates}
|
||||||
|
incoming = data.get("updates", [])
|
||||||
|
new_updates = []
|
||||||
|
|
||||||
|
for note_data in incoming:
|
||||||
|
if isinstance(note_data, dict):
|
||||||
|
if "id" in note_data and str(note_data["id"]) in existing:
|
||||||
|
note = existing[str(note_data["id"])]
|
||||||
|
note.content = note_data.get("content", note.content)
|
||||||
|
new_updates.append(note)
|
||||||
|
elif "content" in note_data:
|
||||||
|
new_updates.append(WorkNote(content=note_data["content"]))
|
||||||
|
|
||||||
|
log.updates[:] = new_updates # This replaces in-place
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="notes" class="form-label">Notes</label>
|
<label for="notes" class="form-label">Notes</label>
|
||||||
|
@ -95,6 +96,18 @@
|
||||||
rows="15">{{ log.notes if log.notes else '' }}</textarea>
|
rows="15">{{ log.notes if log.notes else '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
#}
|
||||||
|
<label class="form-label">Updates</label>
|
||||||
|
{% for update in log.updates %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">{{ update.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||||
|
<textarea name="update{{ loop.index0 }}" id="" class="form-control" rows="5" data-note-id="{{ update.id if update.id }}">{{ update.content }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -103,19 +116,29 @@
|
||||||
const saveButton = document.getElementById("saveButton");
|
const saveButton = document.getElementById("saveButton");
|
||||||
const deleteButton = document.getElementById("deleteButton");
|
const deleteButton = document.getElementById("deleteButton");
|
||||||
|
|
||||||
|
const updateTextareas = Array.from(document.querySelectorAll("textarea[name^='update']"));
|
||||||
|
const updates = updateTextareas.map(el => {
|
||||||
|
const id = el.dataset.noteId;
|
||||||
|
const content = el.value;
|
||||||
|
return id ? { id, content } : { content };
|
||||||
|
});
|
||||||
|
|
||||||
if (saveButton) {
|
if (saveButton) {
|
||||||
saveButton.addEventListener("click", async (e) => {
|
saveButton.addEventListener("click", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const updateTextareas = Array.from(document.querySelectorAll("textarea[name^='update']"));
|
||||||
|
const updates = updateTextareas.map(el => el.value).filter(val => val.trim() !== '');
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
start_time: document.querySelector("input[name='start']").value,
|
start_time: document.querySelector("input[name='start']").value,
|
||||||
end_time: document.querySelector("input[name='end']").value,
|
end_time: document.querySelector("input[name='end']").value,
|
||||||
notes: document.querySelector("textarea[name='notes']").value,
|
|
||||||
complete: document.querySelector("input[name='complete']").checked,
|
complete: document.querySelector("input[name='complete']").checked,
|
||||||
analysis: document.querySelector("input[name='analysis']").checked,
|
analysis: document.querySelector("input[name='analysis']").checked,
|
||||||
followup: document.querySelector("input[name='followup']").checked,
|
followup: document.querySelector("input[name='followup']").checked,
|
||||||
contact_id: document.querySelector("select[name='contact']").value || null,
|
contact_id: document.querySelector("select[name='contact']").value || null,
|
||||||
work_item_id: document.querySelector("select[name='item']").value || null,
|
work_item_id: document.querySelector("select[name='item']").value || null,
|
||||||
|
updates: updates
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -27,7 +27,8 @@ def eager_load_room_relationships(query):
|
||||||
def eager_load_worklog_relationships(query):
|
def eager_load_worklog_relationships(query):
|
||||||
return query.options(
|
return query.options(
|
||||||
joinedload(WorkLog.contact),
|
joinedload(WorkLog.contact),
|
||||||
joinedload(WorkLog.work_item)
|
joinedload(WorkLog.work_item),
|
||||||
|
joinedload(WorkLog.updates)
|
||||||
)
|
)
|
||||||
|
|
||||||
def chunk_list(lst, chunk_size):
|
def chunk_list(lst, chunk_size):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue