feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
"""
|
|
|
|
|
Main API Router for Time Tracking Module
|
|
|
|
|
=========================================
|
|
|
|
|
|
|
|
|
|
Samler alle endpoints for modulet.
|
|
|
|
|
Isoleret routing uden påvirkning af existing Hub endpoints.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import logging
|
2026-01-02 12:45:25 +01:00
|
|
|
from typing import Optional, List, Dict, Any
|
2026-01-10 21:09:29 +01:00
|
|
|
from datetime import datetime
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
|
2026-01-02 12:49:19 +01:00
|
|
|
from fastapi import APIRouter, HTTPException, Depends, Body
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
|
2025-12-16 22:07:20 +01:00
|
|
|
from app.core.database import execute_query, execute_update, execute_query_single
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
from app.timetracking.backend.models import (
|
|
|
|
|
TModuleSyncStats,
|
|
|
|
|
TModuleApprovalStats,
|
|
|
|
|
TModuleWizardNextEntry,
|
|
|
|
|
TModuleWizardProgress,
|
|
|
|
|
TModuleTimeApproval,
|
|
|
|
|
TModuleTimeWithContext,
|
|
|
|
|
TModuleOrder,
|
|
|
|
|
TModuleOrderWithLines,
|
|
|
|
|
TModuleEconomicExportRequest,
|
|
|
|
|
TModuleEconomicExportResult,
|
|
|
|
|
TModuleMetadata,
|
|
|
|
|
TModuleUninstallRequest,
|
2025-12-23 14:39:57 +01:00
|
|
|
TModuleUninstallResult,
|
|
|
|
|
TModuleBulkRateUpdate
|
2025-12-16 22:07:20 +01:00
|
|
|
)
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
from app.timetracking.backend.vtiger_sync import vtiger_service
|
|
|
|
|
from app.timetracking.backend.wizard import wizard
|
|
|
|
|
from app.timetracking.backend.order_service import order_service
|
|
|
|
|
from app.timetracking.backend.economic_export import economic_service
|
|
|
|
|
from app.timetracking.backend.audit import audit
|
2026-01-10 01:37:08 +01:00
|
|
|
from app.services.customer_consistency import CustomerConsistencyService
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2025-12-16 22:07:20 +01:00
|
|
|
router = APIRouter(prefix="/timetracking")
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# SYNC ENDPOINTS
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
@router.post("/sync", response_model=TModuleSyncStats, tags=["Sync"])
|
2025-12-10 18:29:13 +01:00
|
|
|
async def sync_from_vtiger(
|
|
|
|
|
user_id: Optional[int] = None,
|
|
|
|
|
fetch_comments: bool = False
|
|
|
|
|
):
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
"""
|
|
|
|
|
🔍 Synkroniser data fra vTiger (READ-ONLY).
|
|
|
|
|
|
|
|
|
|
Henter:
|
|
|
|
|
- Accounts (kunder)
|
|
|
|
|
- HelpDesk (cases)
|
|
|
|
|
- ModComments (tidsregistreringer)
|
|
|
|
|
|
|
|
|
|
Gemmes i tmodule_* tabeller (isoleret).
|
2025-12-10 18:29:13 +01:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_id: ID på bruger der kører sync
|
|
|
|
|
fetch_comments: Hent også interne kommentarer (langsomt - ~0.4s pr case)
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
logger.info("🚀 Starting vTiger sync...")
|
2025-12-10 18:29:13 +01:00
|
|
|
result = await vtiger_service.full_sync(user_id=user_id, fetch_comments=fetch_comments)
|
2026-01-05 10:56:32 +01:00
|
|
|
|
|
|
|
|
# Auto-link customers after sync to update links based on fresh vTiger data
|
|
|
|
|
logger.info("🔗 Auto-linking customers after sync...")
|
|
|
|
|
try:
|
|
|
|
|
link_query = "SELECT * FROM link_tmodule_customers_to_hub()"
|
|
|
|
|
link_results = execute_query(link_query)
|
|
|
|
|
logger.info(f"✅ Linked {len(link_results)} customers automatically")
|
|
|
|
|
except Exception as link_error:
|
|
|
|
|
logger.warning(f"⚠️ Customer linking failed (non-critical): {link_error}")
|
|
|
|
|
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Sync failed: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
@router.post("/sync/case/{case_id}/comments", tags=["Sync"])
|
|
|
|
|
async def sync_case_comments(case_id: int):
|
|
|
|
|
"""
|
|
|
|
|
🔍 Synkroniser kommentarer for en specifik case fra vTiger.
|
|
|
|
|
|
|
|
|
|
Bruges til on-demand opdatering når man ser på en case i wizard.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Hent case fra database
|
2025-12-16 15:36:11 +01:00
|
|
|
case = execute_query_single(
|
2025-12-10 18:29:13 +01:00
|
|
|
"SELECT vtiger_id FROM tmodule_cases WHERE id = %s",
|
2025-12-16 15:36:11 +01:00
|
|
|
(case_id,))
|
2025-12-10 18:29:13 +01:00
|
|
|
|
|
|
|
|
if not case:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
|
|
|
|
|
|
# Sync comments
|
|
|
|
|
result = await vtiger_service.sync_case_comments(case['vtiger_id'])
|
|
|
|
|
|
|
|
|
|
if not result['success']:
|
|
|
|
|
raise HTTPException(status_code=500, detail=result.get('error', 'Failed to sync comments'))
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Failed to sync comments for case {case_id}: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
@router.get("/sync/test-connection", tags=["Sync"])
|
|
|
|
|
async def test_vtiger_connection():
|
|
|
|
|
"""Test forbindelse til vTiger"""
|
|
|
|
|
try:
|
|
|
|
|
is_connected = await vtiger_service.test_connection()
|
|
|
|
|
return {
|
|
|
|
|
"connected": is_connected,
|
|
|
|
|
"service": "vTiger CRM",
|
|
|
|
|
"read_only": vtiger_service.read_only,
|
|
|
|
|
"dry_run": vtiger_service.dry_run
|
|
|
|
|
}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-01-05 17:06:44 +01:00
|
|
|
@router.post("/sync/fix-empty-case-titles", tags=["Sync"])
|
|
|
|
|
async def fix_empty_case_titles():
|
|
|
|
|
"""
|
|
|
|
|
🔧 Opdater tomme case titles fra vtiger_data.
|
|
|
|
|
|
|
|
|
|
Henter titles fra vtiger_data JSON for cases der har tom eller generisk title.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
logger.info("🔧 Fixing empty case titles...")
|
|
|
|
|
|
|
|
|
|
# Find cases med tomme eller generiske titles
|
|
|
|
|
cases_query = """
|
|
|
|
|
SELECT id, vtiger_id, title,
|
|
|
|
|
vtiger_data->>'title' as vtiger_title,
|
|
|
|
|
vtiger_data->>'ticket_title' as vtiger_ticket_title
|
|
|
|
|
FROM tmodule_cases
|
|
|
|
|
WHERE title IS NULL
|
|
|
|
|
OR TRIM(title) = ''
|
|
|
|
|
OR LOWER(title) IN ('ingen titel', 'no title', 'none')
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
cases = execute_query(cases_query)
|
|
|
|
|
|
|
|
|
|
if not cases:
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"message": "No cases with empty titles found",
|
|
|
|
|
"updated": 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updated_count = 0
|
|
|
|
|
for case in cases:
|
|
|
|
|
# Prioriter title fra vtiger_data
|
|
|
|
|
new_title = case.get('vtiger_title') or case.get('vtiger_ticket_title')
|
|
|
|
|
|
|
|
|
|
if new_title and new_title.strip():
|
|
|
|
|
execute_query(
|
|
|
|
|
"UPDATE tmodule_cases SET title = %s WHERE id = %s",
|
|
|
|
|
(new_title, case['id'])
|
|
|
|
|
)
|
|
|
|
|
updated_count += 1
|
|
|
|
|
logger.info(f"✅ Updated case {case['vtiger_id']}: '{new_title}'")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"message": f"Updated {updated_count} cases",
|
|
|
|
|
"total_checked": len(cases),
|
|
|
|
|
"updated": updated_count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Fix empty titles failed: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-01-05 14:15:04 +01:00
|
|
|
@router.post("/sync/manual-link-customer", tags=["Sync"])
|
|
|
|
|
async def manual_link_customer(tmodule_customer_id: int, hub_customer_id: int):
|
|
|
|
|
"""
|
|
|
|
|
🔗 Manually link en tmodule_customer til en Hub customer.
|
|
|
|
|
|
|
|
|
|
Brug dette når auto-linking fejler pga. navn-forskelle eller andre edge cases.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
logger.info(f"🔗 Manual linking tmodule {tmodule_customer_id} → Hub {hub_customer_id}")
|
|
|
|
|
|
|
|
|
|
# Get Hub customer's economic number
|
|
|
|
|
hub_query = "SELECT economic_customer_number FROM customers WHERE id = %s"
|
|
|
|
|
hub_result = execute_query(hub_query, (hub_customer_id,))
|
|
|
|
|
|
|
|
|
|
if not hub_result:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Hub customer {hub_customer_id} not found")
|
|
|
|
|
|
|
|
|
|
economic_number = hub_result[0]['economic_customer_number']
|
|
|
|
|
|
|
|
|
|
# Update tmodule_customer
|
|
|
|
|
update_query = """
|
|
|
|
|
UPDATE tmodule_customers
|
|
|
|
|
SET hub_customer_id = %s,
|
|
|
|
|
economic_customer_number = %s,
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
RETURNING id, name, hub_customer_id, economic_customer_number
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
result = execute_query(update_query, (hub_customer_id, economic_number, tmodule_customer_id))
|
|
|
|
|
|
|
|
|
|
if not result:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"tmodule_customer {tmodule_customer_id} not found")
|
|
|
|
|
|
|
|
|
|
logger.info(f"✅ Manual link complete: {result[0]}")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"linked_customer": result[0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Manual linking failed: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-01-05 10:56:32 +01:00
|
|
|
@router.post("/sync/relink-customers", tags=["Sync"])
|
|
|
|
|
async def relink_customers():
|
|
|
|
|
"""
|
|
|
|
|
🔗 Re-link alle tmodule_customers til Hub customers.
|
|
|
|
|
|
|
|
|
|
Bruger linking funktion der prioriterer:
|
|
|
|
|
1. economic_customer_number match (mest præcis)
|
|
|
|
|
2. Navn match (fallback)
|
|
|
|
|
|
|
|
|
|
Kør efter vTiger sync for at opdatere kunde-links baseret på
|
|
|
|
|
nyeste data fra vTiger (cf_854 economic numbers).
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
logger.info("🔗 Starting customer re-linking...")
|
|
|
|
|
|
|
|
|
|
# Call database function
|
|
|
|
|
query = "SELECT * FROM link_tmodule_customers_to_hub()"
|
|
|
|
|
results = execute_query(query)
|
|
|
|
|
|
|
|
|
|
# Count by action type
|
|
|
|
|
stats = {
|
|
|
|
|
"economic_matches": 0,
|
|
|
|
|
"name_matches": 0,
|
|
|
|
|
"total_linked": len(results)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for result in results:
|
|
|
|
|
if result['action'] == 'economic_number_match':
|
|
|
|
|
stats["economic_matches"] += 1
|
|
|
|
|
elif result['action'] == 'name_match':
|
|
|
|
|
stats["name_matches"] += 1
|
|
|
|
|
|
|
|
|
|
logger.info(f"✅ Customer linking complete: {stats}")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"stats": stats,
|
|
|
|
|
"linked_customers": results
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Customer linking failed: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-01-02 00:01:12 +01:00
|
|
|
@router.get("/debug/raw-stats", tags=["Debug"])
|
|
|
|
|
async def get_debug_raw_stats():
|
|
|
|
|
"""
|
|
|
|
|
🔍 DEBUG: Vis rå statistik fra databasen uden filtering.
|
|
|
|
|
|
|
|
|
|
Bruges til at diagnosticere hvorfor timelogs ikke vises.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Total counts without any filtering
|
|
|
|
|
total_query = """
|
|
|
|
|
SELECT
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_customers) as total_customers,
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_cases) as total_cases,
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_times) as total_times,
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_times WHERE billable = true) as billable_times,
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_times WHERE status = 'pending') as pending_times,
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_times WHERE vtiger_data->>'cf_timelog_invoiced' = '0') as not_invoiced_times,
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_times WHERE vtiger_data->>'cf_timelog_invoiced' IS NULL) as null_invoiced_times,
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_times WHERE billable = true AND status = 'pending') as billable_pending,
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_times WHERE billable = true AND status = 'pending' AND vtiger_data->>'cf_timelog_invoiced' = '0') as filtered_pending
|
|
|
|
|
"""
|
|
|
|
|
totals = execute_query_single(total_query)
|
|
|
|
|
|
|
|
|
|
# Sample timelogs to see actual data
|
|
|
|
|
sample_query = """
|
|
|
|
|
SELECT
|
|
|
|
|
id, vtiger_id, description, worked_date, original_hours,
|
|
|
|
|
status, billable,
|
|
|
|
|
vtiger_data->>'cf_timelog_invoiced' as cf_timelog_invoiced,
|
|
|
|
|
vtiger_data->>'isbillable' as is_billable_field,
|
|
|
|
|
customer_id, case_id
|
|
|
|
|
FROM tmodule_times
|
|
|
|
|
ORDER BY worked_date DESC
|
|
|
|
|
LIMIT 10
|
|
|
|
|
"""
|
|
|
|
|
samples = execute_query(sample_query)
|
|
|
|
|
|
|
|
|
|
# Check what invoice statuses actually exist
|
|
|
|
|
invoice_status_query = """
|
|
|
|
|
SELECT
|
|
|
|
|
vtiger_data->>'cf_timelog_invoiced' as invoice_status,
|
|
|
|
|
COUNT(*) as count
|
|
|
|
|
FROM tmodule_times
|
|
|
|
|
GROUP BY vtiger_data->>'cf_timelog_invoiced'
|
|
|
|
|
ORDER BY count DESC
|
|
|
|
|
"""
|
|
|
|
|
invoice_statuses = execute_query(invoice_status_query)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"totals": totals,
|
|
|
|
|
"sample_timelogs": samples,
|
|
|
|
|
"invoice_statuses": invoice_statuses,
|
|
|
|
|
"explanation": {
|
|
|
|
|
"issue": "If not_invoiced_times is 0 but total_times > 0, then the cf_timelog_invoiced field is not '0' in vTiger",
|
|
|
|
|
"solution": "The SQL views filter on cf_timelog_invoiced = '0' but vTiger might use different values",
|
|
|
|
|
"check": "Look at invoice_statuses to see what values actually exist"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Debug query failed: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
# ============================================================================
|
|
|
|
|
# WIZARD / APPROVAL ENDPOINTS
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
@router.get("/wizard/stats", response_model=List[TModuleApprovalStats], tags=["Wizard"])
|
|
|
|
|
async def get_all_customer_stats():
|
|
|
|
|
"""Hent approval statistik for alle kunder"""
|
|
|
|
|
try:
|
|
|
|
|
return wizard.get_all_customers_stats()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/wizard/next", response_model=TModuleWizardNextEntry, tags=["Wizard"])
|
2025-12-10 18:29:13 +01:00
|
|
|
async def get_next_pending_entry(
|
|
|
|
|
customer_id: Optional[int] = None,
|
|
|
|
|
exclude_time_card: bool = True
|
|
|
|
|
):
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
"""
|
|
|
|
|
Hent næste pending tidsregistrering til godkendelse.
|
|
|
|
|
|
|
|
|
|
Query params:
|
2025-12-10 18:29:13 +01:00
|
|
|
- customer_id: Filtrer til specifik kunde (optional)
|
|
|
|
|
- exclude_time_card: Ekskluder klippekort-kunder (default: true)
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
"""
|
|
|
|
|
try:
|
2025-12-10 18:29:13 +01:00
|
|
|
return wizard.get_next_pending_entry(
|
|
|
|
|
customer_id=customer_id,
|
|
|
|
|
exclude_time_card=exclude_time_card
|
|
|
|
|
)
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
@router.post("/wizard/approve/{time_id}", response_model=TModuleTimeWithContext, tags=["Wizard"])
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
async def approve_time_entry(
|
2025-12-10 18:29:13 +01:00
|
|
|
time_id: int,
|
2026-01-02 12:49:19 +01:00
|
|
|
request: Dict[str, Any] = Body(...),
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Godkend en tidsregistrering.
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
Path params:
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
- time_id: ID på tidsregistreringen
|
2025-12-10 18:29:13 +01:00
|
|
|
|
2026-01-02 12:45:25 +01:00
|
|
|
Body:
|
|
|
|
|
- billable_hours: Timer efter godkendelse (optional)
|
|
|
|
|
- hourly_rate: Timepris i DKK (optional)
|
|
|
|
|
- is_travel: True hvis kørsel (optional)
|
|
|
|
|
- approval_note: Godkendelsesnote (optional)
|
|
|
|
|
- rounding_method: "up", "down", "nearest" (optional)
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
"""
|
|
|
|
|
try:
|
2025-12-10 18:29:13 +01:00
|
|
|
from app.core.config import settings
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
2026-01-10 21:09:29 +01:00
|
|
|
# SPECIAL HANDLER FOR HUB WORKLOGS (Negative IDs)
|
|
|
|
|
if time_id < 0:
|
|
|
|
|
worklog_id = abs(time_id)
|
|
|
|
|
logger.info(f"🔄 Approving Hub Worklog {worklog_id}")
|
|
|
|
|
|
|
|
|
|
w_entry = execute_query_single("SELECT * FROM tticket_worklog WHERE id = %s", (worklog_id,))
|
|
|
|
|
if not w_entry:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Worklog not found")
|
|
|
|
|
|
|
|
|
|
billable_hours = request.get('billable_hours')
|
|
|
|
|
approved_hours = Decimal(str(billable_hours)) if billable_hours is not None else Decimal(str(w_entry['hours']))
|
|
|
|
|
|
|
|
|
|
is_billable = request.get('billable', True)
|
|
|
|
|
new_billing = 'invoice' if is_billable else 'internal'
|
|
|
|
|
|
|
|
|
|
execute_query("""
|
|
|
|
|
UPDATE tticket_worklog
|
|
|
|
|
SET hours = %s, billing_method = %s, status = 'billable'
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
""", (approved_hours, new_billing, worklog_id))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"id": time_id,
|
|
|
|
|
"worked_date": w_entry['work_date'],
|
|
|
|
|
"original_hours": w_entry['hours'],
|
|
|
|
|
"status": "approved",
|
|
|
|
|
# Mock fields for schema validation
|
|
|
|
|
"customer_id": 0,
|
|
|
|
|
"case_id": 0,
|
|
|
|
|
"description": w_entry['description'],
|
|
|
|
|
"case_title": "Ticket Worklog",
|
|
|
|
|
"customer_name": "Hub Customer",
|
|
|
|
|
"created_at": w_entry['created_at'],
|
|
|
|
|
"last_synced_at": datetime.now(),
|
|
|
|
|
"approved_hours": approved_hours
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
# Hent timelog
|
|
|
|
|
query = """
|
|
|
|
|
SELECT t.*, c.title as case_title, c.status as case_status,
|
|
|
|
|
cust.name as customer_name, cust.hourly_rate as customer_rate
|
|
|
|
|
FROM tmodule_times t
|
|
|
|
|
JOIN tmodule_cases c ON t.case_id = c.id
|
|
|
|
|
JOIN tmodule_customers cust ON t.customer_id = cust.id
|
|
|
|
|
WHERE t.id = %s
|
|
|
|
|
"""
|
2025-12-16 15:36:11 +01:00
|
|
|
entry = execute_query_single(query, (time_id,))
|
2025-12-10 18:29:13 +01:00
|
|
|
|
|
|
|
|
if not entry:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Time entry not found")
|
|
|
|
|
|
|
|
|
|
# Beregn approved_hours
|
2026-01-02 12:45:25 +01:00
|
|
|
billable_hours = request.get('billable_hours')
|
|
|
|
|
rounding_method = request.get('rounding_method')
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
if billable_hours is None:
|
|
|
|
|
approved_hours = Decimal(str(entry['original_hours']))
|
|
|
|
|
|
|
|
|
|
# Auto-afrund hvis enabled
|
|
|
|
|
if settings.TIMETRACKING_AUTO_ROUND:
|
|
|
|
|
increment = Decimal(str(settings.TIMETRACKING_ROUND_INCREMENT))
|
|
|
|
|
method = rounding_method or settings.TIMETRACKING_ROUND_METHOD
|
|
|
|
|
|
|
|
|
|
if method == "up":
|
|
|
|
|
approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_UP') * increment
|
|
|
|
|
elif method == "down":
|
|
|
|
|
approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_DOWN') * increment
|
|
|
|
|
else:
|
|
|
|
|
approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_HALF_UP') * increment
|
2026-01-02 12:45:25 +01:00
|
|
|
|
|
|
|
|
rounded_to = increment
|
|
|
|
|
else:
|
|
|
|
|
rounded_to = None
|
2025-12-10 18:29:13 +01:00
|
|
|
else:
|
|
|
|
|
approved_hours = Decimal(str(billable_hours))
|
2026-01-02 12:45:25 +01:00
|
|
|
rounded_to = None
|
2025-12-10 18:29:13 +01:00
|
|
|
|
2026-01-10 01:37:08 +01:00
|
|
|
# Note: hourly_rate is stored on customer level (tmodule_customers.hourly_rate), not on time entries
|
|
|
|
|
# Frontend sends it for calculation display but we don't store it per time entry
|
2025-12-10 18:29:13 +01:00
|
|
|
|
2026-01-02 12:45:25 +01:00
|
|
|
# Godkend med alle felter
|
2026-01-10 01:37:08 +01:00
|
|
|
logger.info(f"🔍 Creating approval for time_id={time_id}: approved_hours={approved_hours}, rounded_to={rounded_to}, is_travel={request.get('is_travel', False)}, billable={request.get('billable', True)}")
|
2026-01-02 12:58:53 +01:00
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
approval = TModuleTimeApproval(
|
|
|
|
|
time_id=time_id,
|
2026-01-02 12:51:12 +01:00
|
|
|
approved_hours=approved_hours,
|
|
|
|
|
rounded_to=rounded_to,
|
2026-01-02 12:45:25 +01:00
|
|
|
approval_note=request.get('approval_note'),
|
2026-01-10 01:37:08 +01:00
|
|
|
billable=request.get('billable', True), # Accept from request, default til fakturerbar
|
2026-01-02 12:45:25 +01:00
|
|
|
is_travel=request.get('is_travel', False)
|
2025-12-10 18:29:13 +01:00
|
|
|
)
|
|
|
|
|
|
2026-01-02 12:58:53 +01:00
|
|
|
logger.info(f"✅ Approval object created successfully")
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
return wizard.approve_time_entry(approval, user_id=user_id)
|
2025-12-10 18:29:13 +01:00
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
except Exception as e:
|
2026-01-02 12:58:53 +01:00
|
|
|
logger.error(f"❌ Error approving entry {time_id}: {e}", exc_info=True)
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/wizard/reject/{time_id}", response_model=TModuleTimeWithContext, tags=["Wizard"])
|
|
|
|
|
async def reject_time_entry(
|
|
|
|
|
time_id: int,
|
2026-01-10 21:09:29 +01:00
|
|
|
request: Dict[str, Any] = Body(None), # Allow body
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
reason: Optional[str] = None,
|
|
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
):
|
|
|
|
|
"""Afvis en tidsregistrering"""
|
|
|
|
|
try:
|
2026-01-10 21:09:29 +01:00
|
|
|
# Handle body extraction if reason is missing from query
|
|
|
|
|
if not reason and request and 'rejection_note' in request:
|
|
|
|
|
reason = request['rejection_note']
|
|
|
|
|
|
|
|
|
|
if time_id < 0:
|
|
|
|
|
worklog_id = abs(time_id)
|
|
|
|
|
|
|
|
|
|
# Retrieve to confirm existence
|
|
|
|
|
w = execute_query_single("SELECT * FROM tticket_worklog WHERE id = %s", (worklog_id,))
|
|
|
|
|
if not w:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Entry not found")
|
|
|
|
|
|
|
|
|
|
execute_query("UPDATE tticket_worklog SET status = 'rejected' WHERE id = %s", (worklog_id,))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"id": time_id,
|
|
|
|
|
"status": "rejected",
|
|
|
|
|
"original_hours": w['hours'],
|
|
|
|
|
"worked_date": w['work_date'],
|
|
|
|
|
# Mock fields for schema validation
|
|
|
|
|
"customer_id": 0,
|
|
|
|
|
"case_id": 0,
|
|
|
|
|
"description": w.get('description', ''),
|
|
|
|
|
"case_title": "Ticket Worklog",
|
|
|
|
|
"customer_name": "Hub Customer",
|
|
|
|
|
"created_at": w['created_at'],
|
|
|
|
|
"last_synced_at": datetime.now(),
|
|
|
|
|
"billable": False
|
|
|
|
|
}
|
|
|
|
|
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
return wizard.reject_time_entry(time_id, reason=reason, user_id=user_id)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-01-10 21:09:29 +01:00
|
|
|
from app.timetracking.backend.models import TModuleWizardEditRequest
|
|
|
|
|
|
|
|
|
|
@router.patch("/wizard/entry/{time_id}", response_model=TModuleTimeWithContext, tags=["Wizard"])
|
|
|
|
|
async def update_entry_details(
|
|
|
|
|
time_id: int,
|
|
|
|
|
request: TModuleWizardEditRequest
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Opdater detaljer på en tidsregistrering (før godkendelse).
|
|
|
|
|
Tillader ændring af beskrivelse, antal timer og faktureringsmetode.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
|
|
# 1. Handling Hub Worklogs (Negative IDs)
|
|
|
|
|
if time_id < 0:
|
|
|
|
|
worklog_id = abs(time_id)
|
|
|
|
|
w = execute_query_single("SELECT * FROM tticket_worklog WHERE id = %s", (worklog_id,))
|
|
|
|
|
if not w:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Worklog not found")
|
|
|
|
|
|
|
|
|
|
updates = []
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
if request.description is not None:
|
|
|
|
|
updates.append("description = %s")
|
|
|
|
|
params.append(request.description)
|
|
|
|
|
|
|
|
|
|
if request.original_hours is not None:
|
|
|
|
|
updates.append("hours = %s")
|
|
|
|
|
params.append(request.original_hours)
|
|
|
|
|
|
|
|
|
|
if request.billing_method is not None:
|
|
|
|
|
updates.append("billing_method = %s")
|
|
|
|
|
params.append(request.billing_method)
|
|
|
|
|
|
|
|
|
|
if updates:
|
|
|
|
|
params.append(worklog_id)
|
|
|
|
|
execute_query(f"UPDATE tticket_worklog SET {', '.join(updates)} WHERE id = %s", tuple(params))
|
|
|
|
|
w = execute_query_single("SELECT * FROM tticket_worklog WHERE id = %s", (worklog_id,))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"id": time_id,
|
|
|
|
|
"worked_date": w['work_date'],
|
|
|
|
|
"original_hours": w['hours'],
|
|
|
|
|
"status": "pending", # Always return as pending/draft context here
|
|
|
|
|
"description": w['description'],
|
|
|
|
|
"customer_id": 0,
|
|
|
|
|
"case_id": 0,
|
|
|
|
|
"case_title": "Updated",
|
|
|
|
|
"customer_name": "Hub Customer",
|
|
|
|
|
"created_at": w['created_at'],
|
|
|
|
|
"last_synced_at": datetime.now(),
|
|
|
|
|
"billable": True
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 2. Handling Module Times (Positive IDs)
|
|
|
|
|
else:
|
|
|
|
|
t = execute_query_single("SELECT * FROM tmodule_times WHERE id = %s", (time_id,))
|
|
|
|
|
if not t:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Time entry not found")
|
|
|
|
|
|
|
|
|
|
updates = []
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
if request.description is not None:
|
|
|
|
|
updates.append("description = %s")
|
|
|
|
|
params.append(request.description)
|
|
|
|
|
|
|
|
|
|
if request.original_hours is not None:
|
|
|
|
|
updates.append("original_hours = %s")
|
|
|
|
|
params.append(request.original_hours)
|
|
|
|
|
|
|
|
|
|
if request.billable is not None:
|
|
|
|
|
updates.append("billable = %s")
|
|
|
|
|
params.append(request.billable)
|
|
|
|
|
|
|
|
|
|
if updates:
|
|
|
|
|
params.append(time_id)
|
|
|
|
|
execute_query(f"UPDATE tmodule_times SET {', '.join(updates)} WHERE id = %s", tuple(params))
|
|
|
|
|
|
|
|
|
|
# Fetch fresh with context for response
|
|
|
|
|
query = """
|
|
|
|
|
SELECT t.*, c.title as case_title, c.status as case_status,
|
|
|
|
|
cust.name as customer_name
|
|
|
|
|
FROM tmodule_times t
|
|
|
|
|
LEFT JOIN tmodule_cases c ON t.case_id = c.id
|
|
|
|
|
LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
|
|
|
|
|
WHERE t.id = %s
|
|
|
|
|
"""
|
|
|
|
|
return execute_query_single(query, (time_id,))
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Failed to update entry {time_id}: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2025-12-13 12:06:28 +01:00
|
|
|
@router.post("/wizard/reset/{time_id}", response_model=TModuleTimeWithContext, tags=["Wizard"])
|
|
|
|
|
async def reset_to_pending(
|
|
|
|
|
time_id: int,
|
|
|
|
|
reason: Optional[str] = None,
|
|
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Nulstil en godkendt/afvist tidsregistrering tilbage til pending.
|
|
|
|
|
|
|
|
|
|
Query params:
|
|
|
|
|
- reason: Årsag til nulstilling (optional)
|
|
|
|
|
- user_id: ID på brugeren der nulstiller (optional)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return wizard.reset_to_pending(time_id, reason=reason, user_id=user_id)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Reset failed: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
@router.get("/wizard/case/{case_id}/entries", response_model=List[TModuleTimeWithContext], tags=["Wizard"])
|
|
|
|
|
async def get_case_entries(
|
|
|
|
|
case_id: int,
|
|
|
|
|
exclude_time_card: bool = True
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Hent alle pending timelogs for en case.
|
|
|
|
|
|
|
|
|
|
Bruges til at vise alle tidsregistreringer i samme case grupperet.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return wizard.get_case_entries(case_id, exclude_time_card=exclude_time_card)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/wizard/case/{case_id}/details", tags=["Wizard"])
|
|
|
|
|
async def get_case_details(case_id: int):
|
|
|
|
|
"""
|
|
|
|
|
Hent komplet case information inkl. alle timelogs og kommentarer.
|
|
|
|
|
|
|
|
|
|
Returnerer:
|
|
|
|
|
- case_id, case_title, case_description, case_status
|
|
|
|
|
- timelogs: ALLE tidsregistreringer (pending, approved, rejected)
|
|
|
|
|
- case_comments: Kommentarer fra vTiger
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return wizard.get_case_details(case_id)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/wizard/case/{case_id}/approve-all", tags=["Wizard"])
|
|
|
|
|
async def approve_all_case_entries(
|
|
|
|
|
case_id: int,
|
|
|
|
|
user_id: Optional[int] = None,
|
|
|
|
|
exclude_time_card: bool = True
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Bulk-godkend alle pending timelogs for en case.
|
|
|
|
|
|
|
|
|
|
Afr under automatisk efter configured settings.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return wizard.approve_case_entries(
|
|
|
|
|
case_id=case_id,
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
exclude_time_card=exclude_time_card
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
@router.get("/wizard/progress/{customer_id}", response_model=TModuleWizardProgress, tags=["Wizard"])
|
|
|
|
|
async def get_customer_progress(customer_id: int):
|
|
|
|
|
"""Hent wizard progress for en kunde"""
|
|
|
|
|
try:
|
|
|
|
|
return wizard.get_customer_progress(customer_id)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# ORDER ENDPOINTS
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
2026-01-10 01:37:08 +01:00
|
|
|
@router.get("/customers/{customer_id}/data-consistency", tags=["Customers", "Data Consistency"])
|
|
|
|
|
async def check_tmodule_customer_data_consistency(customer_id: int):
|
|
|
|
|
"""
|
|
|
|
|
🔍 Check data consistency across Hub, vTiger, and e-conomic for tmodule_customer.
|
|
|
|
|
|
|
|
|
|
Before creating order, verify customer data is in sync across all systems.
|
|
|
|
|
Maps tmodule_customers.hub_customer_id to the consistency service.
|
|
|
|
|
|
|
|
|
|
Returns discrepancies found between the three systems.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
|
|
|
|
if not settings.AUTO_CHECK_CONSISTENCY:
|
|
|
|
|
return {
|
|
|
|
|
"enabled": False,
|
|
|
|
|
"message": "Data consistency checking is disabled"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Get tmodule_customer and find linked hub customer
|
|
|
|
|
tmodule_customer = execute_query_single(
|
|
|
|
|
"SELECT * FROM tmodule_customers WHERE id = %s",
|
|
|
|
|
(customer_id,)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not tmodule_customer:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Customer {customer_id} not found in tmodule_customers")
|
|
|
|
|
|
|
|
|
|
# Get linked hub customer ID
|
|
|
|
|
hub_customer_id = tmodule_customer.get('hub_customer_id')
|
|
|
|
|
|
|
|
|
|
if not hub_customer_id:
|
|
|
|
|
return {
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"customer_id": customer_id,
|
|
|
|
|
"discrepancy_count": 0,
|
|
|
|
|
"discrepancies": {},
|
|
|
|
|
"systems_available": {
|
|
|
|
|
"hub": False,
|
|
|
|
|
"vtiger": bool(tmodule_customer.get('vtiger_id')),
|
|
|
|
|
"economic": False
|
|
|
|
|
},
|
|
|
|
|
"message": "Customer not linked to Hub - cannot check consistency"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Use Hub customer ID for consistency check
|
|
|
|
|
consistency_service = CustomerConsistencyService()
|
|
|
|
|
|
|
|
|
|
# Fetch data from all systems
|
|
|
|
|
all_data = await consistency_service.fetch_all_data(hub_customer_id)
|
|
|
|
|
|
|
|
|
|
# Compare data
|
|
|
|
|
discrepancies = consistency_service.compare_data(all_data)
|
|
|
|
|
|
|
|
|
|
# Count actual discrepancies
|
|
|
|
|
discrepancy_count = sum(
|
|
|
|
|
1 for field_data in discrepancies.values()
|
|
|
|
|
if field_data['discrepancy']
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"customer_id": customer_id,
|
|
|
|
|
"hub_customer_id": hub_customer_id,
|
|
|
|
|
"discrepancy_count": discrepancy_count,
|
|
|
|
|
"discrepancies": discrepancies,
|
|
|
|
|
"systems_available": {
|
|
|
|
|
"hub": True,
|
|
|
|
|
"vtiger": all_data.get('vtiger') is not None,
|
|
|
|
|
"economic": all_data.get('economic') is not None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Failed to check consistency for tmodule_customer {customer_id}: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/customers/{customer_id}/sync-field", tags=["Customers", "Data Consistency"])
|
|
|
|
|
async def sync_tmodule_customer_field(
|
|
|
|
|
customer_id: int,
|
|
|
|
|
field_name: str = Body(..., description="Hub field name to sync"),
|
|
|
|
|
source_system: str = Body(..., description="Source system: hub, vtiger, or economic"),
|
|
|
|
|
source_value: str = Body(..., description="The correct value to sync")
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
🔄 Sync a single field across all systems for tmodule_customer.
|
|
|
|
|
|
|
|
|
|
Takes the correct value from one system and updates the others.
|
|
|
|
|
Maps tmodule_customers.hub_customer_id to the consistency service.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
|
|
|
|
# Validate source system
|
|
|
|
|
if source_system not in ['hub', 'vtiger', 'economic']:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail=f"Invalid source_system: {source_system}. Must be hub, vtiger, or economic"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Get tmodule_customer and find linked hub customer
|
|
|
|
|
tmodule_customer = execute_query_single(
|
|
|
|
|
"SELECT * FROM tmodule_customers WHERE id = %s",
|
|
|
|
|
(customer_id,)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not tmodule_customer:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Customer {customer_id} not found in tmodule_customers")
|
|
|
|
|
|
|
|
|
|
# Get linked hub customer ID
|
|
|
|
|
hub_customer_id = tmodule_customer.get('hub_customer_id')
|
|
|
|
|
|
|
|
|
|
if not hub_customer_id:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail="Customer not linked to Hub - cannot sync fields"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
consistency_service = CustomerConsistencyService()
|
|
|
|
|
|
|
|
|
|
# Perform sync on the linked Hub customer
|
|
|
|
|
results = await consistency_service.sync_field(
|
|
|
|
|
customer_id=hub_customer_id,
|
|
|
|
|
field_name=field_name,
|
|
|
|
|
source_system=source_system,
|
|
|
|
|
source_value=source_value
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.info(f"✅ Field '{field_name}' synced from {source_system}: {results}")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"field": field_name,
|
|
|
|
|
"source": source_system,
|
|
|
|
|
"value": source_value,
|
|
|
|
|
"results": results
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Failed to sync field for tmodule_customer {customer_id}: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
@router.post("/orders/generate/{customer_id}", response_model=TModuleOrderWithLines, tags=["Orders"])
|
|
|
|
|
async def generate_order(customer_id: int, user_id: Optional[int] = None):
|
|
|
|
|
"""
|
|
|
|
|
Generer ordre for alle godkendte tider for en kunde.
|
|
|
|
|
|
|
|
|
|
Aggregerer:
|
|
|
|
|
- Alle godkendte tidsregistreringer
|
|
|
|
|
- Grupperet efter case
|
|
|
|
|
- Beregner totals med moms
|
|
|
|
|
|
|
|
|
|
Markerer tidsregistreringer som 'billed'.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return order_service.generate_order_for_customer(customer_id, user_id=user_id)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orders", response_model=List[TModuleOrder], tags=["Orders"])
|
|
|
|
|
async def list_orders(
|
|
|
|
|
customer_id: Optional[int] = None,
|
|
|
|
|
status: Optional[str] = None,
|
|
|
|
|
limit: int = 100
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
List ordrer med filtrering.
|
|
|
|
|
|
|
|
|
|
Query params:
|
|
|
|
|
- customer_id: Filtrer til specifik kunde
|
|
|
|
|
- status: Filtrer på status (draft, exported, sent, cancelled)
|
|
|
|
|
- limit: Max antal resultater (default: 100)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return order_service.list_orders(
|
|
|
|
|
customer_id=customer_id,
|
|
|
|
|
status=status,
|
|
|
|
|
limit=limit
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orders/{order_id}", response_model=TModuleOrderWithLines, tags=["Orders"])
|
|
|
|
|
async def get_order(order_id: int):
|
|
|
|
|
"""Hent ordre med linjer"""
|
|
|
|
|
try:
|
|
|
|
|
return order_service.get_order_with_lines(order_id)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/orders/{order_id}/cancel", response_model=TModuleOrder, tags=["Orders"])
|
|
|
|
|
async def cancel_order(
|
|
|
|
|
order_id: int,
|
|
|
|
|
reason: Optional[str] = None,
|
|
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Annuller en ordre.
|
|
|
|
|
|
|
|
|
|
Kun muligt for draft orders (ikke exported).
|
|
|
|
|
Resetter tidsregistreringer tilbage til 'approved'.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return order_service.cancel_order(order_id, reason=reason, user_id=user_id)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 12:28:12 +01:00
|
|
|
@router.post("/orders/{order_id}/unlock", tags=["Orders"])
|
|
|
|
|
async def unlock_order(
|
|
|
|
|
order_id: int,
|
|
|
|
|
admin_code: str,
|
|
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
🔓 Lås en eksporteret ordre op for ændringer (ADMIN ONLY).
|
|
|
|
|
|
|
|
|
|
Kræver:
|
|
|
|
|
1. Korrekt admin unlock code (fra TIMETRACKING_ADMIN_UNLOCK_CODE)
|
|
|
|
|
2. Ordren skal være slettet fra e-conomic først
|
|
|
|
|
|
|
|
|
|
Query params:
|
|
|
|
|
- admin_code: Admin unlock kode
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
|
|
|
|
# Verify admin code
|
|
|
|
|
if not settings.TIMETRACKING_ADMIN_UNLOCK_CODE:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=500,
|
|
|
|
|
detail="Admin unlock code ikke konfigureret i systemet"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if admin_code != settings.TIMETRACKING_ADMIN_UNLOCK_CODE:
|
|
|
|
|
logger.warning(f"⚠️ Ugyldig unlock code forsøg for ordre {order_id}")
|
|
|
|
|
raise HTTPException(status_code=403, detail="Ugyldig admin kode")
|
|
|
|
|
|
|
|
|
|
# Get order
|
|
|
|
|
order = order_service.get_order_with_lines(order_id)
|
|
|
|
|
|
|
|
|
|
if order.status != 'exported':
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail="Kun eksporterede ordrer kan låses op"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not order.economic_draft_id:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail="Ordre har ingen e-conomic ID"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Check if order still exists in e-conomic
|
|
|
|
|
try:
|
|
|
|
|
draft_exists = await economic_service.check_draft_exists(order.economic_draft_id)
|
|
|
|
|
|
|
|
|
|
if draft_exists:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail=f"⚠️ Ordren findes stadig i e-conomic (Draft #{order.economic_draft_id}). Slet den i e-conomic først!"
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Kunne ikke tjekke e-conomic status: {e}")
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=500,
|
|
|
|
|
detail=f"Kunne ikke verificere e-conomic status: {str(e)}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Unlock order - set status back to draft
|
|
|
|
|
update_query = """
|
|
|
|
|
UPDATE tmodule_orders
|
|
|
|
|
SET status = 'draft',
|
|
|
|
|
economic_draft_id = NULL,
|
|
|
|
|
exported_at = NULL
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
RETURNING *
|
|
|
|
|
"""
|
|
|
|
|
|
2025-12-16 15:36:11 +01:00
|
|
|
result = execute_query_single(update_query, (order_id,))
|
2025-12-15 12:28:12 +01:00
|
|
|
|
|
|
|
|
# Log unlock
|
|
|
|
|
audit.log_event(
|
|
|
|
|
event_type="order_unlocked",
|
|
|
|
|
entity_type="order",
|
|
|
|
|
entity_id=order_id,
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
details={
|
|
|
|
|
"previous_economic_id": order.economic_draft_id,
|
|
|
|
|
"unlocked_by_admin": True
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.info(f"🔓 Order {order_id} unlocked by admin (user {user_id})")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"message": "Ordre låst op - kan nu redigeres eller slettes",
|
|
|
|
|
"order_id": order_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Unlock failed: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
# ============================================================================
|
|
|
|
|
# e-conomic EXPORT ENDPOINTS
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
@router.post("/export", response_model=TModuleEconomicExportResult, tags=["Export"])
|
|
|
|
|
async def export_to_economic(
|
|
|
|
|
request: TModuleEconomicExportRequest,
|
|
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
🚨 Eksporter ordre til e-conomic som draft order.
|
|
|
|
|
|
|
|
|
|
SAFETY FLAGS:
|
|
|
|
|
- TIMETRACKING_ECONOMIC_READ_ONLY (default: True)
|
|
|
|
|
- TIMETRACKING_ECONOMIC_DRY_RUN (default: True)
|
|
|
|
|
|
|
|
|
|
Hvis begge er enabled, køres kun dry-run simulation.
|
|
|
|
|
|
|
|
|
|
Body:
|
|
|
|
|
- order_id: ID på ordren
|
|
|
|
|
- force: Re-eksporter selvom allerede eksporteret (default: false)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return await economic_service.export_order(request, user_id=user_id)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/export/test-connection", tags=["Export"])
|
|
|
|
|
async def test_economic_connection():
|
|
|
|
|
"""Test forbindelse til e-conomic"""
|
|
|
|
|
try:
|
|
|
|
|
is_connected = await economic_service.test_connection()
|
|
|
|
|
return {
|
|
|
|
|
"connected": is_connected,
|
|
|
|
|
"service": "e-conomic",
|
|
|
|
|
"read_only": economic_service.read_only,
|
|
|
|
|
"dry_run": economic_service.dry_run,
|
|
|
|
|
"export_type": economic_service.export_type
|
|
|
|
|
}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# MODULE METADATA & ADMIN ENDPOINTS
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
@router.get("/metadata", response_model=TModuleMetadata, tags=["Admin"])
|
|
|
|
|
async def get_module_metadata():
|
|
|
|
|
"""Hent modul metadata"""
|
|
|
|
|
try:
|
2025-12-16 15:36:11 +01:00
|
|
|
result = execute_query_single(
|
|
|
|
|
"SELECT * FROM tmodule_metadata ORDER BY id DESC LIMIT 1")
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
|
|
|
|
|
if not result:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Module metadata not found")
|
|
|
|
|
|
|
|
|
|
return TModuleMetadata(**result)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/health", tags=["Admin"])
|
|
|
|
|
async def module_health():
|
|
|
|
|
"""Module health check"""
|
|
|
|
|
try:
|
|
|
|
|
# Check database tables exist
|
|
|
|
|
tables_query = """
|
|
|
|
|
SELECT COUNT(*) as count FROM information_schema.tables
|
|
|
|
|
WHERE table_name LIKE 'tmodule_%'
|
|
|
|
|
"""
|
2025-12-16 15:36:11 +01:00
|
|
|
result = execute_query_single(tables_query)
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
table_count = result['count'] if result else 0
|
|
|
|
|
|
|
|
|
|
# Get stats - count each table separately
|
|
|
|
|
try:
|
|
|
|
|
stats = {
|
|
|
|
|
"customers": 0,
|
|
|
|
|
"cases": 0,
|
|
|
|
|
"times": 0,
|
|
|
|
|
"orders": 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for table_name in ["customers", "cases", "times", "orders"]:
|
2025-12-16 15:36:11 +01:00
|
|
|
count_result = execute_query_single(
|
|
|
|
|
f"SELECT COUNT(*) as count FROM tmodule_{table_name}")
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
stats[table_name] = count_result['count'] if count_result else 0
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
stats = {"error": str(e)}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"status": "healthy" if table_count >= 6 else "degraded",
|
|
|
|
|
"module": "Time Tracking & Billing",
|
|
|
|
|
"version": "1.0.0",
|
|
|
|
|
"tables": table_count,
|
|
|
|
|
"statistics": stats,
|
|
|
|
|
"safety": {
|
|
|
|
|
"vtiger_read_only": vtiger_service.read_only,
|
|
|
|
|
"vtiger_dry_run": vtiger_service.dry_run,
|
|
|
|
|
"economic_read_only": economic_service.read_only,
|
|
|
|
|
"economic_dry_run": economic_service.dry_run
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Health check error: {e}")
|
|
|
|
|
return JSONResponse(
|
2025-12-10 18:29:13 +01:00
|
|
|
status_code=500,
|
|
|
|
|
content={"status": "error", "message": str(e)}
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
@router.get("/config", tags=["Admin"])
|
|
|
|
|
async def get_config():
|
|
|
|
|
"""Hent modul konfiguration"""
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"default_hourly_rate": float(settings.TIMETRACKING_DEFAULT_HOURLY_RATE),
|
|
|
|
|
"auto_round": settings.TIMETRACKING_AUTO_ROUND,
|
|
|
|
|
"round_increment": float(settings.TIMETRACKING_ROUND_INCREMENT),
|
|
|
|
|
"round_method": settings.TIMETRACKING_ROUND_METHOD,
|
|
|
|
|
"vtiger_read_only": settings.TIMETRACKING_VTIGER_READ_ONLY,
|
|
|
|
|
"vtiger_dry_run": settings.TIMETRACKING_VTIGER_DRY_RUN,
|
|
|
|
|
"economic_read_only": settings.TIMETRACKING_ECONOMIC_READ_ONLY,
|
|
|
|
|
"economic_dry_run": settings.TIMETRACKING_ECONOMIC_DRY_RUN
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# CUSTOMER MANAGEMENT ENDPOINTS
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
@router.patch("/customers/{customer_id}/hourly-rate", tags=["Customers"])
|
|
|
|
|
async def update_customer_hourly_rate(customer_id: int, hourly_rate: float, user_id: Optional[int] = None):
|
|
|
|
|
"""
|
|
|
|
|
Opdater timepris for en kunde.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
customer_id: Kunde ID
|
|
|
|
|
hourly_rate: Ny timepris i DKK (f.eks. 850.00)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
|
|
# Validate rate
|
|
|
|
|
if hourly_rate < 0:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Hourly rate must be positive")
|
|
|
|
|
|
|
|
|
|
rate_decimal = Decimal(str(hourly_rate))
|
|
|
|
|
|
|
|
|
|
# Update customer hourly rate
|
|
|
|
|
execute_update(
|
|
|
|
|
"UPDATE tmodule_customers SET hourly_rate = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
|
|
|
|
|
(rate_decimal, customer_id)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Audit log
|
|
|
|
|
audit.log_event(
|
|
|
|
|
entity_type="customer",
|
|
|
|
|
entity_id=str(customer_id),
|
|
|
|
|
event_type="hourly_rate_updated",
|
|
|
|
|
details={"hourly_rate": float(hourly_rate)},
|
|
|
|
|
user_id=user_id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Return updated customer
|
2025-12-16 15:36:11 +01:00
|
|
|
customer = execute_query_single(
|
2025-12-10 18:29:13 +01:00
|
|
|
"SELECT id, name, hourly_rate FROM tmodule_customers WHERE id = %s",
|
2025-12-16 15:36:11 +01:00
|
|
|
(customer_id,))
|
2025-12-10 18:29:13 +01:00
|
|
|
|
|
|
|
|
if not customer:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"customer_id": customer_id,
|
|
|
|
|
"name": customer['name'],
|
|
|
|
|
"hourly_rate": float(customer['hourly_rate']) if customer['hourly_rate'] else None,
|
|
|
|
|
"updated": True
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error updating hourly rate: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2025-12-23 14:31:10 +01:00
|
|
|
@router.post("/customers/bulk-update-rate", tags=["Customers"])
|
|
|
|
|
async def bulk_update_customer_hourly_rates(
|
2025-12-23 14:39:57 +01:00
|
|
|
request: TModuleBulkRateUpdate,
|
2025-12-23 14:31:10 +01:00
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Opdater timepris for flere kunder på én gang.
|
|
|
|
|
|
2025-12-23 14:39:57 +01:00
|
|
|
Request body:
|
2025-12-23 14:31:10 +01:00
|
|
|
customer_ids: Liste af kunde-ID'er
|
|
|
|
|
hourly_rate: Ny timepris i DKK (f.eks. 850.00)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Antal opdaterede kunder
|
|
|
|
|
"""
|
|
|
|
|
try:
|
2025-12-23 14:39:57 +01:00
|
|
|
# Validate inputs (already validated by Pydantic)
|
|
|
|
|
customer_ids = request.customer_ids
|
|
|
|
|
rate_decimal = request.hourly_rate
|
2025-12-23 14:31:10 +01:00
|
|
|
|
|
|
|
|
# Update all selected customers
|
|
|
|
|
execute_update(
|
|
|
|
|
"UPDATE tmodule_customers SET hourly_rate = %s, updated_at = CURRENT_TIMESTAMP WHERE id = ANY(%s)",
|
|
|
|
|
(rate_decimal, customer_ids)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Count affected rows
|
|
|
|
|
updated_count = len(customer_ids)
|
|
|
|
|
|
|
|
|
|
# Audit log for each customer
|
|
|
|
|
for customer_id in customer_ids:
|
|
|
|
|
audit.log_event(
|
|
|
|
|
entity_type="customer",
|
|
|
|
|
entity_id=str(customer_id),
|
|
|
|
|
event_type="hourly_rate_updated",
|
2025-12-23 14:39:57 +01:00
|
|
|
details={"hourly_rate": float(rate_decimal), "bulk_update": True},
|
2025-12-23 14:31:10 +01:00
|
|
|
user_id=user_id
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-23 14:39:57 +01:00
|
|
|
logger.info(f"✅ Bulk updated hourly rate for {updated_count} customers to {rate_decimal} DKK")
|
2025-12-23 14:31:10 +01:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"updated": updated_count,
|
2025-12-23 14:39:57 +01:00
|
|
|
"hourly_rate": float(rate_decimal)
|
2025-12-23 14:31:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error in bulk hourly rate update: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
@router.patch("/customers/{customer_id}/time-card", tags=["Customers"])
|
|
|
|
|
async def toggle_customer_time_card(customer_id: int, enabled: bool, user_id: Optional[int] = None):
|
|
|
|
|
"""
|
|
|
|
|
Skift klippekort-status for kunde.
|
|
|
|
|
|
|
|
|
|
Klippekort-kunder faktureres eksternt og skal kunne skjules i godkendelsesflow.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Update customer time card flag
|
|
|
|
|
execute_update(
|
|
|
|
|
"UPDATE tmodule_customers SET uses_time_card = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
|
|
|
|
|
(enabled, customer_id)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Audit log
|
|
|
|
|
audit.log_event(
|
|
|
|
|
entity_type="customer",
|
|
|
|
|
entity_id=str(customer_id),
|
|
|
|
|
event_type="time_card_toggled",
|
|
|
|
|
details={"enabled": enabled},
|
|
|
|
|
user_id=user_id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Return updated customer
|
2025-12-16 15:36:11 +01:00
|
|
|
customer = execute_query_single(
|
2025-12-10 18:29:13 +01:00
|
|
|
"SELECT * FROM tmodule_customers WHERE id = %s",
|
2025-12-16 15:36:11 +01:00
|
|
|
(customer_id,))
|
2025-12-10 18:29:13 +01:00
|
|
|
|
|
|
|
|
if not customer:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"customer_id": customer_id,
|
|
|
|
|
"name": customer['name'],
|
|
|
|
|
"uses_time_card": customer['uses_time_card'],
|
|
|
|
|
"updated": True
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error toggling time card: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/customers", tags=["Customers"])
|
|
|
|
|
async def list_customers(
|
|
|
|
|
include_time_card: bool = True,
|
|
|
|
|
only_with_entries: bool = False
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
List kunder med filtrering.
|
|
|
|
|
|
|
|
|
|
Query params:
|
|
|
|
|
- include_time_card: Inkluder klippekort-kunder (default: true)
|
|
|
|
|
- only_with_entries: Kun kunder med pending tidsregistreringer (default: false)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
if only_with_entries:
|
|
|
|
|
# Use view that includes entry counts
|
|
|
|
|
query = """
|
|
|
|
|
SELECT customer_id, customer_name, customer_vtiger_id, uses_time_card,
|
|
|
|
|
total_entries, pending_count
|
|
|
|
|
FROM tmodule_approval_stats
|
|
|
|
|
WHERE total_entries > 0
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if not include_time_card:
|
|
|
|
|
query += " AND uses_time_card = false"
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY customer_name"
|
|
|
|
|
|
2025-12-16 15:36:11 +01:00
|
|
|
customers = execute_query_single(query)
|
2025-12-10 18:29:13 +01:00
|
|
|
else:
|
|
|
|
|
# Simple customer list
|
|
|
|
|
query = "SELECT * FROM tmodule_customers"
|
|
|
|
|
|
|
|
|
|
if not include_time_card:
|
|
|
|
|
query += " WHERE uses_time_card = false"
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY name"
|
|
|
|
|
|
|
|
|
|
customers = execute_query(query)
|
|
|
|
|
|
|
|
|
|
return {"customers": customers, "total": len(customers)}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error listing customers: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/customers/{customer_id}/times", tags=["Customers"])
|
|
|
|
|
async def get_customer_time_entries(customer_id: int, status: Optional[str] = None):
|
|
|
|
|
"""
|
|
|
|
|
Hent alle tidsregistreringer for en kunde.
|
|
|
|
|
|
|
|
|
|
Path params:
|
|
|
|
|
- customer_id: Kunde ID
|
|
|
|
|
|
|
|
|
|
Query params:
|
|
|
|
|
- status: Filtrer på status (pending, approved, rejected, billed)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
query = """
|
|
|
|
|
SELECT t.*,
|
|
|
|
|
COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
|
|
|
|
|
c.vtiger_id AS case_vtiger_id,
|
|
|
|
|
c.description AS case_description,
|
2026-01-10 01:37:08 +01:00
|
|
|
c.priority AS case_priority,
|
|
|
|
|
c.module_type AS case_type,
|
2025-12-10 18:29:13 +01:00
|
|
|
cust.name AS customer_name
|
|
|
|
|
FROM tmodule_times t
|
|
|
|
|
LEFT JOIN tmodule_cases c ON t.case_id = c.id
|
|
|
|
|
LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
|
|
|
|
|
WHERE t.customer_id = %s
|
2026-01-09 08:01:28 +01:00
|
|
|
AND t.billed_via_thehub_id IS NULL
|
|
|
|
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
|
|
|
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
|
|
|
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
|
|
|
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
|
|
|
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
|
|
|
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
|
|
|
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
|
|
|
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
|
|
|
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
2025-12-10 18:29:13 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
params = [customer_id]
|
|
|
|
|
|
|
|
|
|
if status:
|
|
|
|
|
query += " AND t.status = %s"
|
|
|
|
|
params.append(status)
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY t.worked_date DESC, t.id DESC"
|
|
|
|
|
|
|
|
|
|
times = execute_query(query, tuple(params))
|
|
|
|
|
|
2026-01-10 21:09:29 +01:00
|
|
|
# 🔗 Combine with Hub Worklogs (tticket_worklog)
|
|
|
|
|
# Only if we can find the linked Hub Customer ID
|
|
|
|
|
try:
|
|
|
|
|
cust_res = execute_query_single(
|
|
|
|
|
"SELECT hub_customer_id, name FROM tmodule_customers WHERE id = %s",
|
|
|
|
|
(customer_id,)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if cust_res and cust_res.get('hub_customer_id'):
|
|
|
|
|
hub_id = cust_res['hub_customer_id']
|
|
|
|
|
hub_name = cust_res['name']
|
|
|
|
|
|
|
|
|
|
# Fetch worklogs
|
|
|
|
|
w_query = """
|
|
|
|
|
SELECT
|
|
|
|
|
(w.id * -1) as id,
|
|
|
|
|
w.work_date as worked_date,
|
|
|
|
|
w.hours as original_hours,
|
|
|
|
|
w.description,
|
|
|
|
|
CASE
|
|
|
|
|
WHEN w.status = 'draft' THEN 'pending'
|
|
|
|
|
ELSE w.status
|
|
|
|
|
END as status,
|
|
|
|
|
|
|
|
|
|
-- Ticket info as Case info
|
|
|
|
|
t.subject as case_title,
|
|
|
|
|
t.ticket_number as case_vtiger_id,
|
|
|
|
|
t.description as case_description,
|
|
|
|
|
CASE
|
|
|
|
|
WHEN t.priority = 'urgent' THEN 'Høj'
|
|
|
|
|
ELSE 'Normal'
|
|
|
|
|
END as case_priority,
|
|
|
|
|
w.work_type as case_type,
|
|
|
|
|
|
|
|
|
|
-- Customer info
|
|
|
|
|
%s as customer_name,
|
|
|
|
|
%s as customer_id,
|
|
|
|
|
|
|
|
|
|
-- Logic
|
|
|
|
|
CASE
|
|
|
|
|
WHEN w.billing_method IN ('internal', 'warranty') THEN false
|
|
|
|
|
ELSE true
|
|
|
|
|
END as billable,
|
|
|
|
|
false as is_travel,
|
|
|
|
|
|
|
|
|
|
-- Extra context for frontend flags if needed
|
|
|
|
|
w.billing_method as _billing_method
|
|
|
|
|
|
|
|
|
|
FROM tticket_worklog w
|
|
|
|
|
JOIN tticket_tickets t ON w.ticket_id = t.id
|
|
|
|
|
WHERE t.customer_id = %s
|
|
|
|
|
"""
|
|
|
|
|
w_params = [hub_name, customer_id, hub_id]
|
|
|
|
|
|
|
|
|
|
if status:
|
|
|
|
|
if status == 'pending':
|
|
|
|
|
w_query += " AND w.status = 'draft'"
|
|
|
|
|
else:
|
|
|
|
|
w_query += " AND w.status = %s"
|
|
|
|
|
w_params.append(status)
|
|
|
|
|
|
|
|
|
|
w_times = execute_query(w_query, tuple(w_params))
|
|
|
|
|
|
|
|
|
|
if w_times:
|
|
|
|
|
times.extend(w_times)
|
|
|
|
|
# Re-sort combined list
|
|
|
|
|
times.sort(key=lambda x: (x.get('worked_date') or '', x.get('id')), reverse=True)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"⚠️ Failed to fetch hub worklogs for wizard: {e}")
|
|
|
|
|
# Continue with just tmodule times
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
return {"times": times, "total": len(times)}
|
2026-01-02 16:08:59 +01:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error fetching customer time entries: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
2026-01-02 15:53:00 +01:00
|
|
|
|
|
|
|
|
|
2026-01-10 01:37:08 +01:00
|
|
|
@router.get("/times", tags=["Times"])
|
|
|
|
|
async def list_time_entries(
|
|
|
|
|
limit: int = 100,
|
|
|
|
|
offset: int = 0,
|
|
|
|
|
status: Optional[str] = None,
|
|
|
|
|
customer_id: Optional[int] = None,
|
|
|
|
|
user_name: Optional[str] = None,
|
|
|
|
|
search: Optional[str] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Hent liste af tidsregistreringer med filtre.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
query = """
|
|
|
|
|
SELECT t.*,
|
|
|
|
|
COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
|
|
|
|
|
c.priority AS case_priority,
|
|
|
|
|
cust.name AS customer_name
|
|
|
|
|
FROM tmodule_times t
|
|
|
|
|
LEFT JOIN tmodule_cases c ON t.case_id = c.id
|
|
|
|
|
LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
|
|
|
|
|
WHERE 1=1
|
|
|
|
|
"""
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
if status:
|
|
|
|
|
query += " AND t.status = %s"
|
|
|
|
|
params.append(status)
|
|
|
|
|
|
|
|
|
|
if customer_id:
|
|
|
|
|
query += " AND t.customer_id = %s"
|
|
|
|
|
params.append(customer_id)
|
|
|
|
|
|
|
|
|
|
if user_name:
|
|
|
|
|
query += " AND t.user_name ILIKE %s"
|
|
|
|
|
params.append(f"%{user_name}%")
|
|
|
|
|
|
|
|
|
|
if search:
|
|
|
|
|
query += """ AND (
|
|
|
|
|
t.description ILIKE %s OR
|
|
|
|
|
cust.name ILIKE %s OR
|
|
|
|
|
c.title ILIKE %s
|
|
|
|
|
)"""
|
|
|
|
|
wildcard = f"%{search}%"
|
|
|
|
|
params.extend([wildcard, wildcard, wildcard])
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY t.worked_date DESC, t.id DESC LIMIT %s OFFSET %s"
|
|
|
|
|
params.extend([limit, offset])
|
|
|
|
|
|
|
|
|
|
times = execute_query(query, tuple(params))
|
|
|
|
|
return {"times": times}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error listing times: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-01-02 15:53:00 +01:00
|
|
|
@router.get("/times/{time_id}", tags=["Times"])
|
|
|
|
|
async def get_time_entry(time_id: int):
|
|
|
|
|
"""
|
|
|
|
|
Hent en specifik tidsregistrering.
|
|
|
|
|
|
|
|
|
|
Path params:
|
|
|
|
|
- time_id: Time entry ID
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
query = """
|
|
|
|
|
SELECT t.*,
|
|
|
|
|
COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
|
|
|
|
|
c.vtiger_id AS case_vtiger_id,
|
|
|
|
|
c.description AS case_description,
|
|
|
|
|
cust.name AS customer_name
|
|
|
|
|
FROM tmodule_times t
|
|
|
|
|
LEFT JOIN tmodule_cases c ON t.case_id = c.id
|
|
|
|
|
LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
|
|
|
|
|
WHERE t.id = %s
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
time_entry = execute_query_single(query, (time_id,))
|
|
|
|
|
|
|
|
|
|
if not time_entry:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Time entry not found")
|
|
|
|
|
|
|
|
|
|
return time_entry
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error fetching time entry {time_id}: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
2025-12-10 18:29:13 +01:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error getting customer time entries: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
@router.delete("/admin/uninstall", response_model=TModuleUninstallResult, tags=["Admin"])
|
|
|
|
|
async def uninstall_module(
|
|
|
|
|
request: TModuleUninstallRequest,
|
|
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
🚨 SLET MODULET FULDSTÆNDIGT.
|
|
|
|
|
|
|
|
|
|
ADVARSEL: Dette sletter ALLE data i modulet!
|
|
|
|
|
Kan ikke fortrydes.
|
|
|
|
|
|
|
|
|
|
Body:
|
|
|
|
|
- confirm: SKAL være true
|
|
|
|
|
- delete_all_data: SKAL være true
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Validate request (Pydantic validators already check this)
|
|
|
|
|
if not request.confirm or not request.delete_all_data:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail="You must confirm uninstall and data deletion"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.warning(f"⚠️ UNINSTALLING TIME TRACKING MODULE (user_id: {user_id})")
|
|
|
|
|
|
|
|
|
|
# Log uninstall
|
|
|
|
|
audit.log_module_uninstalled(user_id=user_id)
|
|
|
|
|
|
|
|
|
|
# Execute DROP script
|
|
|
|
|
uninstall_script = """
|
|
|
|
|
DROP VIEW IF EXISTS tmodule_order_details CASCADE;
|
|
|
|
|
DROP VIEW IF EXISTS tmodule_next_pending CASCADE;
|
|
|
|
|
DROP VIEW IF EXISTS tmodule_approval_stats CASCADE;
|
|
|
|
|
|
|
|
|
|
DROP TRIGGER IF EXISTS tmodule_orders_generate_number ON tmodule_orders;
|
|
|
|
|
DROP TRIGGER IF EXISTS tmodule_orders_update ON tmodule_orders;
|
|
|
|
|
DROP TRIGGER IF EXISTS tmodule_times_update ON tmodule_times;
|
|
|
|
|
DROP TRIGGER IF EXISTS tmodule_cases_update ON tmodule_cases;
|
|
|
|
|
DROP TRIGGER IF EXISTS tmodule_customers_update ON tmodule_customers;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS tmodule_generate_order_number() CASCADE;
|
|
|
|
|
DROP FUNCTION IF EXISTS tmodule_update_timestamp() CASCADE;
|
|
|
|
|
|
|
|
|
|
DROP TABLE IF EXISTS tmodule_sync_log CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS tmodule_order_lines CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS tmodule_orders CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS tmodule_times CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS tmodule_cases CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS tmodule_customers CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS tmodule_metadata CASCADE;
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Count rows before deletion
|
|
|
|
|
try:
|
|
|
|
|
count_query = """
|
|
|
|
|
SELECT
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_customers) +
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_cases) +
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_times) +
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_orders) +
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_order_lines) +
|
|
|
|
|
(SELECT COUNT(*) FROM tmodule_sync_log) as total
|
|
|
|
|
"""
|
2025-12-16 15:36:11 +01:00
|
|
|
count_result = execute_query(count_query)
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
total_rows = count_result['total'] if count_result else 0
|
|
|
|
|
except:
|
|
|
|
|
total_rows = 0
|
|
|
|
|
|
|
|
|
|
# Execute uninstall (split into separate statements)
|
|
|
|
|
from app.core.database import get_db_connection
|
|
|
|
|
import psycopg2
|
|
|
|
|
|
2025-12-16 22:07:20 +01:00
|
|
|
conn = get_db_connection()
|
feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- Added FastAPI router for time tracking views including dashboard, wizard, and orders.
- Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration.
- Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs.
- Introduced a script to list all registered routes, focusing on time tracking routes.
- Added test script to verify route registration and specifically check for time tracking routes.
2025-12-09 22:46:30 +01:00
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
dropped_items = {
|
|
|
|
|
"views": [],
|
|
|
|
|
"triggers": [],
|
|
|
|
|
"functions": [],
|
|
|
|
|
"tables": []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Drop views
|
|
|
|
|
for view in ["tmodule_order_details", "tmodule_next_pending", "tmodule_approval_stats"]:
|
|
|
|
|
cursor.execute(f"DROP VIEW IF EXISTS {view} CASCADE")
|
|
|
|
|
dropped_items["views"].append(view)
|
|
|
|
|
|
|
|
|
|
# Drop triggers
|
|
|
|
|
triggers = [
|
|
|
|
|
("tmodule_orders_generate_number", "tmodule_orders"),
|
|
|
|
|
("tmodule_orders_update", "tmodule_orders"),
|
|
|
|
|
("tmodule_times_update", "tmodule_times"),
|
|
|
|
|
("tmodule_cases_update", "tmodule_cases"),
|
|
|
|
|
("tmodule_customers_update", "tmodule_customers")
|
|
|
|
|
]
|
|
|
|
|
for trigger_name, table_name in triggers:
|
|
|
|
|
cursor.execute(f"DROP TRIGGER IF EXISTS {trigger_name} ON {table_name}")
|
|
|
|
|
dropped_items["triggers"].append(trigger_name)
|
|
|
|
|
|
|
|
|
|
# Drop functions
|
|
|
|
|
for func in ["tmodule_generate_order_number", "tmodule_update_timestamp"]:
|
|
|
|
|
cursor.execute(f"DROP FUNCTION IF EXISTS {func}() CASCADE")
|
|
|
|
|
dropped_items["functions"].append(func)
|
|
|
|
|
|
|
|
|
|
# Drop tables
|
|
|
|
|
for table in [
|
|
|
|
|
"tmodule_sync_log",
|
|
|
|
|
"tmodule_order_lines",
|
|
|
|
|
"tmodule_orders",
|
|
|
|
|
"tmodule_times",
|
|
|
|
|
"tmodule_cases",
|
|
|
|
|
"tmodule_customers",
|
|
|
|
|
"tmodule_metadata"
|
|
|
|
|
]:
|
|
|
|
|
cursor.execute(f"DROP TABLE IF EXISTS {table} CASCADE")
|
|
|
|
|
dropped_items["tables"].append(table)
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
logger.warning(f"✅ Module uninstalled - deleted {total_rows} rows")
|
|
|
|
|
|
|
|
|
|
return TModuleUninstallResult(
|
|
|
|
|
success=True,
|
|
|
|
|
message=f"Time Tracking Module successfully uninstalled. Deleted {total_rows} rows.",
|
|
|
|
|
tables_dropped=dropped_items["tables"],
|
|
|
|
|
views_dropped=dropped_items["views"],
|
|
|
|
|
functions_dropped=dropped_items["functions"],
|
|
|
|
|
rows_deleted=total_rows
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
conn.rollback()
|
|
|
|
|
raise e
|
|
|
|
|
finally:
|
|
|
|
|
cursor.close()
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Uninstall failed: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|