95 lines
3.7 KiB
Python
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 |