Compare commits

...

6 Commits

7 changed files with 89 additions and 21 deletions

View File

@ -9,7 +9,7 @@ from typing import List, Dict, Optional
from datetime import datetime, date, timedelta
from decimal import Decimal
from pathlib import Path
from app.core.database import execute_query, execute_insert, execute_update, execute_query_single
from app.core.database import execute_query, execute_insert, execute_update, execute_query_single, table_has_column
from app.core.config import settings
from app.services.economic_service import get_economic_service
from app.services.ollama_service import ollama_service
@ -710,8 +710,29 @@ async def list_supplier_invoices(
params.append(vendor_id)
if sag_id:
query += " AND si.sag_id = %s"
params.append(sag_id)
if table_has_column("supplier_invoices", "sag_id"):
query += " AND si.sag_id = %s"
params.append(sag_id)
elif (
table_has_column("supplier_invoice_relations", "supplier_invoice_id")
and table_has_column("supplier_invoice_relations", "relation_type")
and table_has_column("supplier_invoice_relations", "relation_id")
):
query += """
AND EXISTS (
SELECT 1
FROM supplier_invoice_relations sir
WHERE sir.supplier_invoice_id = si.id
AND sir.relation_type = 'sag'
AND sir.relation_id = %s
)
"""
params.append(sag_id)
else:
logger.warning(
"⚠️ supplier invoice sag filter requested, but no schema link available (sag_id column/relation table missing)"
)
query += " AND 1 = 0"
if overdue_only:
query += " AND si.due_date < CURRENT_DATE AND si.paid_date IS NULL"

View File

@ -11,7 +11,7 @@ from datetime import datetime, timedelta, timezone
from typing import List, Optional, Dict
from uuid import uuid4
from fastapi import APIRouter, HTTPException, Query, UploadFile, File, Request, Form, Response
from fastapi import APIRouter, HTTPException, Query, UploadFile, File, Request, Form, Response, Body
from fastapi.responses import FileResponse, HTMLResponse
from pydantic import BaseModel, Field
from app.core.database import execute_query, execute_query_single, table_has_column
@ -1273,7 +1273,7 @@ async def delete_todo_step(step_id: int):
raise HTTPException(status_code=500, detail="Failed to delete todo step")
@router.patch("/sag/{sag_id:int}")
async def update_sag(sag_id: int, updates: dict):
async def update_sag(sag_id: int, updates: dict = Body(...)):
"""Update a case."""
try:
# Check if case exists
@ -2892,7 +2892,7 @@ async def get_sale_item(sag_id: int, item_id: int):
@router.patch("/sag/{sag_id}/sale-items/{item_id}")
async def update_sale_item(sag_id: int, item_id: int, updates: dict):
async def update_sale_item(sag_id: int, item_id: int, updates: dict = Body(...)):
"""Update a sale item for a case."""
try:
check = execute_query(

View File

@ -3461,7 +3461,7 @@
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="mb-0 text-secondary" style="font-size:0.8rem;">Status</label>
<select id="topbarStatusSelect" class="form-select form-select-sm bg-light" style="width: 62%;">
<select id="topbarStatusSelect" class="form-select form-select-sm bg-light" style="width: 62%;" onchange="saveCaseStatusFromTopbar()">
{% for st in status_options %}
<option value="{{ st }}" {% if (case.status or '')|lower == st|lower %}selected{% endif %}>{{ st|capitalize }}</option>
{% endfor %}
@ -3708,11 +3708,6 @@
});
};
bindChange('topbarStatusSelect', async (el) => {
await patchCase({ status: el.value || 'åben' });
location.reload();
});
bindChange('topbarTypeSelect', async (el) => {
await patchCase({ type: String(el.value || 'ticket').toLowerCase() });
location.reload();

View File

@ -1267,7 +1267,7 @@ window.addEventListener('unhandledrejection', function(event) {
<script src="/static/js/tag-picker.js?v=2.2"></script>
<script src="/static/js/task-template-selector.js?v=1.1"></script>
<script src="/static/js/notifications.js?v=1.0"></script>
<script src="/static/js/telefoni.js?v=2.2"></script>
<script src="/static/js/telefoni.js?v=2.3"></script>
<script src="/static/js/sms.js?v=1.0"></script>
<script src="/static/js/bug-report.js?v=1.0"></script>
<script src="/static/js/bottom-bar.js?v=2.23"></script>

View File

@ -536,12 +536,18 @@ if __name__ == "__main__":
log_level="info"
)
else:
api_workers_raw = os.getenv("API_WORKERS", "1").strip()
try:
api_workers = max(1, int(api_workers_raw))
except ValueError:
api_workers = 1
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=False,
workers=2,
workers=api_workers,
timeout_keep_alive=65,
access_log=True,
log_level="info"

View File

@ -0,0 +1,48 @@
-- Migration 190: Align sag_sager status constraint with current case status model
-- Fixes PATCH /api/v1/sag/{id} failures when using statuses beyond 'åben'/'lukket'.
DO $$
DECLARE
constraint_row RECORD;
BEGIN
-- Drop legacy check constraints on sag_sager.status regardless of their generated name.
FOR constraint_row IN
SELECT c.conname
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = 'public'
AND t.relname = 'sag_sager'
AND c.contype = 'c'
AND pg_get_constraintdef(c.oid) ILIKE '%status%'
LOOP
EXECUTE format('ALTER TABLE public.sag_sager DROP CONSTRAINT IF EXISTS %I', constraint_row.conname);
END LOOP;
END $$;
UPDATE public.sag_sager
SET status = CASE
WHEN lower(trim(status)) IN ('aaben', 'open') THEN 'åben'
WHEN lower(trim(status)) IN ('i_gang', 'in_progress', 'under behandling') THEN 'under behandling'
WHEN lower(trim(status)) IN ('on_hold', 'waiting', 'afventer') THEN 'afventer'
WHEN lower(trim(status)) IN ('resolved', 'løst') THEN 'løst'
WHEN lower(trim(status)) IN ('closed', 'afsluttet', 'lukket') THEN 'lukket'
ELSE status
END;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = 'public'
AND t.relname = 'sag_sager'
AND c.conname = 'sag_sager_status_check'
) THEN
ALTER TABLE public.sag_sager
ADD CONSTRAINT sag_sager_status_check
CHECK (status IN ('åben', 'under behandling', 'afventer', 'løst', 'lukket'));
END IF;
END $$;

View File

@ -226,18 +226,16 @@
}
function connect() {
if (ws && ws.readyState === WebSocket.OPEN) {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
return;
}
const token = getToken();
if (!token) {
scheduleReconnect();
return;
}
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const url = `${proto}://${window.location.host}/api/v1/telefoni/ws?token=${encodeURIComponent(token)}`;
// Fallback to cookie-auth websocket when token is HttpOnly and cannot be read by JS.
const url = token
? `${proto}://${window.location.host}/api/v1/telefoni/ws?token=${encodeURIComponent(token)}`
: `${proto}://${window.location.host}/api/v1/telefoni/ws`;
ws = new WebSocket(url);
ws.onopen = () => console.log('📞 Telefoni WS connected');