work-tracing/app/utils/queries.py
ilia-gurielidze-autstand 9e6d0a6911 first commit
2025-05-05 12:12:46 +04:00

95 lines
3.7 KiB
Python

"""
SQL query building functions for employee workstation activity tracking.
This module contains functions for building SQL queries to calculate working durations.
"""
from flask import current_app
def calculate_duration_sql(time_period):
"""
Generates the core SQL query to calculate working durations.
Uses LEAD() window function to pair 'working' with the next event.
Calculates duration in hours using PostgreSQL functions.
Args:
time_period (str): Time period to group by ('daily', 'weekly', or 'monthly')
Returns:
str: SQL query for calculating durations
"""
# PostgreSQL date functions (already compatible with the database)
period_grouping = {
'daily': "DATE_TRUNC('day', start_time)",
'weekly': "DATE_TRUNC('week', start_time)", # PostgreSQL week starts Monday
'monthly': "DATE_TRUNC('month', start_time)"
}.get(time_period, "DATE_TRUNC('day', start_time)") # Default to daily if invalid
# Calculate duration using EXTRACT(EPOCH FROM interval) / 3600 for hours
duration_calculation = "EXTRACT(EPOCH FROM (next_event_time - start_time)) / 3600.0"
# Use public schema explicitly, ensure proper aggregation by user and period
sql_query = f"""
WITH EventPairs AS (
SELECT
"user",
ts AS start_time,
state,
LEAD(ts) OVER (PARTITION BY "user" ORDER BY ts) AS next_event_time,
LEAD(state) OVER (PARTITION BY "user" ORDER BY ts) AS next_event_state
FROM public.work_events
ORDER BY "user", ts
),
CalculatedDurations AS (
SELECT
"user",
{period_grouping} AS period_start,
SUM(
CASE
WHEN state = 'working' AND next_event_time IS NOT NULL THEN
{duration_calculation}
ELSE 0 -- Ignore intervals starting with 'stopped' or without a following event
END
) AS total_hours,
MIN(CASE WHEN state = 'working' THEN start_time END) AS first_login_time
FROM EventPairs
WHERE state = 'working' -- Only consider intervals that start with 'working'
GROUP BY "user", period_start
)
-- Final aggregation to ensure one row per user per period
SELECT
"user",
period_start,
SUM(total_hours) AS total_hours,
MIN(first_login_time) AS first_login_time
FROM CalculatedDurations
GROUP BY "user", period_start
ORDER BY "user", period_start DESC;
"""
# Add debug logging to see the SQL query
current_app.logger.info(f"Generated SQL query: {sql_query}")
return sql_query
def filter_sql_by_user(base_sql, user):
"""
Applies a user filter to the SQL query safely.
Args:
base_sql (str): The base SQL query to modify
user (str): Username to filter by
Returns:
str: SQL query with user filter applied
"""
# Find the position of GROUP BY to insert WHERE clause correctly
group_by_pos = base_sql.find("GROUP BY")
if group_by_pos != -1:
# Make the user filter case-insensitive using LOWER()
where_clause = "WHERE state = 'working' AND LOWER(\"user\") = LOWER(:user)\n "
# Replace the generic WHERE clause with the user-specific one
filtered_sql = base_sql[:base_sql.find("WHERE")] + where_clause + base_sql[group_by_pos:]
return filtered_sql
else:
# Should not happen with the current query structure, but handle defensively
return base_sql # Return original if GROUP BY not found