- Added views for listing fixed-price agreements, displaying agreement details, and a reporting dashboard. - Created HTML templates for listing, detailing, and reporting on fixed-price agreements. - Introduced API endpoint to fetch active customers for agreement creation. - Added migration scripts for creating necessary database tables and views for fixed-price agreements, billing periods, and reporting. - Implemented triggers for auto-generating agreement numbers and updating timestamps. - Enhanced ticket management with archived ticket views and filtering capabilities.
798 lines
26 KiB
Python
798 lines
26 KiB
Python
"""
|
|
Pydantic Models for Ticket System & Klippekort Module
|
|
======================================================
|
|
|
|
Alle models repræsenterer data fra tticket_* tabeller.
|
|
Modulet er isoleret og har ingen afhængigheder til core Hub-models.
|
|
"""
|
|
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from typing import List, Optional
|
|
from pydantic import BaseModel, Field, field_validator
|
|
from enum import Enum
|
|
|
|
|
|
# ============================================================================
|
|
# ENUMS
|
|
# ============================================================================
|
|
|
|
class TicketStatus(str, Enum):
|
|
"""Ticket status workflow"""
|
|
OPEN = "open"
|
|
IN_PROGRESS = "in_progress"
|
|
WAITING_CUSTOMER = "waiting_customer"
|
|
WAITING_INTERNAL = "waiting_internal"
|
|
RESOLVED = "resolved"
|
|
CLOSED = "closed"
|
|
|
|
|
|
class TicketPriority(str, Enum):
|
|
"""Ticket prioritet"""
|
|
LOW = "low"
|
|
NORMAL = "normal"
|
|
HIGH = "high"
|
|
URGENT = "urgent"
|
|
|
|
|
|
class TicketSource(str, Enum):
|
|
"""Hvor ticket blev oprettet fra"""
|
|
EMAIL = "email"
|
|
PORTAL = "portal"
|
|
PHONE = "phone"
|
|
MANUAL = "manual"
|
|
API = "api"
|
|
|
|
|
|
class WorkType(str, Enum):
|
|
"""Type af arbejde"""
|
|
SUPPORT = "support"
|
|
DEVELOPMENT = "development"
|
|
TROUBLESHOOTING = "troubleshooting"
|
|
ON_SITE = "on_site"
|
|
MEETING = "meeting"
|
|
OTHER = "other"
|
|
|
|
|
|
class BillingMethod(str, Enum):
|
|
"""Afregningsmetode"""
|
|
PREPAID_CARD = "prepaid_card"
|
|
FIXED_PRICE = "fixed_price"
|
|
INVOICE = "invoice"
|
|
INTERNAL = "internal"
|
|
WARRANTY = "warranty"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
class WorklogStatus(str, Enum):
|
|
"""Worklog status"""
|
|
DRAFT = "draft"
|
|
BILLABLE = "billable"
|
|
BILLED = "billed"
|
|
NON_BILLABLE = "non_billable"
|
|
|
|
|
|
class PrepaidCardStatus(str, Enum):
|
|
"""Klippekort status"""
|
|
ACTIVE = "active"
|
|
DEPLETED = "depleted"
|
|
EXPIRED = "expired"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
class TransactionType(str, Enum):
|
|
"""Klippekort transaction type"""
|
|
PURCHASE = "purchase"
|
|
TOP_UP = "top_up"
|
|
USAGE = "usage"
|
|
REFUND = "refund"
|
|
EXPIRATION = "expiration"
|
|
CANCELLATION = "cancellation"
|
|
|
|
|
|
class TicketType(str, Enum):
|
|
"""Ticket kategorisering"""
|
|
INCIDENT = "incident" # Fejl der skal fixes (Høj urgens)
|
|
REQUEST = "request" # Bestilling / Ønske (Planlægges)
|
|
PROBLEM = "problem" # Root cause (Fejlfinding)
|
|
PROJECT = "project" # Større projektarbejde
|
|
|
|
|
|
# ============================================================================
|
|
# TICKET MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketBase(BaseModel):
|
|
"""Base model for ticket"""
|
|
subject: str = Field(..., min_length=1, max_length=500)
|
|
description: Optional[str] = None
|
|
status: TicketStatus = Field(default=TicketStatus.OPEN)
|
|
priority: TicketPriority = Field(default=TicketPriority.NORMAL)
|
|
ticket_type: TicketType = Field(default=TicketType.INCIDENT, description="Type af sag")
|
|
internal_note: Optional[str] = Field(default=None, description="Intern note der vises prominent til medarbejdere")
|
|
category: Optional[str] = Field(None, max_length=100)
|
|
customer_id: Optional[int] = Field(None, description="Reference til customers.id")
|
|
contact_id: Optional[int] = Field(None, description="Reference til contacts.id")
|
|
assigned_to_user_id: Optional[int] = Field(None, description="Reference til users.user_id")
|
|
source: TicketSource = Field(default=TicketSource.MANUAL)
|
|
tags: Optional[List[str]] = Field(default_factory=list)
|
|
custom_fields: Optional[dict] = Field(default_factory=dict)
|
|
|
|
|
|
class TTicketCreate(TTicketBase):
|
|
"""Model for creating a ticket"""
|
|
ticket_number: Optional[str] = Field(None, description="Auto-generated hvis ikke angivet")
|
|
created_by_user_id: Optional[int] = Field(None, description="User der opretter ticket")
|
|
|
|
|
|
class TTicketUpdate(BaseModel):
|
|
"""Model for updating a ticket (partial updates)"""
|
|
subject: Optional[str] = Field(None, min_length=1, max_length=500)
|
|
description: Optional[str] = None
|
|
status: Optional[TicketStatus] = None
|
|
priority: Optional[TicketPriority] = None
|
|
category: Optional[str] = Field(None, max_length=100)
|
|
customer_id: Optional[int] = None
|
|
contact_id: Optional[int] = None
|
|
assigned_to_user_id: Optional[int] = None
|
|
tags: Optional[List[str]] = None
|
|
custom_fields: Optional[dict] = None
|
|
|
|
|
|
class TTicket(TTicketBase):
|
|
"""Full ticket model with DB fields"""
|
|
id: int
|
|
ticket_number: str
|
|
created_by_user_id: Optional[int] = None
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
first_response_at: Optional[datetime] = None
|
|
resolved_at: Optional[datetime] = None
|
|
closed_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class TTicketWithStats(TTicket):
|
|
"""Ticket med statistik (fra view)"""
|
|
comment_count: Optional[int] = 0
|
|
attachment_count: Optional[int] = 0
|
|
pending_hours: Optional[Decimal] = None
|
|
billed_hours: Optional[Decimal] = None
|
|
last_comment_at: Optional[datetime] = None
|
|
age_hours: Optional[float] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# COMMENT MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketCommentBase(BaseModel):
|
|
"""Base model for comment"""
|
|
comment_text: str = Field(..., min_length=1)
|
|
is_internal: bool = Field(default=False, description="Intern note (ikke kunde-synlig)")
|
|
|
|
|
|
class TTicketCommentCreate(TTicketCommentBase):
|
|
"""Model for creating a comment"""
|
|
ticket_id: int = Field(..., gt=0)
|
|
user_id: Optional[int] = Field(None, description="User der opretter kommentar")
|
|
|
|
|
|
class TTicketComment(TTicketCommentBase):
|
|
"""Full comment model with DB fields"""
|
|
id: int
|
|
ticket_id: int
|
|
user_id: Optional[int] = None
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# ATTACHMENT MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketAttachmentBase(BaseModel):
|
|
"""Base model for attachment"""
|
|
file_name: str = Field(..., min_length=1, max_length=255)
|
|
file_path: str = Field(..., min_length=1, max_length=500)
|
|
file_size: Optional[int] = Field(None, ge=0)
|
|
mime_type: Optional[str] = Field(None, max_length=100)
|
|
|
|
|
|
class TTicketAttachmentCreate(TTicketAttachmentBase):
|
|
"""Model for creating an attachment"""
|
|
ticket_id: int = Field(..., gt=0)
|
|
uploaded_by_user_id: Optional[int] = None
|
|
|
|
|
|
class TTicketAttachment(TTicketAttachmentBase):
|
|
"""Full attachment model with DB fields"""
|
|
id: int
|
|
ticket_id: int
|
|
uploaded_by_user_id: Optional[int] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# WORKLOG MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketWorklogBase(BaseModel):
|
|
"""Base model for worklog entry"""
|
|
work_date: date = Field(..., description="Dato arbejdet blev udført")
|
|
hours: Decimal = Field(..., gt=0, le=24, description="Timer brugt")
|
|
work_type: WorkType = Field(default=WorkType.SUPPORT)
|
|
description: Optional[str] = None
|
|
billing_method: BillingMethod = Field(default=BillingMethod.INVOICE)
|
|
is_internal: bool = Field(default=False, description="Skjul for kunde (vises ikke på faktura/portal)")
|
|
|
|
@field_validator('hours')
|
|
@classmethod
|
|
def validate_hours(cls, v):
|
|
"""Validate hours is reasonable (max 24 hours per day)"""
|
|
if v <= 0:
|
|
raise ValueError('Hours must be greater than 0')
|
|
if v > 24:
|
|
raise ValueError('Hours cannot exceed 24 per entry')
|
|
return v
|
|
|
|
|
|
class TTicketWorklogCreate(TTicketWorklogBase):
|
|
"""Model for creating a worklog entry"""
|
|
ticket_id: int = Field(..., gt=0)
|
|
user_id: Optional[int] = Field(None, description="User der opretter worklog")
|
|
prepaid_card_id: Optional[int] = Field(None, description="Klippekort ID hvis billing_method=prepaid_card")
|
|
|
|
|
|
class TTicketWorklogUpdate(BaseModel):
|
|
"""Model for updating a worklog entry (partial updates)"""
|
|
work_date: Optional[date] = None
|
|
hours: Optional[Decimal] = Field(None, gt=0, le=24)
|
|
work_type: Optional[WorkType] = None
|
|
description: Optional[str] = None
|
|
billing_method: Optional[BillingMethod] = None
|
|
status: Optional[WorklogStatus] = None
|
|
prepaid_card_id: Optional[int] = None
|
|
is_internal: Optional[bool] = None
|
|
|
|
|
|
class TTicketWorklog(TTicketWorklogBase):
|
|
"""Full worklog model with DB fields"""
|
|
id: int
|
|
ticket_id: int
|
|
user_id: Optional[int] = None
|
|
status: WorklogStatus = Field(default=WorklogStatus.DRAFT)
|
|
prepaid_card_id: Optional[int] = None
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
billed_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class TTicketWorklogWithDetails(TTicketWorklog):
|
|
"""Worklog med ticket detaljer (til review UI)"""
|
|
ticket_number: Optional[str] = None
|
|
ticket_subject: Optional[str] = None
|
|
customer_id: Optional[int] = None
|
|
ticket_status: Optional[str] = None
|
|
has_sufficient_balance: Optional[bool] = True
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# PREPAID CARD (KLIPPEKORT) MODELS
|
|
# ============================================================================
|
|
|
|
class TPrepaidCardBase(BaseModel):
|
|
"""Base model for prepaid card"""
|
|
customer_id: int = Field(..., gt=0, description="Reference til customers.id")
|
|
purchased_hours: Decimal = Field(..., gt=0, description="Timer købt")
|
|
price_per_hour: Decimal = Field(..., gt=0, description="DKK pr. time")
|
|
total_amount: Decimal = Field(..., gt=0, description="Total pris DKK")
|
|
expires_at: Optional[datetime] = Field(None, description="Udløbsdato (NULL = ingen udløb)")
|
|
notes: Optional[str] = None
|
|
|
|
@field_validator('total_amount')
|
|
@classmethod
|
|
def validate_total(cls, v, info):
|
|
"""Validate that total_amount matches purchased_hours * price_per_hour"""
|
|
# Note: This validator runs after all fields are set in Pydantic v2
|
|
# If we need cross-field validation, use model_validator instead
|
|
if v <= 0:
|
|
raise ValueError('Total amount must be greater than 0')
|
|
return v
|
|
|
|
|
|
class TPrepaidCardCreate(TPrepaidCardBase):
|
|
"""Model for creating a prepaid card"""
|
|
card_number: Optional[str] = Field(None, description="Auto-generated hvis ikke angivet")
|
|
created_by_user_id: Optional[int] = Field(None, description="User der opretter kort")
|
|
economic_invoice_number: Optional[str] = Field(None, max_length=50)
|
|
economic_product_number: Optional[str] = Field(None, max_length=50)
|
|
|
|
|
|
class TPrepaidCardUpdate(BaseModel):
|
|
"""Model for updating a prepaid card (partial updates)"""
|
|
status: Optional[PrepaidCardStatus] = None
|
|
expires_at: Optional[datetime] = None
|
|
notes: Optional[str] = None
|
|
economic_invoice_number: Optional[str] = Field(None, max_length=50)
|
|
economic_product_number: Optional[str] = Field(None, max_length=50)
|
|
|
|
|
|
class TPrepaidCard(TPrepaidCardBase):
|
|
"""Full prepaid card model with DB fields"""
|
|
id: int
|
|
card_number: str
|
|
used_hours: Decimal = Field(default=Decimal('0'))
|
|
remaining_hours: Decimal # Generated column
|
|
status: PrepaidCardStatus = Field(default=PrepaidCardStatus.ACTIVE)
|
|
purchased_at: datetime
|
|
economic_invoice_number: Optional[str] = None
|
|
economic_product_number: Optional[str] = None
|
|
created_by_user_id: Optional[int] = None
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class TPrepaidCardWithStats(TPrepaidCard):
|
|
"""Prepaid card med statistik (fra view)"""
|
|
usage_count: Optional[int] = 0
|
|
total_hours_used: Optional[Decimal] = None
|
|
billed_usage_count: Optional[int] = 0
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# PREPAID TRANSACTION MODELS
|
|
# ============================================================================
|
|
|
|
class TPrepaidTransactionBase(BaseModel):
|
|
"""Base model for prepaid transaction"""
|
|
transaction_type: TransactionType
|
|
hours: Decimal = Field(..., description="Timer (positiv=tilføj, negativ=træk)")
|
|
description: Optional[str] = None
|
|
|
|
|
|
class TPrepaidTransactionCreate(TPrepaidTransactionBase):
|
|
"""Model for creating a transaction"""
|
|
card_id: int = Field(..., gt=0)
|
|
worklog_id: Optional[int] = Field(None, description="Reference til worklog (NULL for køb/top-up)")
|
|
balance_after: Decimal = Field(..., ge=0, description="Saldo efter transaction")
|
|
created_by_user_id: Optional[int] = None
|
|
|
|
|
|
class TPrepaidTransaction(TPrepaidTransactionBase):
|
|
"""Full transaction model with DB fields"""
|
|
id: int
|
|
card_id: int
|
|
worklog_id: Optional[int] = None
|
|
balance_after: Decimal
|
|
created_at: datetime
|
|
created_by_user_id: Optional[int] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# EMAIL INTEGRATION MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketEmailLogBase(BaseModel):
|
|
"""Base model for email log"""
|
|
action: str = Field(..., max_length=50)
|
|
|
|
|
|
class TTicketEmailLogCreate(TTicketEmailLogBase):
|
|
"""Model for creating an email log entry"""
|
|
ticket_id: Optional[int] = None
|
|
email_id: Optional[int] = Field(None, description="Reference til email_messages.id")
|
|
email_message_id: Optional[str] = Field(None, max_length=500, description="Email Message-ID header")
|
|
|
|
|
|
class TTicketEmailLog(TTicketEmailLogBase):
|
|
"""Full email log model with DB fields"""
|
|
id: int
|
|
ticket_id: Optional[int] = None
|
|
email_id: Optional[int] = None
|
|
email_message_id: Optional[str] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# AUDIT LOG MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketAuditLogBase(BaseModel):
|
|
"""Base model for audit log"""
|
|
entity_type: str = Field(..., max_length=50)
|
|
action: str = Field(..., max_length=50)
|
|
old_value: Optional[str] = None
|
|
new_value: Optional[str] = None
|
|
details: Optional[dict] = Field(default_factory=dict)
|
|
|
|
|
|
class TTicketAuditLogCreate(TTicketAuditLogBase):
|
|
"""Model for creating an audit log entry"""
|
|
ticket_id: Optional[int] = None
|
|
entity_id: Optional[int] = None
|
|
user_id: Optional[int] = None
|
|
|
|
|
|
class TTicketAuditLog(TTicketAuditLogBase):
|
|
"""Full audit log model with DB fields"""
|
|
id: int
|
|
ticket_id: Optional[int] = None
|
|
entity_id: Optional[int] = None
|
|
user_id: Optional[int] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# RESPONSE MODELS (for API responses)
|
|
# ============================================================================
|
|
|
|
class TicketListResponse(BaseModel):
|
|
"""Response model for ticket lists"""
|
|
tickets: List[TTicketWithStats]
|
|
total: int
|
|
page: int = 1
|
|
page_size: int = 50
|
|
|
|
|
|
class WorklogReviewResponse(BaseModel):
|
|
"""Response model for worklog review page"""
|
|
worklogs: List[TTicketWorklogWithDetails]
|
|
total: int
|
|
total_hours: Decimal
|
|
total_billable_hours: Decimal
|
|
|
|
|
|
class PrepaidCardBalanceResponse(BaseModel):
|
|
"""Response model for prepaid card balance check"""
|
|
card: TPrepaidCardWithStats
|
|
can_deduct: bool
|
|
required_hours: Optional[Decimal] = None
|
|
message: Optional[str] = None
|
|
|
|
|
|
# ============================================================================
|
|
# REQUEST MODELS (for specific actions)
|
|
# ============================================================================
|
|
|
|
class TicketStatusUpdateRequest(BaseModel):
|
|
"""Request model for updating ticket status"""
|
|
status: TicketStatus
|
|
note: Optional[str] = Field(None, description="Note til audit log")
|
|
|
|
|
|
class WorklogBillingRequest(BaseModel):
|
|
"""Request model for marking worklogs as billable"""
|
|
worklog_ids: List[int] = Field(..., min_length=1)
|
|
note: Optional[str] = Field(None, description="Note til audit log")
|
|
|
|
|
|
class PrepaidCardTopUpRequest(BaseModel):
|
|
"""Request model for topping up prepaid card"""
|
|
hours: Decimal = Field(..., gt=0, description="Timer at tilføje")
|
|
note: Optional[str] = Field(None, description="Beskrivelse af top-up")
|
|
|
|
|
|
class PrepaidCardDeductRequest(BaseModel):
|
|
"""Request model for deducting hours from prepaid card"""
|
|
worklog_id: int = Field(..., gt=0, description="Worklog ID der skal trækkes fra kort")
|
|
hours: Decimal = Field(..., gt=0, description="Timer at trække")
|
|
|
|
|
|
# ============================================================================
|
|
# TICKET RELATIONS MODELS (Migration 026)
|
|
# ============================================================================
|
|
|
|
class TicketRelationType(str, Enum):
|
|
"""Ticket relation types"""
|
|
MERGED_INTO = "merged_into"
|
|
SPLIT_FROM = "split_from"
|
|
PARENT_OF = "parent_of"
|
|
CHILD_OF = "child_of"
|
|
RELATED_TO = "related_to"
|
|
|
|
|
|
class TTicketRelationBase(BaseModel):
|
|
"""Base model for ticket relation"""
|
|
ticket_id: int
|
|
related_ticket_id: int
|
|
relation_type: TicketRelationType
|
|
reason: Optional[str] = None
|
|
|
|
|
|
class TTicketRelationCreate(TTicketRelationBase):
|
|
"""Create ticket relation"""
|
|
pass
|
|
|
|
|
|
class TTicketRelation(TTicketRelationBase):
|
|
"""Full ticket relation model"""
|
|
id: int
|
|
created_by_user_id: Optional[int] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# CALENDAR EVENTS MODELS
|
|
# ============================================================================
|
|
|
|
class CalendarEventType(str, Enum):
|
|
"""Calendar event types"""
|
|
APPOINTMENT = "appointment"
|
|
DEADLINE = "deadline"
|
|
MILESTONE = "milestone"
|
|
REMINDER = "reminder"
|
|
FOLLOW_UP = "follow_up"
|
|
|
|
|
|
class CalendarEventStatus(str, Enum):
|
|
"""Calendar event status"""
|
|
PENDING = "pending"
|
|
CONFIRMED = "confirmed"
|
|
COMPLETED = "completed"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
class TTicketCalendarEventBase(BaseModel):
|
|
"""Base model for calendar event"""
|
|
ticket_id: int
|
|
title: str = Field(..., min_length=1, max_length=200)
|
|
description: Optional[str] = None
|
|
event_type: CalendarEventType = Field(default=CalendarEventType.APPOINTMENT)
|
|
event_date: date
|
|
event_time: Optional[str] = None
|
|
duration_minutes: Optional[int] = None
|
|
all_day: bool = False
|
|
status: CalendarEventStatus = Field(default=CalendarEventStatus.PENDING)
|
|
|
|
|
|
class TTicketCalendarEventCreate(TTicketCalendarEventBase):
|
|
"""Create calendar event"""
|
|
suggested_by_ai: bool = False
|
|
ai_confidence: Optional[Decimal] = None
|
|
ai_source_text: Optional[str] = None
|
|
|
|
|
|
class TTicketCalendarEvent(TTicketCalendarEventBase):
|
|
"""Full calendar event model"""
|
|
id: int
|
|
suggested_by_ai: bool = False
|
|
ai_confidence: Optional[Decimal] = None
|
|
ai_source_text: Optional[str] = None
|
|
created_by_user_id: Optional[int] = None
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
completed_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# TEMPLATES MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketTemplateBase(BaseModel):
|
|
"""Base model for template"""
|
|
name: str = Field(..., min_length=1, max_length=200)
|
|
description: Optional[str] = None
|
|
category: Optional[str] = None
|
|
subject_template: Optional[str] = Field(None, max_length=500)
|
|
body_template: str = Field(..., min_length=1)
|
|
available_placeholders: Optional[List[str]] = None
|
|
default_attachments: Optional[dict] = None
|
|
is_active: bool = True
|
|
requires_approval: bool = False
|
|
|
|
|
|
class TTicketTemplateCreate(TTicketTemplateBase):
|
|
"""Create template"""
|
|
pass
|
|
|
|
|
|
class TTicketTemplate(TTicketTemplateBase):
|
|
"""Full template model"""
|
|
id: int
|
|
created_by_user_id: Optional[int] = None
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
last_used_at: Optional[datetime] = None
|
|
usage_count: int = 0
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class TemplateRenderRequest(BaseModel):
|
|
"""Request to render template with data"""
|
|
template_id: int
|
|
ticket_id: int
|
|
custom_data: Optional[dict] = None
|
|
|
|
|
|
class TemplateRenderResponse(BaseModel):
|
|
"""Rendered template"""
|
|
subject: Optional[str] = None
|
|
body: str
|
|
placeholders_used: List[str]
|
|
|
|
|
|
# ============================================================================
|
|
# AI SUGGESTIONS MODELS
|
|
# ============================================================================
|
|
|
|
class AISuggestionType(str, Enum):
|
|
"""AI suggestion types"""
|
|
CONTACT_UPDATE = "contact_update"
|
|
NEW_CONTACT = "new_contact"
|
|
CATEGORY = "category"
|
|
TAG = "tag"
|
|
PRIORITY = "priority"
|
|
DEADLINE = "deadline"
|
|
CALENDAR_EVENT = "calendar_event"
|
|
TEMPLATE = "template"
|
|
MERGE = "merge"
|
|
RELATED_TICKET = "related_ticket"
|
|
|
|
|
|
class AISuggestionStatus(str, Enum):
|
|
"""AI suggestion status"""
|
|
PENDING = "pending"
|
|
ACCEPTED = "accepted"
|
|
REJECTED = "rejected"
|
|
AUTO_EXPIRED = "auto_expired"
|
|
|
|
|
|
class TTicketAISuggestionBase(BaseModel):
|
|
"""Base model for AI suggestion"""
|
|
ticket_id: int
|
|
suggestion_type: AISuggestionType
|
|
suggestion_data: dict # Struktureret data om forslaget
|
|
confidence: Optional[Decimal] = None
|
|
reasoning: Optional[str] = None
|
|
source_text: Optional[str] = None
|
|
source_comment_id: Optional[int] = None
|
|
|
|
|
|
class TTicketAISuggestionCreate(TTicketAISuggestionBase):
|
|
"""Create AI suggestion"""
|
|
expires_at: Optional[datetime] = None
|
|
|
|
|
|
class TTicketAISuggestion(TTicketAISuggestionBase):
|
|
"""Full AI suggestion model"""
|
|
id: int
|
|
status: AISuggestionStatus = Field(default=AISuggestionStatus.PENDING)
|
|
reviewed_by_user_id: Optional[int] = None
|
|
reviewed_at: Optional[datetime] = None
|
|
created_at: datetime
|
|
expires_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class AISuggestionReviewRequest(BaseModel):
|
|
"""Request to accept/reject AI suggestion"""
|
|
action: str = Field(..., pattern="^(accept|reject)$")
|
|
note: Optional[str] = None
|
|
|
|
|
|
# ============================================================================
|
|
# EMAIL METADATA MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketEmailMetadataBase(BaseModel):
|
|
"""Base model for email metadata"""
|
|
ticket_id: int
|
|
message_id: Optional[str] = None
|
|
in_reply_to: Optional[str] = None
|
|
references: Optional[str] = None
|
|
from_email: str
|
|
from_name: Optional[str] = None
|
|
from_signature: Optional[str] = None
|
|
|
|
|
|
class TTicketEmailMetadataCreate(TTicketEmailMetadataBase):
|
|
"""Create email metadata"""
|
|
matched_contact_id: Optional[int] = None
|
|
match_confidence: Optional[Decimal] = None
|
|
match_method: Optional[str] = None
|
|
suggested_contacts: Optional[dict] = None
|
|
extracted_phone: Optional[str] = None
|
|
extracted_address: Optional[str] = None
|
|
extracted_company: Optional[str] = None
|
|
extracted_title: Optional[str] = None
|
|
|
|
|
|
class TTicketEmailMetadata(TTicketEmailMetadataCreate):
|
|
"""Full email metadata model"""
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# AUDIT LOG MODELS
|
|
# ============================================================================
|
|
|
|
class TTicketAuditLog(BaseModel):
|
|
"""Audit log entry"""
|
|
id: int
|
|
ticket_id: int
|
|
action: str
|
|
field_name: Optional[str] = None
|
|
old_value: Optional[str] = None
|
|
new_value: Optional[str] = None
|
|
user_id: Optional[int] = None
|
|
performed_at: datetime
|
|
reason: Optional[str] = None
|
|
metadata: Optional[dict] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# EXTENDED REQUEST MODELS
|
|
# ============================================================================
|
|
|
|
class TicketMergeRequest(BaseModel):
|
|
"""Request to merge tickets"""
|
|
source_ticket_ids: List[int] = Field(..., min_length=1, description="Tickets at lægge sammen")
|
|
target_ticket_id: int = Field(..., description="Primær ticket der skal beholdes")
|
|
reason: Optional[str] = Field(None, description="Hvorfor lægges de sammen")
|
|
|
|
|
|
class TicketSplitRequest(BaseModel):
|
|
"""Request to split ticket"""
|
|
source_ticket_id: int = Field(..., description="Ticket at splitte")
|
|
comment_ids: List[int] = Field(..., min_length=1, description="Kommentarer til ny ticket")
|
|
new_subject: str = Field(..., min_length=1, description="Emne på ny ticket")
|
|
new_description: Optional[str] = Field(None, description="Beskrivelse på ny ticket")
|
|
reason: Optional[str] = Field(None, description="Hvorfor splittes ticketen")
|
|
|
|
|
|
class TicketDeadlineUpdateRequest(BaseModel):
|
|
"""Request to update ticket deadline"""
|
|
deadline: Optional[datetime] = None
|
|
reason: Optional[str] = None
|