Skip to content

Commit 0fea3fd

Browse files
committed
feat: Implement CSV Ticket Viewer
- Refactor App component to replace existing features with CSV Ticket Table. - Add CSVTicketTable component for displaying tickets from CSV data source. - Introduce API functions for fetching CSV ticket fields, tickets, and statistics. - Create CSV data source in backend to handle loading and processing of CSV files. - Enhance AgentChat component to display error details from API responses. - Update styles and layout for improved user experience in ticket viewing. Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent 4049650 commit 0fea3fd

7 files changed

Lines changed: 1329 additions & 36 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ Thumbs.db
5050
*.log
5151
logs/
5252
*.db
53+
csv/*.csv

backend/app.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
from agents import AgentRequest, AgentResponse, agent_service
3434
from api_decorators import operation
3535

36+
# CSV ticket service
37+
from csv_data import Ticket, get_csv_ticket_service
38+
3639
# FastMCP client for direct ticket MCP calls (no AI)
3740
from fastmcp import Client as MCPClient
3841
from mcp_handler import handle_mcp_request
@@ -386,6 +389,180 @@ async def get_qa_tickets():
386389
# NON-TASK ENDPOINTS
387390
# ============================================================================
388391

392+
# ============================================================================
393+
# CSV TICKET ENDPOINTS
394+
# ============================================================================
395+
396+
# Initialize CSV ticket service
397+
_csv_ticket_service = get_csv_ticket_service()
398+
399+
# Load CSV data on startup (using relative path from backend folder)
400+
_csv_data_path = Path(__file__).parent.parent / "csv" / "data.csv"
401+
if _csv_data_path.exists():
402+
_csv_loaded = _csv_ticket_service.load_csv(_csv_data_path)
403+
print(f"📊 Loaded {_csv_loaded} tickets from CSV")
404+
405+
406+
# Define which fields are available for display
407+
CSV_TICKET_FIELDS = [
408+
{"name": "id", "label": "ID", "type": "uuid"},
409+
{"name": "summary", "label": "Summary", "type": "string"},
410+
{"name": "status", "label": "Status", "type": "enum"},
411+
{"name": "priority", "label": "Priority", "type": "enum"},
412+
{"name": "assignee", "label": "Assignee", "type": "string"},
413+
{"name": "assigned_group", "label": "Assigned Group", "type": "string"},
414+
{"name": "requester_name", "label": "Requester", "type": "string"},
415+
{"name": "requester_email", "label": "Email", "type": "string"},
416+
{"name": "city", "label": "City", "type": "string"},
417+
{"name": "country", "label": "Country", "type": "string"},
418+
{"name": "service", "label": "Service", "type": "string"},
419+
{"name": "incident_type", "label": "Incident Type", "type": "string"},
420+
{"name": "product_name", "label": "Product", "type": "string"},
421+
{"name": "manufacturer", "label": "Manufacturer", "type": "string"},
422+
{"name": "created_at", "label": "Created", "type": "datetime"},
423+
{"name": "updated_at", "label": "Updated", "type": "datetime"},
424+
{"name": "urgency", "label": "Urgency", "type": "string"},
425+
{"name": "impact", "label": "Impact", "type": "string"},
426+
{"name": "resolution", "label": "Resolution", "type": "string"},
427+
{"name": "notes", "label": "Notes", "type": "string"},
428+
]
429+
430+
431+
@app.route("/api/csv-tickets/fields", methods=["GET"])
432+
async def get_csv_ticket_fields():
433+
"""Get metadata about available CSV ticket fields."""
434+
return jsonify({
435+
"fields": CSV_TICKET_FIELDS,
436+
"total_tickets": _csv_ticket_service.total_count,
437+
})
438+
439+
440+
@app.route("/api/csv-tickets", methods=["GET"])
441+
async def get_csv_tickets():
442+
"""
443+
Get CSV tickets with optional filtering, sorting, and field selection.
444+
445+
Query params:
446+
- fields: comma-separated list of field names to include
447+
- status: filter by status (new, assigned, in_progress, pending, resolved, closed)
448+
- has_assignee: filter by assignee presence (true/false)
449+
- assigned_group: filter by group name
450+
- sort: field name to sort by
451+
- sort_dir: asc or desc (default: asc)
452+
- limit: max number of results
453+
- offset: number of results to skip
454+
"""
455+
from tickets import TicketStatus
456+
457+
# Parse query params
458+
fields_param = request.args.get("fields", "")
459+
status_param = request.args.get("status")
460+
has_assignee_param = request.args.get("has_assignee")
461+
assigned_group_param = request.args.get("assigned_group")
462+
sort_param = request.args.get("sort", "created_at")
463+
sort_dir = request.args.get("sort_dir", "desc")
464+
limit = request.args.get("limit", type=int)
465+
offset = request.args.get("offset", 0, type=int)
466+
467+
# Determine which fields to include
468+
if fields_param:
469+
selected_fields = [f.strip() for f in fields_param.split(",")]
470+
else:
471+
# Default fields for table display
472+
selected_fields = ["summary", "status", "priority", "assignee", "assigned_group", "requester_name", "city", "created_at"]
473+
474+
# Parse filters
475+
status_filter = None
476+
if status_param:
477+
try:
478+
status_filter = TicketStatus(status_param)
479+
except ValueError:
480+
pass
481+
482+
has_assignee_filter = None
483+
if has_assignee_param is not None:
484+
has_assignee_filter = has_assignee_param.lower() == "true"
485+
486+
# Get tickets with filters
487+
tickets = _csv_ticket_service.list_tickets(
488+
status=status_filter,
489+
assigned_group=assigned_group_param,
490+
has_assignee=has_assignee_filter,
491+
)
492+
493+
# Sort tickets
494+
def get_sort_key(ticket: Ticket):
495+
val = getattr(ticket, sort_param, None)
496+
if val is None:
497+
return ""
498+
if hasattr(val, "value"): # Enum
499+
return val.value
500+
return val
501+
502+
try:
503+
tickets = sorted(tickets, key=get_sort_key, reverse=(sort_dir == "desc"))
504+
except TypeError:
505+
pass # Skip sorting if types are incompatible
506+
507+
total_count = len(tickets)
508+
509+
# Apply pagination
510+
if limit:
511+
tickets = tickets[offset:offset + limit]
512+
elif offset:
513+
tickets = tickets[offset:]
514+
515+
# Build response with selected fields only
516+
result = []
517+
for ticket in tickets:
518+
row = {}
519+
for field in selected_fields:
520+
val = getattr(ticket, field, None)
521+
if val is None:
522+
row[field] = None
523+
elif hasattr(val, "value"): # Enum
524+
row[field] = val.value
525+
elif hasattr(val, "isoformat"): # datetime
526+
row[field] = val.isoformat()
527+
elif hasattr(val, "hex"): # UUID
528+
row[field] = str(val)
529+
else:
530+
row[field] = val
531+
result.append(row)
532+
533+
return jsonify({
534+
"tickets": result,
535+
"total": total_count,
536+
"offset": offset,
537+
"limit": limit,
538+
"fields": selected_fields,
539+
})
540+
541+
542+
@app.route("/api/csv-tickets/stats", methods=["GET"])
543+
async def get_csv_ticket_stats():
544+
"""Get statistics about CSV tickets."""
545+
from collections import Counter
546+
547+
tickets = _csv_ticket_service.list_tickets()
548+
549+
statuses = Counter(t.status.value for t in tickets)
550+
priorities = Counter(t.priority.value for t in tickets)
551+
groups = Counter(t.assigned_group for t in tickets if t.assigned_group)
552+
cities = Counter(t.city for t in tickets if t.city)
553+
554+
unassigned_count = sum(1 for t in tickets if t.assignee is None and t.assigned_group is not None)
555+
556+
return jsonify({
557+
"total": len(tickets),
558+
"unassigned": unassigned_count,
559+
"by_status": dict(statuses),
560+
"by_priority": dict(priorities),
561+
"by_group": dict(groups.most_common(10)),
562+
"by_city": dict(cities.most_common(10)),
563+
})
564+
565+
389566
@app.route("/api/health", methods=["GET"])
390567
async def health_check():
391568
"""Health check endpoint."""

0 commit comments

Comments
 (0)