392 lines
14 KiB
Python
392 lines
14 KiB
Python
"""
|
|
Timeline view implementation for the Content Scheduler.
|
|
Provides interactive Gantt charts and progress tracking visualization.
|
|
"""
|
|
|
|
import streamlit as st
|
|
import plotly.figure_factory as ff
|
|
import plotly.graph_objects as go
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Dict, Any, Optional
|
|
import pandas as pd
|
|
import json
|
|
|
|
# Use unified database models
|
|
from lib.database.models import ContentItem, Schedule, ScheduleStatus, get_session
|
|
|
|
class TimelineView:
|
|
"""Interactive timeline view with Gantt charts and progress tracking."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the timeline view."""
|
|
self.session = get_session()
|
|
|
|
def render(self):
|
|
"""Render the timeline view."""
|
|
st.header("Schedule Timeline")
|
|
|
|
# Timeline controls
|
|
self._render_timeline_controls()
|
|
|
|
# Main timeline view
|
|
self._render_timeline()
|
|
|
|
# Progress tracking
|
|
self._render_progress_tracking()
|
|
|
|
def _render_timeline_controls(self):
|
|
"""Render timeline control options."""
|
|
col1, col2, col3 = st.columns([2, 2, 1])
|
|
|
|
with col1:
|
|
view_type = st.selectbox(
|
|
"View Type",
|
|
["Gantt Chart", "Timeline", "List View"],
|
|
help="Select the type of timeline visualization"
|
|
)
|
|
|
|
with col2:
|
|
date_range = st.date_input(
|
|
"Date Range",
|
|
value=(
|
|
datetime.now().date(),
|
|
datetime.now().date() + timedelta(days=7)
|
|
),
|
|
help="Select the date range to display"
|
|
)
|
|
|
|
with col3:
|
|
if st.button("Export", help="Export timeline data"):
|
|
self._export_timeline_data()
|
|
|
|
def _render_timeline(self):
|
|
"""Render the main timeline visualization."""
|
|
# Get schedules for the selected date range
|
|
schedules = self._get_schedules_for_timeline()
|
|
|
|
if not schedules:
|
|
st.info("No schedules found for the selected date range.")
|
|
return
|
|
|
|
# Create Gantt chart data
|
|
gantt_data = self._create_gantt_data(schedules)
|
|
|
|
# Create and display Gantt chart
|
|
fig = self._create_gantt_chart(gantt_data)
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
# Display schedule details
|
|
self._render_schedule_details(schedules)
|
|
|
|
def _render_progress_tracking(self):
|
|
"""Render progress tracking visualization."""
|
|
st.subheader("Progress Tracking")
|
|
|
|
# Progress metrics
|
|
col1, col2, col3 = st.columns(3)
|
|
|
|
with col1:
|
|
self._render_progress_metric(
|
|
"Completed",
|
|
self._get_completed_count(),
|
|
"green"
|
|
)
|
|
|
|
with col2:
|
|
self._render_progress_metric(
|
|
"In Progress",
|
|
self._get_in_progress_count(),
|
|
"orange"
|
|
)
|
|
|
|
with col3:
|
|
self._render_progress_metric(
|
|
"Pending",
|
|
self._get_pending_count(),
|
|
"blue"
|
|
)
|
|
|
|
# Progress chart
|
|
self._render_progress_chart()
|
|
|
|
def _get_schedules_for_timeline(self) -> List[Schedule]:
|
|
"""Get schedules for the timeline view."""
|
|
try:
|
|
# Get date range from session state or use default
|
|
if hasattr(st.session_state, 'date_range') and st.session_state.date_range:
|
|
start_date, end_date = st.session_state.date_range
|
|
else:
|
|
start_date = datetime.now().date()
|
|
end_date = start_date + timedelta(days=7)
|
|
|
|
# Convert to datetime
|
|
start_datetime = datetime.combine(start_date, datetime.min.time())
|
|
end_datetime = datetime.combine(end_date, datetime.max.time())
|
|
|
|
# Query schedules from unified database
|
|
schedules = self.session.query(Schedule).filter(
|
|
Schedule.scheduled_time >= start_datetime,
|
|
Schedule.scheduled_time <= end_datetime
|
|
).all()
|
|
|
|
return schedules
|
|
|
|
except Exception as e:
|
|
st.error(f"Failed to get schedules: {str(e)}")
|
|
return []
|
|
|
|
def _create_gantt_data(self, schedules: List[Schedule]) -> List[Dict[str, Any]]:
|
|
"""Create data for Gantt chart."""
|
|
gantt_data = []
|
|
|
|
for schedule in schedules:
|
|
# Get content item details
|
|
content_item = self.session.query(ContentItem).filter(
|
|
ContentItem.id == schedule.content_item_id
|
|
).first()
|
|
|
|
if content_item:
|
|
# Calculate task duration
|
|
duration = timedelta(hours=1) # Default duration
|
|
|
|
# Create task data
|
|
task = {
|
|
'Task': content_item.title[:50] + "..." if len(content_item.title) > 50 else content_item.title,
|
|
'Start': schedule.scheduled_time,
|
|
'Finish': schedule.scheduled_time + duration,
|
|
'Resource': schedule.status.value,
|
|
'Status': schedule.status.value,
|
|
'Progress': self._calculate_progress(schedule)
|
|
}
|
|
|
|
gantt_data.append(task)
|
|
|
|
return gantt_data
|
|
|
|
def _create_gantt_chart(self, gantt_data: List[Dict[str, Any]]) -> go.Figure:
|
|
"""Create Gantt chart visualization."""
|
|
if not gantt_data:
|
|
# Return empty figure
|
|
fig = go.Figure()
|
|
fig.update_layout(
|
|
title='Content Schedule Timeline',
|
|
xaxis_title='Timeline',
|
|
yaxis_title='Status',
|
|
height=400
|
|
)
|
|
return fig
|
|
|
|
# Convert data to DataFrame
|
|
df = pd.DataFrame(gantt_data)
|
|
|
|
# Create Gantt chart
|
|
fig = ff.create_gantt(
|
|
df,
|
|
index_col='Resource',
|
|
show_colorbar=True,
|
|
group_tasks=True,
|
|
showgrid_x=True,
|
|
showgrid_y=True
|
|
)
|
|
|
|
# Update layout
|
|
fig.update_layout(
|
|
title='Content Schedule Timeline',
|
|
xaxis_title='Timeline',
|
|
yaxis_title='Status',
|
|
height=400,
|
|
showlegend=True
|
|
)
|
|
|
|
return fig
|
|
|
|
def _render_schedule_details(self, schedules: List[Schedule]):
|
|
"""Render detailed schedule information."""
|
|
st.subheader("Schedule Details")
|
|
|
|
for schedule in schedules:
|
|
# Get content item details
|
|
content_item = self.session.query(ContentItem).filter(
|
|
ContentItem.id == schedule.content_item_id
|
|
).first()
|
|
|
|
if content_item:
|
|
with st.expander(f"{content_item.title} - {schedule.status.value}"):
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
st.write("**Schedule Information**")
|
|
st.write(f"Content Type: {content_item.content_type.value if content_item.content_type else 'Unknown'}")
|
|
st.write(f"Status: {schedule.status.value}")
|
|
st.write(f"Scheduled Time: {schedule.scheduled_time}")
|
|
st.write(f"Priority: {schedule.priority}")
|
|
if schedule.recurrence:
|
|
st.write(f"Recurrence: {schedule.recurrence}")
|
|
|
|
with col2:
|
|
st.write("**Progress**")
|
|
progress = self._calculate_progress(schedule)
|
|
st.progress(progress / 100)
|
|
st.write(f"Progress: {progress:.1f}%")
|
|
|
|
# Action buttons
|
|
col2a, col2b = st.columns(2)
|
|
with col2a:
|
|
if st.button(f"Edit {schedule.id}", key=f"edit_{schedule.id}"):
|
|
st.session_state.edit_schedule_id = schedule.id
|
|
with col2b:
|
|
if st.button(f"Cancel {schedule.id}", key=f"cancel_{schedule.id}"):
|
|
self._cancel_schedule(schedule.id)
|
|
|
|
def _render_progress_metric(self, label: str, value: int, color: str):
|
|
"""Render a progress metric."""
|
|
st.metric(label, value)
|
|
|
|
def _render_progress_chart(self):
|
|
"""Render progress chart visualization."""
|
|
try:
|
|
# Get progress data
|
|
progress_data = self._get_progress_data()
|
|
|
|
if progress_data:
|
|
# Create pie chart
|
|
labels = list(progress_data.keys())
|
|
values = list(progress_data.values())
|
|
|
|
fig = go.Figure(data=[go.Pie(labels=labels, values=values)])
|
|
fig.update_layout(
|
|
title="Schedule Status Distribution",
|
|
height=300
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
else:
|
|
st.info("No progress data available.")
|
|
|
|
except Exception as e:
|
|
st.error(f"Error rendering progress chart: {str(e)}")
|
|
|
|
def _calculate_progress(self, schedule: Schedule) -> float:
|
|
"""Calculate progress percentage for a schedule."""
|
|
try:
|
|
if schedule.status == ScheduleStatus.COMPLETED:
|
|
return 100.0
|
|
elif schedule.status == ScheduleStatus.RUNNING:
|
|
return 50.0
|
|
elif schedule.status == ScheduleStatus.FAILED:
|
|
return 0.0
|
|
else: # PENDING
|
|
return 0.0
|
|
|
|
except Exception as e:
|
|
st.error(f"Error calculating progress: {str(e)}")
|
|
return 0.0
|
|
|
|
def _get_completed_count(self) -> int:
|
|
"""Get count of completed schedules."""
|
|
try:
|
|
return self.session.query(Schedule).filter(
|
|
Schedule.status == ScheduleStatus.COMPLETED
|
|
).count()
|
|
except Exception as e:
|
|
st.error(f"Error getting completed count: {str(e)}")
|
|
return 0
|
|
|
|
def _get_in_progress_count(self) -> int:
|
|
"""Get count of in-progress schedules."""
|
|
try:
|
|
return self.session.query(Schedule).filter(
|
|
Schedule.status == ScheduleStatus.RUNNING
|
|
).count()
|
|
except Exception as e:
|
|
st.error(f"Error getting in-progress count: {str(e)}")
|
|
return 0
|
|
|
|
def _get_pending_count(self) -> int:
|
|
"""Get count of pending schedules."""
|
|
try:
|
|
return self.session.query(Schedule).filter(
|
|
Schedule.status == ScheduleStatus.PENDING
|
|
).count()
|
|
except Exception as e:
|
|
st.error(f"Error getting pending count: {str(e)}")
|
|
return 0
|
|
|
|
def _get_progress_data(self) -> Dict[str, int]:
|
|
"""Get progress data for visualization."""
|
|
try:
|
|
progress_data = {}
|
|
|
|
# Count schedules by status
|
|
for status in ScheduleStatus:
|
|
count = self.session.query(Schedule).filter(
|
|
Schedule.status == status
|
|
).count()
|
|
progress_data[status.value] = count
|
|
|
|
return progress_data
|
|
|
|
except Exception as e:
|
|
st.error(f"Error getting progress data: {str(e)}")
|
|
return {}
|
|
|
|
def _cancel_schedule(self, schedule_id: int):
|
|
"""Cancel a schedule."""
|
|
try:
|
|
schedule = self.session.query(Schedule).filter(
|
|
Schedule.id == schedule_id
|
|
).first()
|
|
|
|
if schedule:
|
|
schedule.status = ScheduleStatus.CANCELLED
|
|
self.session.commit()
|
|
st.success(f"Schedule {schedule_id} cancelled successfully!")
|
|
st.experimental_rerun()
|
|
else:
|
|
st.error("Schedule not found.")
|
|
|
|
except Exception as e:
|
|
st.error(f"Error cancelling schedule: {str(e)}")
|
|
self.session.rollback()
|
|
|
|
def _export_timeline_data(self):
|
|
"""Export timeline data."""
|
|
try:
|
|
schedules = self._get_schedules_for_timeline()
|
|
|
|
if schedules:
|
|
# Prepare export data
|
|
export_data = []
|
|
|
|
for schedule in schedules:
|
|
content_item = self.session.query(ContentItem).filter(
|
|
ContentItem.id == schedule.content_item_id
|
|
).first()
|
|
|
|
if content_item:
|
|
export_data.append({
|
|
'Schedule ID': schedule.id,
|
|
'Title': content_item.title,
|
|
'Content Type': content_item.content_type.value if content_item.content_type else 'Unknown',
|
|
'Scheduled Time': schedule.scheduled_time.isoformat(),
|
|
'Status': schedule.status.value,
|
|
'Priority': schedule.priority,
|
|
'Recurrence': schedule.recurrence or 'None'
|
|
})
|
|
|
|
# Convert to CSV
|
|
df = pd.DataFrame(export_data)
|
|
csv = df.to_csv(index=False)
|
|
|
|
# Provide download
|
|
st.download_button(
|
|
label="Download Timeline Data",
|
|
data=csv,
|
|
file_name=f"timeline_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
|
|
mime="text/csv"
|
|
)
|
|
else:
|
|
st.warning("No data to export.")
|
|
|
|
except Exception as e:
|
|
st.error(f"Error exporting data: {str(e)}") |