diff --git a/AGENTS.md b/AGENTS.md index a3cdd37..59691e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,11 @@ Frontend: Alpine.js (CDN) + Tailwind CSS (CDN). Auth state in localStorage. Role ## Ticket System (Sprint 2) +- `reported_at` (nullable DateTime, alembic `d5e0f2a1c3b4`) records a ticket's original reported date; + `TicketCreate.reported_at` lets Admin/Wahab enter backdated tickets that stay active. It defaults to + now when omitted (migration backfilled existing rows from `created_at`). SLA deadlines run from + `created_at`, not `reported_at` — backfilling history never instantly breaches a ticket. + ### Status Lifecycle (16 statuses) New → Logged → Triage → Assigned → Accepted → Travelling → On Site → In Progress → Waiting Parts → Escalated → Completed → On-Field Verification → Wahab Review → Closed → Reopened → Cancelled diff --git a/alembic/versions/d5e0f2a1c3b4_tickets_reported_at.py b/alembic/versions/d5e0f2a1c3b4_tickets_reported_at.py new file mode 100644 index 0000000..fbaa617 --- /dev/null +++ b/alembic/versions/d5e0f2a1c3b4_tickets_reported_at.py @@ -0,0 +1,47 @@ +"""tickets.reported_at column — original reported date for backdated tickets + +Revision ID: d5e0f2a1c3b4 +Revises: c4e8f1a2d3b4 +Create Date: 2026-08-03 00:00:00.000000 + +Backdated-ticket support (Wahab demo): one schema change. + +* Add ``tickets.reported_at`` (nullable DateTime) so historical/backfilled + tickets keep their true report date instead of inheriting today's + ``created_at``. +* Backfill existing rows with their ``created_at`` value so no ticket shows + an empty reported date after the upgrade. The service layer also defaults + new tickets without a ``reported_at`` to now, so the column is effectively + always populated from here on. +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'd5e0f2a1c3b4' +down_revision: Union[str, Sequence[str], None] = 'c4e8f1a2d3b4' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Add tickets.reported_at (nullable) and backfill from created_at.""" + op.add_column( + 'tickets', + sa.Column( + 'reported_at', + sa.DateTime(), + nullable=True, + comment='Original reported date; backdated/backfilled tickets keep their true report date', + ), + ) + # Backfill: every existing ticket was reported when it was created. + op.execute('UPDATE tickets SET reported_at = created_at WHERE reported_at IS NULL') + + +def downgrade() -> None: + """Drop tickets.reported_at.""" + op.drop_column('tickets', 'reported_at') diff --git a/app/main.py b/app/main.py index e10f27e..d45289d 100644 --- a/app/main.py +++ b/app/main.py @@ -38,6 +38,14 @@ async def ensure_legacy_schema(conn) -> None: text("ALTER TABLE tickets ADD COLUMN phone VARCHAR(50)") ) logger.info("Added missing tickets.phone column (legacy database)") + if "reported_at" not in ticket_columns: + await conn.execute( + text("ALTER TABLE tickets ADD COLUMN reported_at DATETIME") + ) + await conn.execute( + text("UPDATE tickets SET reported_at = created_at WHERE reported_at IS NULL") + ) + logger.info("Added missing tickets.reported_at column (legacy database)") result = await conn.execute( text( "UPDATE categories SET name = 'Missing Item' " diff --git a/app/models/ticket.py b/app/models/ticket.py index 919bdc5..17882aa 100644 --- a/app/models/ticket.py +++ b/app/models/ticket.py @@ -55,6 +55,11 @@ class Ticket(Base): customer_rating: Mapped[int | None] = mapped_column(Integer, nullable=True) reopen_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) closed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + reported_at: Mapped[datetime | None] = mapped_column( + DateTime, + nullable=True, + comment="Original reported date. Backdated/backfilled tickets keep their true report date; NULL falls back to created_at.", + ) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime, diff --git a/app/schemas/ticket.py b/app/schemas/ticket.py index e3d9ae0..86172ba 100644 --- a/app/schemas/ticket.py +++ b/app/schemas/ticket.py @@ -46,6 +46,7 @@ class TicketCreate(BaseModel): assigned_to: int | None = None customer_name: str | None = None phone: str | None = None + reported_at: datetime | None = None # original report date for backdated/backfilled tickets; defaults to now when omitted class TicketUpdate(BaseModel): @@ -100,6 +101,7 @@ class TicketBrief(BaseModel): description: str | None = None sla_deadline: datetime | None = None reopen_count: int = 0 + reported_at: datetime | None = None created_at: datetime updated_at: datetime diff --git a/app/services/ticket.py b/app/services/ticket.py index a8cf8da..00fd6d1 100644 --- a/app/services/ticket.py +++ b/app/services/ticket.py @@ -114,6 +114,10 @@ async def create_ticket( ticket_number = await _generate_ticket_number(db) priority = data.get("priority") sla_deadline = compute_sla_deadline(priority) if priority else None + # Original report date: backdated/backfilled tickets keep their true date; + # when omitted the ticket is considered reported right now. The SLA clock + # is unchanged — deadlines run from creation time, not the reported date. + reported_at = data.get("reported_at") or datetime.now(timezone.utc) ticket = Ticket( ticket_number=ticket_number, @@ -126,6 +130,7 @@ async def create_ticket( reported_via=data.get("reported_via"), description=data.get("description"), assigned_to=data.get("assigned_to"), + reported_at=reported_at, sla_deadline=sla_deadline, ) db.add(ticket) diff --git a/app/templates/tickets/detail.html b/app/templates/tickets/detail.html index ffdce9d..872a844 100644 --- a/app/templates/tickets/detail.html +++ b/app/templates/tickets/detail.html @@ -23,7 +23,11 @@ -
Created
+Created + + · Reported + +
Defaults to today. Use a past date when entering an old/backlogged issue — it stays active in the normal workflow.
+