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 Service for Time Tracking Module
|
|
|
|
|
========================================
|
|
|
|
|
|
|
|
|
|
Step-by-step approval flow for time entries.
|
|
|
|
|
Brugeren godkender én tidsregistrering ad gangen.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import logging
|
2025-12-10 18:29:13 +01:00
|
|
|
from typing import Optional, List, Dict, Any
|
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 decimal import Decimal
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
from app.core.database import execute_query, execute_update
|
|
|
|
|
from app.timetracking.backend.models import (
|
|
|
|
|
TModuleTimeWithContext,
|
|
|
|
|
TModuleTimeApproval,
|
|
|
|
|
TModuleWizardProgress,
|
|
|
|
|
TModuleWizardNextEntry,
|
|
|
|
|
TModuleApprovalStats
|
|
|
|
|
)
|
|
|
|
|
from app.timetracking.backend.audit import audit
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WizardService:
|
|
|
|
|
"""Service for managing wizard-based approval flow"""
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_customer_stats(customer_id: int) -> Optional[TModuleApprovalStats]:
|
|
|
|
|
"""Hent approval statistics for en kunde"""
|
|
|
|
|
try:
|
|
|
|
|
query = """
|
|
|
|
|
SELECT * FROM tmodule_approval_stats
|
|
|
|
|
WHERE customer_id = %s
|
|
|
|
|
"""
|
|
|
|
|
result = execute_query(query, (customer_id,), fetchone=True)
|
|
|
|
|
|
|
|
|
|
if not result:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return TModuleApprovalStats(**result)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error getting customer stats: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_all_customers_stats() -> list[TModuleApprovalStats]:
|
|
|
|
|
"""Hent approval statistics for alle kunder"""
|
|
|
|
|
try:
|
|
|
|
|
query = "SELECT * FROM tmodule_approval_stats ORDER BY customer_name"
|
|
|
|
|
results = execute_query(query)
|
|
|
|
|
|
|
|
|
|
return [TModuleApprovalStats(**row) for row in results]
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error getting all customer stats: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_next_pending_entry(
|
2025-12-10 18:29:13 +01:00
|
|
|
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
|
|
|
) -> TModuleWizardNextEntry:
|
|
|
|
|
"""
|
|
|
|
|
Hent næste pending tidsregistrering til godkendelse.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
customer_id: Valgfri - filtrer til specifik kunde
|
2025-12-10 18:29:13 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
TModuleWizardNextEntry med has_next=True hvis der er flere
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
if customer_id:
|
|
|
|
|
# Hent næste for specifik kunde
|
|
|
|
|
query = """
|
|
|
|
|
SELECT * FROM tmodule_next_pending
|
|
|
|
|
WHERE customer_id = %s
|
|
|
|
|
LIMIT 1
|
|
|
|
|
"""
|
|
|
|
|
result = execute_query(query, (customer_id,), fetchone=True)
|
|
|
|
|
else:
|
|
|
|
|
# Hent næste generelt
|
2025-12-10 18:29:13 +01:00
|
|
|
if exclude_time_card:
|
|
|
|
|
query = """
|
|
|
|
|
SELECT np.* FROM tmodule_next_pending np
|
|
|
|
|
JOIN tmodule_customers c ON np.customer_id = c.id
|
|
|
|
|
WHERE c.uses_time_card = false
|
|
|
|
|
LIMIT 1
|
|
|
|
|
"""
|
|
|
|
|
else:
|
|
|
|
|
query = "SELECT * FROM tmodule_next_pending 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
|
|
|
result = execute_query(query, fetchone=True)
|
|
|
|
|
|
|
|
|
|
if not result:
|
|
|
|
|
# Ingen flere entries
|
|
|
|
|
return TModuleWizardNextEntry(
|
|
|
|
|
has_next=False,
|
|
|
|
|
time_entry=None,
|
|
|
|
|
progress=None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Build entry with context
|
|
|
|
|
entry = TModuleTimeWithContext(**result)
|
|
|
|
|
|
|
|
|
|
# Get progress if customer_id known
|
|
|
|
|
progress = None
|
|
|
|
|
cust_id = customer_id or entry.customer_id
|
|
|
|
|
if cust_id:
|
|
|
|
|
stats = WizardService.get_customer_stats(cust_id)
|
|
|
|
|
if stats:
|
|
|
|
|
progress = TModuleWizardProgress(
|
|
|
|
|
customer_id=stats.customer_id,
|
|
|
|
|
customer_name=stats.customer_name,
|
|
|
|
|
total_entries=stats.total_entries,
|
|
|
|
|
approved_entries=stats.approved_count,
|
|
|
|
|
pending_entries=stats.pending_count,
|
|
|
|
|
rejected_entries=stats.rejected_count,
|
|
|
|
|
current_case_id=entry.case_id,
|
|
|
|
|
current_case_title=entry.case_title
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return TModuleWizardNextEntry(
|
|
|
|
|
has_next=True,
|
|
|
|
|
time_entry=entry,
|
|
|
|
|
progress=progress
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error getting next entry: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def approve_time_entry(
|
|
|
|
|
approval: TModuleTimeApproval,
|
|
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
) -> TModuleTimeWithContext:
|
|
|
|
|
"""
|
|
|
|
|
Godkend en tidsregistrering.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
approval: Approval data med time_id og approved_hours
|
|
|
|
|
user_id: ID på brugeren der godkender
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Opdateret tidsregistrering
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Hent original entry
|
|
|
|
|
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
|
|
|
|
|
"""
|
|
|
|
|
entry = execute_query(query, (approval.time_id,), fetchone=True)
|
|
|
|
|
|
|
|
|
|
if not entry:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Time entry not found")
|
|
|
|
|
|
|
|
|
|
if entry['status'] != 'pending':
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail=f"Time entry already {entry['status']}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Update entry
|
|
|
|
|
update_query = """
|
|
|
|
|
UPDATE tmodule_times
|
|
|
|
|
SET status = 'approved',
|
|
|
|
|
approved_hours = %s,
|
|
|
|
|
rounded_to = %s,
|
|
|
|
|
approval_note = %s,
|
|
|
|
|
billable = %s,
|
feat: Implement email processing system with scheduler, fetching, classification, and rule matching
- Added EmailProcessorService to orchestrate email workflow: fetching, saving, classifying, and matching rules.
- Introduced EmailScheduler for background processing of emails every 5 minutes.
- Developed EmailService to handle email fetching from IMAP and Microsoft Graph API.
- Created database migration for email system, including tables for email messages, rules, attachments, and analysis.
- Implemented AI classification and extraction for invoices and time confirmations.
- Added logging for better traceability and error handling throughout the email processing pipeline.
2025-12-11 02:31:29 +01:00
|
|
|
is_travel = %s,
|
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
|
|
|
approved_at = CURRENT_TIMESTAMP,
|
|
|
|
|
approved_by = %s
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
execute_update(
|
|
|
|
|
update_query,
|
|
|
|
|
(
|
|
|
|
|
approval.approved_hours,
|
|
|
|
|
approval.rounded_to,
|
|
|
|
|
approval.approval_note,
|
|
|
|
|
approval.billable,
|
feat: Implement email processing system with scheduler, fetching, classification, and rule matching
- Added EmailProcessorService to orchestrate email workflow: fetching, saving, classifying, and matching rules.
- Introduced EmailScheduler for background processing of emails every 5 minutes.
- Developed EmailService to handle email fetching from IMAP and Microsoft Graph API.
- Created database migration for email system, including tables for email messages, rules, attachments, and analysis.
- Implemented AI classification and extraction for invoices and time confirmations.
- Added logging for better traceability and error handling throughout the email processing pipeline.
2025-12-11 02:31:29 +01:00
|
|
|
approval.is_travel,
|
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,
|
|
|
|
|
approval.time_id
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Log approval
|
|
|
|
|
audit.log_approval(
|
|
|
|
|
time_id=approval.time_id,
|
|
|
|
|
original_hours=float(entry['original_hours']),
|
|
|
|
|
approved_hours=float(approval.approved_hours),
|
|
|
|
|
rounded_to=float(approval.rounded_to) if approval.rounded_to else None,
|
|
|
|
|
note=approval.approval_note,
|
|
|
|
|
user_id=user_id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
f"✅ Approved time entry {approval.time_id}: "
|
|
|
|
|
f"{entry['original_hours']}h → {approval.approved_hours}h"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Return updated entry
|
|
|
|
|
updated = execute_query(query, (approval.time_id,), fetchone=True)
|
|
|
|
|
return TModuleTimeWithContext(**updated)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error approving time entry: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def reject_time_entry(
|
|
|
|
|
time_id: int,
|
|
|
|
|
reason: Optional[str] = None,
|
|
|
|
|
user_id: Optional[int] = None
|
|
|
|
|
) -> TModuleTimeWithContext:
|
|
|
|
|
"""
|
|
|
|
|
Afvis en tidsregistrering.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
time_id: ID på tidsregistreringen
|
|
|
|
|
reason: Årsag til afvisning
|
|
|
|
|
user_id: ID på brugeren der afviser
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Opdateret tidsregistrering
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Check exists
|
|
|
|
|
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
|
|
|
|
|
"""
|
|
|
|
|
entry = execute_query(query, (time_id,), fetchone=True)
|
|
|
|
|
|
|
|
|
|
if not entry:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Time entry not found")
|
|
|
|
|
|
|
|
|
|
if entry['status'] != 'pending':
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail=f"Time entry already {entry['status']}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Update to rejected
|
|
|
|
|
update_query = """
|
|
|
|
|
UPDATE tmodule_times
|
|
|
|
|
SET status = 'rejected',
|
|
|
|
|
approval_note = %s,
|
|
|
|
|
billable = false,
|
|
|
|
|
approved_at = CURRENT_TIMESTAMP,
|
|
|
|
|
approved_by = %s
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
execute_update(update_query, (reason, user_id, time_id))
|
|
|
|
|
|
|
|
|
|
# Log rejection
|
|
|
|
|
audit.log_rejection(
|
|
|
|
|
time_id=time_id,
|
|
|
|
|
reason=reason,
|
|
|
|
|
user_id=user_id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.info(f"❌ Rejected time entry {time_id}: {reason}")
|
|
|
|
|
|
|
|
|
|
# Return updated
|
|
|
|
|
updated = execute_query(query, (time_id,), fetchone=True)
|
|
|
|
|
return TModuleTimeWithContext(**updated)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error rejecting time entry: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
2025-12-10 18:29:13 +01:00
|
|
|
@staticmethod
|
|
|
|
|
def approve_case_entries(
|
|
|
|
|
case_id: int,
|
|
|
|
|
user_id: Optional[int] = None,
|
|
|
|
|
exclude_time_card: bool = True
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
Bulk-godkend alle pending tidsregistreringer for en case.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
case_id: Case ID
|
|
|
|
|
user_id: ID på brugeren der godkender
|
|
|
|
|
exclude_time_card: Ekskluder klippekort-kunder
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dict med statistik: approved_count, total_hours, etc.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Hent alle pending entries for case
|
|
|
|
|
entries = WizardService.get_case_entries(case_id, exclude_time_card)
|
|
|
|
|
|
|
|
|
|
if not entries:
|
|
|
|
|
return {
|
|
|
|
|
"approved_count": 0,
|
|
|
|
|
"total_hours": 0.0,
|
|
|
|
|
"case_id": case_id,
|
|
|
|
|
"entries": []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
approved_entries = []
|
|
|
|
|
total_hours = 0.0
|
|
|
|
|
|
|
|
|
|
for entry in entries:
|
|
|
|
|
# Auto-approve med samme timer som original (eller afrundet hvis enabled)
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
|
|
approved_hours = Decimal(str(entry.original_hours))
|
|
|
|
|
|
|
|
|
|
# Afrund hvis enabled
|
|
|
|
|
if settings.TIMETRACKING_AUTO_ROUND:
|
|
|
|
|
increment = Decimal(str(settings.TIMETRACKING_ROUND_INCREMENT))
|
|
|
|
|
|
|
|
|
|
if settings.TIMETRACKING_ROUND_METHOD == "up":
|
|
|
|
|
# Afrund op
|
|
|
|
|
approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_UP') * increment
|
|
|
|
|
elif settings.TIMETRACKING_ROUND_METHOD == "down":
|
|
|
|
|
# Afrund ned
|
|
|
|
|
approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_DOWN') * increment
|
|
|
|
|
else:
|
|
|
|
|
# Nærmeste
|
|
|
|
|
approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_HALF_UP') * increment
|
|
|
|
|
|
|
|
|
|
# Godkend entry
|
|
|
|
|
approval = TModuleTimeApproval(
|
|
|
|
|
time_id=entry.id,
|
|
|
|
|
approved_hours=float(approved_hours)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
approved = WizardService.approve_time_entry(approval, user_id)
|
|
|
|
|
approved_entries.append({
|
|
|
|
|
"id": approved.id,
|
|
|
|
|
"original_hours": float(approved.original_hours),
|
|
|
|
|
"approved_hours": float(approved.approved_hours)
|
|
|
|
|
})
|
|
|
|
|
total_hours += float(approved.approved_hours)
|
|
|
|
|
|
|
|
|
|
# Log bulk approval
|
|
|
|
|
audit.log_event(
|
|
|
|
|
entity_type="case",
|
|
|
|
|
entity_id=str(case_id),
|
|
|
|
|
event_type="bulk_approval",
|
|
|
|
|
details={
|
|
|
|
|
"approved_count": len(approved_entries),
|
|
|
|
|
"total_hours": total_hours
|
|
|
|
|
},
|
|
|
|
|
user_id=user_id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"approved_count": len(approved_entries),
|
|
|
|
|
"total_hours": total_hours,
|
|
|
|
|
"case_id": case_id,
|
|
|
|
|
"entries": approved_entries
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error bulk approving case: {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
|
|
|
@staticmethod
|
|
|
|
|
def get_customer_progress(customer_id: int) -> TModuleWizardProgress:
|
|
|
|
|
"""Hent wizard progress for en kunde"""
|
|
|
|
|
try:
|
|
|
|
|
stats = WizardService.get_customer_stats(customer_id)
|
|
|
|
|
|
|
|
|
|
if not stats:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
|
|
|
|
|
|
|
|
|
# Get current case if any pending entries
|
|
|
|
|
current_case_id = None
|
|
|
|
|
current_case_title = None
|
|
|
|
|
|
|
|
|
|
if stats.pending_count > 0:
|
|
|
|
|
query = """
|
2025-12-10 18:29:13 +01:00
|
|
|
SELECT c.id, c.title
|
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 tmodule_times t
|
|
|
|
|
JOIN tmodule_cases c ON t.case_id = c.id
|
|
|
|
|
WHERE t.customer_id = %s AND t.status = 'pending'
|
|
|
|
|
ORDER BY t.worked_date
|
|
|
|
|
LIMIT 1
|
|
|
|
|
"""
|
|
|
|
|
case = execute_query(query, (customer_id,), fetchone=True)
|
|
|
|
|
if case:
|
|
|
|
|
current_case_id = case['id']
|
|
|
|
|
current_case_title = case['title']
|
|
|
|
|
|
|
|
|
|
return TModuleWizardProgress(
|
|
|
|
|
customer_id=stats.customer_id,
|
|
|
|
|
customer_name=stats.customer_name,
|
|
|
|
|
total_entries=stats.total_entries,
|
|
|
|
|
approved_entries=stats.approved_count,
|
|
|
|
|
pending_entries=stats.pending_count,
|
|
|
|
|
rejected_entries=stats.rejected_count,
|
|
|
|
|
current_case_id=current_case_id,
|
|
|
|
|
current_case_title=current_case_title
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error getting customer progress: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
2025-12-10 18:29:13 +01:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_case_entries(
|
|
|
|
|
case_id: int,
|
|
|
|
|
exclude_time_card: bool = True
|
|
|
|
|
) -> List[TModuleTimeWithContext]:
|
|
|
|
|
"""
|
|
|
|
|
Hent alle pending tidsregistreringer for en specifik case.
|
|
|
|
|
|
|
|
|
|
Bruges til at vise alle timelogs i samme case samtidig.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
case_id: Case ID
|
|
|
|
|
exclude_time_card: Ekskluder klippekort-kunder
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Liste af tidsregistreringer for casen
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
if exclude_time_card:
|
|
|
|
|
query = """
|
|
|
|
|
SELECT t.id, t.vtiger_id, t.case_id, t.customer_id, t.description,
|
|
|
|
|
t.original_hours, t.worked_date, t.user_name, t.status,
|
|
|
|
|
t.approved_hours, t.rounded_to, t.approval_note, t.billable,
|
|
|
|
|
t.approved_at, t.approved_by, t.vtiger_data, t.sync_hash,
|
|
|
|
|
t.created_at, t.updated_at, t.last_synced_at,
|
|
|
|
|
COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
|
|
|
|
|
c.description AS case_description,
|
|
|
|
|
c.status AS case_status,
|
|
|
|
|
c.vtiger_id AS case_vtiger_id,
|
|
|
|
|
cust.name AS customer_name,
|
|
|
|
|
cust.hourly_rate AS customer_rate,
|
|
|
|
|
CONCAT(cont.first_name, ' ', cont.last_name) AS contact_name,
|
|
|
|
|
cont.user_company AS contact_company,
|
|
|
|
|
c.vtiger_data AS case_vtiger_data
|
|
|
|
|
FROM tmodule_times t
|
|
|
|
|
JOIN tmodule_cases c ON t.case_id = c.id
|
|
|
|
|
JOIN tmodule_customers cust ON t.customer_id = cust.id
|
|
|
|
|
LEFT JOIN contacts cont ON cont.vtiger_id = c.vtiger_data->>'contact_id'
|
|
|
|
|
WHERE t.case_id = %s
|
|
|
|
|
AND t.status = 'pending'
|
|
|
|
|
AND t.billable = true
|
|
|
|
|
AND t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
|
|
|
|
AND cust.uses_time_card = false
|
|
|
|
|
ORDER BY t.worked_date, t.id
|
|
|
|
|
"""
|
|
|
|
|
else:
|
|
|
|
|
query = """
|
|
|
|
|
SELECT t.id, t.vtiger_id, t.case_id, t.customer_id, t.description,
|
|
|
|
|
t.original_hours, t.worked_date, t.user_name, t.status,
|
|
|
|
|
t.approved_hours, t.rounded_to, t.approval_note, t.billable,
|
|
|
|
|
t.approved_at, t.approved_by, t.vtiger_data, t.sync_hash,
|
|
|
|
|
t.created_at, t.updated_at, t.last_synced_at,
|
|
|
|
|
COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
|
|
|
|
|
c.description AS case_description,
|
|
|
|
|
c.status AS case_status,
|
|
|
|
|
c.vtiger_id AS case_vtiger_id,
|
|
|
|
|
cust.name AS customer_name,
|
|
|
|
|
cust.hourly_rate AS customer_rate,
|
|
|
|
|
CONCAT(cont.first_name, ' ', cont.last_name) AS contact_name,
|
|
|
|
|
cont.user_company AS contact_company,
|
|
|
|
|
c.vtiger_data AS case_vtiger_data
|
|
|
|
|
FROM tmodule_times t
|
|
|
|
|
JOIN tmodule_cases c ON t.case_id = c.id
|
|
|
|
|
JOIN tmodule_customers cust ON t.customer_id = cust.id
|
|
|
|
|
LEFT JOIN contacts cont ON cont.vtiger_id = c.vtiger_data->>'contact_id'
|
|
|
|
|
WHERE t.case_id = %s
|
|
|
|
|
AND t.status = 'pending'
|
|
|
|
|
AND t.billable = true
|
|
|
|
|
AND t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
|
|
|
|
ORDER BY t.worked_date, t.id
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
results = execute_query(query, (case_id,))
|
|
|
|
|
return [TModuleTimeWithContext(**row) for row in results]
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error getting case entries: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_case_details(case_id: int) -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
Hent komplet case information inkl. alle timelogs og kommentarer.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dict med case info, timelogs (alle statuses), og kommentarer fra vtiger_data
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Hent case info
|
|
|
|
|
case_query = """
|
|
|
|
|
SELECT id, vtiger_id, title, description, status,
|
|
|
|
|
vtiger_data, customer_id
|
|
|
|
|
FROM tmodule_cases
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
"""
|
|
|
|
|
case = execute_query(case_query, (case_id,), fetchone=True)
|
|
|
|
|
|
|
|
|
|
if not case:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
|
|
|
|
|
|
# Hent ALLE timelogs for casen (ikke kun pending)
|
|
|
|
|
timelogs_query = """
|
|
|
|
|
SELECT t.*,
|
|
|
|
|
COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
|
|
|
|
|
c.status AS case_status,
|
|
|
|
|
c.vtiger_id AS case_vtiger_id,
|
|
|
|
|
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.case_id = %s
|
|
|
|
|
ORDER BY t.worked_date DESC, t.created_at DESC
|
|
|
|
|
"""
|
|
|
|
|
timelogs = execute_query(timelogs_query, (case_id,))
|
|
|
|
|
|
|
|
|
|
# Parse case comments from vtiger_data JSON
|
|
|
|
|
case_comments = []
|
|
|
|
|
if case.get('vtiger_data'):
|
|
|
|
|
vtiger_data = case['vtiger_data']
|
|
|
|
|
# vTiger gemmer comments som en array i JSON
|
|
|
|
|
if isinstance(vtiger_data, dict):
|
|
|
|
|
raw_comments = vtiger_data.get('comments', []) or vtiger_data.get('modcomments', [])
|
|
|
|
|
|
|
|
|
|
for comment in raw_comments:
|
|
|
|
|
if isinstance(comment, dict):
|
|
|
|
|
case_comments.append({
|
|
|
|
|
'id': comment.get('modcommentsid', comment.get('id')),
|
|
|
|
|
'comment_text': comment.get('commentcontent', comment.get('comment', '')),
|
|
|
|
|
'creator_name': comment.get('assigned_user_id', comment.get('creator', 'Unknown')),
|
|
|
|
|
'created_at': comment.get('createdtime', comment.get('created_at', ''))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'case_id': case['id'],
|
|
|
|
|
'case_vtiger_id': case['vtiger_id'],
|
|
|
|
|
'case_title': case['title'],
|
|
|
|
|
'case_description': case['description'],
|
|
|
|
|
'case_status': case['status'],
|
|
|
|
|
'timelogs': [TModuleTimeWithContext(**t) for t in timelogs],
|
|
|
|
|
'case_comments': case_comments
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error getting case details: {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
|
|
|
|
|
|
|
|
|
|
|
|
|
# Singleton instance
|
|
|
|
|
wizard = WizardService()
|