Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Free-tier dynos sleep after 15 min of inactivity. The frontend warms the backend
| RMAs | `GET/POST/PUT /rmas` |
| AI | `POST /ai/suggest` |

Status changes on tickets automatically append a row to `ticket_history`.
Ticket creation and every status change automatically append a row to `ticket_history` (creation is logged with `old_status = null`).

Full OpenAPI spec available at `http://localhost:8000/docs` when the backend is running.

Expand Down
21 changes: 17 additions & 4 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,23 @@ def get_ticket(id: str):

@app.post("/tickets")
def create_ticket(t: Ticket):
return db_post(
"tickets",
clean_empty_strings(model_data(t), "customer_id", "device_id", "assigned_user_id"),
)
data = clean_empty_strings(model_data(t), "customer_id", "device_id", "assigned_user_id")
result = db_post("tickets", data)
# Record the initial status so the audit trail starts at creation
# (old_status=None), matching the history rows in the seed data.
# Supabase returns the inserted rows because of Prefer: return=representation.
created = result[0] if isinstance(result, list) and result else result
if isinstance(created, dict) and created.get("id"):
db_post(
"ticket_history",
{
"ticket_id": created["id"],
"old_status": None,
"new_status": created.get("status", data["status"]),
"changed_by": "Agent",
},
)
return result


@app.put("/tickets/{id}")
Expand Down
36 changes: 36 additions & 0 deletions backend/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,42 @@ def fake_get(table, params=""):
) in posts


def test_create_ticket_records_initial_status_history(client, monkeypatch):
test_client, main = client
posts = []

def fake_post(table, data):
posts.append((table, data))
if table == "tickets":
return [{"id": "t9", **data, "status": "In Progress"}]
return [data]

monkeypatch.setattr(main, "db_post", fake_post)

res = test_client.post("/tickets", json={"title": "VPN down", "status": "In Progress"})

assert res.status_code == 200
assert res.json()[0]["id"] == "t9"
assert [table for table, _ in posts] == ["tickets", "ticket_history"]
assert posts[1][1] == {
"ticket_id": "t9",
"old_status": None,
"new_status": "In Progress",
"changed_by": "Agent",
}


def test_create_ticket_skips_history_when_insert_returns_no_row(client, monkeypatch):
test_client, main = client
posts = []
monkeypatch.setattr(main, "db_post", lambda table, data: posts.append(table) or [])

res = test_client.post("/tickets", json={"title": "VPN down"})

assert res.status_code == 200
assert posts == ["tickets"]


def test_ticket_routes_convert_blank_relationship_ids_to_null(client, monkeypatch):
test_client, main = client
calls = []
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/Tickets.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export default function Tickets() {
<h3 className="font-semibold mb-3">Status History</h3>
{detailLoading ? <p className="text-gray-400 text-sm">Loading history…</p> : history.length === 0 ? <p className="text-gray-400 text-sm">No history yet</p> : history.map(h => (
<div key={h.id} className="border-b py-2 text-sm flex gap-2 items-center">
<span className={`whitespace-nowrap px-2 py-0.5 rounded-full text-xs ${statusColor(h.old_status)}`}>{h.old_status}</span>
<span className={`whitespace-nowrap px-2 py-0.5 rounded-full text-xs ${h.old_status ? statusColor(h.old_status) : "bg-gray-50 text-gray-500"}`}>{h.old_status ?? "Created"}</span>
<span className="text-gray-400">→</span>
<span className={`whitespace-nowrap px-2 py-0.5 rounded-full text-xs ${statusColor(h.new_status)}`}>{h.new_status}</span>
<span className="text-gray-400 text-xs ml-auto">{new Date(h.changed_at).toLocaleString()}</span>
Expand Down
Loading