Files
TimeTracker/app/services/client_approval_service.py
T
2025-11-29 07:13:23 +01:00

136 lines
5.4 KiB
Python

"""
Client Time Entry Approval Service
Handles client-side approval workflow for time entries
"""
from typing import Dict, List, Any, Optional
from datetime import datetime, timedelta
from app import db
from app.models.client_time_approval import ClientTimeApproval, ClientApprovalPolicy, ClientApprovalStatus
from app.models import TimeEntry, Client
import logging
logger = logging.getLogger(__name__)
class ClientApprovalService:
"""Service for managing client-side time entry approvals"""
def request_approval(self, time_entry_id: int, requested_by: int, comment: str = None) -> Dict[str, Any]:
"""Request client approval for a time entry"""
time_entry = TimeEntry.query.get(time_entry_id)
if not time_entry:
return {"success": False, "message": "Time entry not found", "error": "not_found"}
project = time_entry.project
if not project or not project.client_id:
return {"success": False, "message": "Project has no associated client", "error": "no_client"}
client = Client.query.get(project.client_id)
if not client:
return {"success": False, "message": "Client not found", "error": "client_not_found"}
# Check if already pending
existing = ClientTimeApproval.query.filter_by(
time_entry_id=time_entry_id, status=ClientApprovalStatus.PENDING
).first()
if existing:
return {"success": False, "message": "Approval already pending", "error": "already_pending"}
# Create approval request
approval = ClientTimeApproval(
time_entry_id=time_entry_id,
project_id=project.id,
client_id=client.id,
requested_by=requested_by,
status=ClientApprovalStatus.PENDING,
request_comment=comment,
)
db.session.add(approval)
db.session.commit()
# Notify client contacts
self._notify_client_contacts(client, approval)
return {"success": True, "message": "Approval requested", "approval": approval.to_dict()}
def approve(self, approval_id: int, contact_id: int, comment: str = None) -> Dict[str, Any]:
"""Approve a time entry (client-side)"""
approval = ClientTimeApproval.query.get(approval_id)
if not approval:
return {"success": False, "message": "Approval not found", "error": "not_found"}
if approval.status != ClientApprovalStatus.PENDING:
return {"success": False, "message": "Approval is not pending", "error": "invalid_status"}
approval.approve(contact_id, comment)
self._notify_requester(approval, "approved", comment)
return {"success": True, "message": "Time entry approved", "approval": approval.to_dict()}
def reject(self, approval_id: int, contact_id: int, reason: str) -> Dict[str, Any]:
"""Reject a time entry (client-side)"""
approval = ClientTimeApproval.query.get(approval_id)
if not approval:
return {"success": False, "message": "Approval not found", "error": "not_found"}
if approval.status != ClientApprovalStatus.PENDING:
return {"success": False, "message": "Approval is not pending", "error": "invalid_status"}
approval.reject(contact_id, reason)
self._notify_requester(approval, "rejected", reason)
return {"success": True, "message": "Time entry rejected", "approval": approval.to_dict()}
def get_pending_approvals_for_client(self, client_id: int) -> List[ClientTimeApproval]:
"""Get pending approvals for a client"""
return (
ClientTimeApproval.query.filter_by(client_id=client_id, status=ClientApprovalStatus.PENDING)
.order_by(ClientTimeApproval.requested_at.desc())
.all()
)
def _notify_client_contacts(self, client: Client, approval: ClientTimeApproval):
"""Send notifications to client contacts"""
from app.models import Contact
from app.utils.notification_service import NotificationService
service = NotificationService()
# Get client contacts
contacts = Contact.query.filter_by(client_id=client.id, is_active=True).all()
for contact in contacts:
if contact.email:
# Send email notification
from app.utils.email import send_email
try:
send_email(
to=contact.email,
subject=f"Time Entry Approval Requested - {approval.time_entry.project.name}",
template="email/client_approval_request.html",
approval=approval,
contact=contact,
)
except Exception as e:
logger.error(f"Error sending approval email to {contact.email}: {e}")
def _notify_requester(self, approval: ClientTimeApproval, status: str, reason: str = None):
"""Send notification to requester"""
from app.utils.notification_service import NotificationService
service = NotificationService()
message = f"Client has {status} time entry {approval.time_entry_id}."
if reason:
message += f" Reason: {reason}"
service.send_notification(
user_id=approval.requested_by,
title=f"Time Entry {status.title()}",
message=message,
type="success" if status == "approved" else "error",
priority="normal",
)