diff --git a/app/core/config.py b/app/core/config.py
index 3c9c6bd..919d9d2 100644
--- a/app/core/config.py
+++ b/app/core/config.py
@@ -78,8 +78,8 @@ class Settings(BaseSettings):
WIKI_READ_ONLY: bool = True
# Ollama LLM
- OLLAMA_ENDPOINT: str = "http://localhost:11434"
- OLLAMA_MODEL: str = "llama3.2:3b"
+ OLLAMA_ENDPOINT: str = "http://172.16.31.195:11434"
+ OLLAMA_MODEL: str = "llama3.2"
# Email System Configuration
# IMAP Settings
diff --git a/app/models/schemas.py b/app/models/schemas.py
index dbe96dc..64d81fb 100644
--- a/app/models/schemas.py
+++ b/app/models/schemas.py
@@ -2,6 +2,7 @@
Pydantic Models and Schemas
"""
+from enum import Enum
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from datetime import datetime, date
@@ -303,3 +304,75 @@ class AnyDeskSessionUpdate(BaseModel):
status: str
ended_at: Optional[str] = None
duration_minutes: Optional[int] = None
+
+
+# ============================================================================
+# SAG MODULE (Cases) - QuickCreate and Priority Support
+# ============================================================================
+
+class SagPriority(str, Enum):
+ """Case priority levels matching database enum"""
+ LOW = "low"
+ NORMAL = "normal"
+ HIGH = "high"
+ URGENT = "urgent"
+
+
+class QuickCreateAnalysis(BaseModel):
+ """AI analysis result for QuickCreate feature"""
+ suggested_title: str
+ suggested_description: str
+ suggested_priority: SagPriority = SagPriority.NORMAL
+ suggested_customer_id: Optional[int] = None
+ suggested_customer_name: Optional[str] = None
+ suggested_technician_id: Optional[int] = None
+ suggested_technician_name: Optional[str] = None
+ suggested_group_id: Optional[int] = None
+ suggested_group_name: Optional[str] = None
+ suggested_tags: List[str] = []
+ hardware_references: List[dict] = [] # [{id, brand, model, serial_number}]
+ confidence: float = 0.0
+ ai_reasoning: Optional[str] = None # Debug info for low confidence
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class SagBase(BaseModel):
+ """Base schema for SAG (cases)"""
+ titel: str
+ beskrivelse: Optional[str] = None
+ priority: SagPriority = SagPriority.NORMAL
+ customer_id: Optional[int] = None
+ ansvarlig_bruger_id: Optional[int] = None
+ assigned_group_id: Optional[int] = None
+ deadline: Optional[datetime] = None
+
+
+class SagCreate(SagBase):
+ """Schema for creating a case"""
+ template_key: Optional[str] = None
+ tags: Optional[List[str]] = None
+
+
+class SagUpdate(BaseModel):
+ """Schema for updating a case"""
+ titel: Optional[str] = None
+ beskrivelse: Optional[str] = None
+ priority: Optional[SagPriority] = None
+ status: Optional[str] = None
+ ansvarlig_bruger_id: Optional[int] = None
+ assigned_group_id: Optional[int] = None
+ deadline: Optional[datetime] = None
+
+
+class Sag(SagBase):
+ """Full case schema"""
+ id: int
+ status: str
+ template_key: Optional[str] = None
+ created_by_user_id: int
+ created_at: datetime
+ updated_at: datetime
+ deleted_at: Optional[datetime] = None
+
+ model_config = ConfigDict(from_attributes=True)
diff --git a/app/modules/sag/backend/router.py b/app/modules/sag/backend/router.py
index a9bf9ba..5559324 100644
--- a/app/modules/sag/backend/router.py
+++ b/app/modules/sag/backend/router.py
@@ -10,9 +10,10 @@ from fastapi import APIRouter, HTTPException, Query, UploadFile, File, Request
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from app.core.database import execute_query, execute_query_single
-from app.models.schemas import TodoStep, TodoStepCreate, TodoStepUpdate
+from app.models.schemas import TodoStep, TodoStepCreate, TodoStepUpdate, QuickCreateAnalysis
from app.core.config import settings
from app.services.email_service import EmailService
+from app.services.case_analysis_service import CaseAnalysisService
try:
import extract_msg
@@ -99,6 +100,35 @@ def _validate_group_id(group_id: Optional[int], field_name: str = "assigned_grou
if not exists:
raise HTTPException(status_code=400, detail=f"Invalid {field_name}")
+# ============================================================================
+# QUICKCREATE AI ANALYSIS
+# ============================================================================
+
+class QuickCreateRequest(BaseModel):
+ text: str = Field(..., min_length=1, max_length=5000)
+ user_id: int
+
+
+@router.post("/sag/analyze-quick-create", response_model=QuickCreateAnalysis)
+async def analyze_quick_create(request: QuickCreateRequest):
+ """
+ Analyze case description text using AI for QuickCreate feature.
+ Returns structured suggestions for customer, technician, priority, tags, etc.
+ """
+ try:
+ logger.info(f"🔍 QuickCreate analysis requested by user {request.user_id}, text length: {len(request.text)}")
+
+ # Initialize service and analyze
+ service = CaseAnalysisService()
+ analysis = await service.analyze_case_text(request.text, request.user_id)
+
+ logger.info(f"âś… QuickCreate analysis complete: confidence={analysis.confidence}, priority={analysis.suggested_priority}")
+ return analysis
+
+ except Exception as e:
+ logger.error(f"❌ QuickCreate analysis failed: {str(e)}", exc_info=True)
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
+
# ============================================================================
# SAGER - CRUD Operations
# ============================================================================
diff --git a/app/modules/sag/templates/create.html b/app/modules/sag/templates/create.html
index 94fe9bf..616457b 100644
--- a/app/modules/sag/templates/create.html
+++ b/app/modules/sag/templates/create.html
@@ -309,8 +309,23 @@
let selectedContactsCompanies = {};
let customerSearchTimeout;
let contactSearchTimeout;
+ let successAlertTimeout;
let telefoniPrefill = { contactId: null, title: null, callId: null, customerId: null, description: null };
+ // Helper function to show success alert
+ function showSuccessAlert(message, duration = 3000) {
+ if (successAlertTimeout) {
+ clearTimeout(successAlertTimeout);
+ }
+ const successDiv = document.getElementById('success');
+ successDiv.classList.remove('d-none');
+ document.getElementById('success-text').textContent = message;
+ successAlertTimeout = setTimeout(() => {
+ successDiv.classList.add('d-none');
+ successAlertTimeout = null;
+ }, duration);
+ }
+
// --- Character Counter ---
const beskrInput = document.getElementById('beskrivelse');
if (beskrInput) {
@@ -415,12 +430,17 @@
}
// --- Selection Logic ---
- function selectCustomer(id, name) {
+ function selectCustomer(id, name, skipAlert = false) {
selectedCustomer = { id, name };
document.getElementById('customer_id').value = id;
document.getElementById('customerSearch').value = '';
document.getElementById('customerResults').classList.add('d-none');
renderSelections();
+
+ // Show notification
+ if (!skipAlert) {
+ showSuccessAlert(`Valgte firma: ${name}`);
+ }
}
function removeCustomer() {
@@ -430,7 +450,8 @@
}
async function selectContact(id, name) {
- if (!selectedContacts[id]) {
+ const isNewContact = !selectedContacts[id];
+ if (isNewContact) {
selectedContacts[id] = { id, name };
}
document.getElementById('contactSearch').value = '';
@@ -446,19 +467,24 @@
if (data.companies && data.companies.length === 1) {
const company = data.companies[0];
- if (!selectedCustomer) {
- selectCustomer(company.id, company.name);
-
- // Show brief notification
- const successDiv = document.getElementById('success');
- successDiv.classList.remove('d-none');
- document.getElementById('success-text').textContent = `Valgte automatisk kunde: ${company.name}`;
- setTimeout(() => successDiv.classList.add('d-none'), 3000);
+ if (!selectedCustomer && isNewContact) {
+ // Auto-select company silently, then show combined alert
+ selectCustomer(company.id, company.name, true);
+ showSuccessAlert(`Valgte kontakt: ${name} + firma: ${company.name}`, 4000);
+ } else if (isNewContact) {
+ // Just show contact alert
+ showSuccessAlert(`Valgte kontakt: ${name}`);
}
+ } else if (isNewContact) {
+ // No auto-select, just show contact alert
+ showSuccessAlert(`Valgte kontakt: ${name}`);
}
}
} catch (e) {
console.error("Auto-select company failed", e);
+ if (isNewContact) {
+ showSuccessAlert(`Valgte kontakt: ${name}`);
+ }
}
loadHardwareForContacts();
diff --git a/app/modules/sag/templates/detail.html b/app/modules/sag/templates/detail.html
index bf8ae51..44a4afe 100644
--- a/app/modules/sag/templates/detail.html
+++ b/app/modules/sag/templates/detail.html
@@ -81,12 +81,6 @@
border: 1px solid rgba(0,0,0,0.1);
}
- /* Wider tooltip for relation type explanations */
- .tooltip-wide .tooltip-inner {
- max-width: 400px;
- text-align: left;
- }
-
.tag-closed {
background-color: #e0e0e0;
color: #666;
@@ -959,15 +953,15 @@
-
- đź”— Relationer
-
+
đź”— Relationer
+
-
+ data-bs-placement="right"
+ title="Hvad betyder relationstyper?
Relateret til: Faglig kobling uden direkte afhængighed. Afledt af: Denne sag er opstået på baggrund af en anden sag. Årsag til: Denne sag er årsagen til en anden sag. Blokkerer: Arbejde i en sag stopper fremdrift i den anden.">
+