Compare commits

...

2 Commits

Author SHA1 Message Date
e1b817252c refactor: replace month dropdown with week dropdown and update related components
feat: add feedback functionality and modal for category feedback
style: add feedback button styles in CSS
docs: add explanation tab for dashboard usage instructions
2025-09-07 20:16:56 +02:00
2d73ae8d63 added some more changes 2025-09-06 07:27:08 +02:00
15 changed files with 289 additions and 91 deletions

View File

@ -59,3 +59,17 @@ hr {
grid-template-columns: 1fr;
}
}
.feedback-button {
background-color: #007bff;
color: white;
border: none;
padding: 5px 10px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 12px;
margin: 2px 2px;
cursor: pointer;
border-radius: 4px;
}

2
data/feedback.csv Normal file
View File

@ -0,0 +1,2 @@
timestamp,category,comment
2025-09-07 20:02:38,groceries,"ABZT please change this air to P1231313"
1 timestamp category comment
2 2025-09-07 20:02:38 groceries ABZT please change this air to P1231313

View File

@ -1,6 +1,6 @@
# initial commit
from dash import Dash
from dash_bootstrap_components.themes import BOOTSTRAP
import dash_bootstrap_components as dbc
from src.components.layout import create_layout
from src.data.loader_gz import load_spc_data
@ -21,7 +21,7 @@ def main() -> None:
# load the data and create the data manager
data = load_spc_data(config["DATA_PATH"])
app = Dash(external_stylesheets=[BOOTSTRAP])
app = Dash(external_stylesheets=[dbc.themes.LUX])
app.title = "Reliability Dashboard"
app.layout = create_layout(app, data)
app.run()

Binary file not shown.

View File

@ -12,15 +12,15 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
Output(ids.BAR_CHART, "children"),
[
Input(ids.YEAR_DROPDOWN, "value"),
Input(ids.MONTH_DROPDOWN, "value"),
Input(ids.WEEK_DROPDOWN, "value"),
Input(ids.CATEGORY_DROPDOWN, "value"),
],
)
def update_bar_chart(
years: list[str], months: list[str], categories: list[str]
years: list[str], weeks: list[str], categories: list[str]
) -> html.Div:
filtered_data = data.query(
"year in @years and month in @months and category in @categories"
"year in @years and week in @weeks and category in @categories"
)
if filtered_data.shape[0] == 0:

View File

@ -14,12 +14,12 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
Output(ids.CATEGORY_DROPDOWN, "value"),
[
Input(ids.YEAR_DROPDOWN, "value"),
Input(ids.MONTH_DROPDOWN, "value"),
Input(ids.WEEK_DROPDOWN, "value"),
Input(ids.SELECT_ALL_CATEGORIES_BUTTON, "n_clicks"),
],
)
def select_all_categories(years: list[str], months: list[str], _: int) -> list[str]:
filtered_data = data.query("year in @years and month in @months")
def select_all_categories(years: list[str], weeks: list[str], _: int) -> list[str]:
filtered_data = data.query("year in @years and week in @weeks")
return sorted(set(filtered_data[DataSchema.CATEGORY].tolist()))
return html.Div(

View File

@ -1,40 +1,123 @@
import pandas as pd
import plotly.express as px
from dash import Dash, dcc, html
from dash.dependencies import Input, Output
from dash import Dash, dcc, html, dash_table, Input, Output, State, callback_context
from datetime import datetime
import os
import dash_bootstrap_components as dbc
from ..data.loader import DataSchema
from . import ids
def render(app: Dash, data: pd.DataFrame) -> html.Div:
@app.callback(
Output(ids.BAR_CHART, "children"),
Output(ids.DATA_TABLE, "children"),
[
Input(ids.YEAR_DROPDOWN, "value"),
Input(ids.MONTH_DROPDOWN, "value"),
Input(ids.WEEK_DROPDOWN, "value"),
Input(ids.CATEGORY_DROPDOWN, "value"),
],
)
def update_data_table(
years: list[str], months: list[str], categories: list[str]
years: list[str], weeks: list[str], categories: list[str]
) -> html.Div:
filtered_data = data.query(
"year in @years and month in @months and category in @categories"
"year in @years and week in @weeks and category in @categories"
)
if filtered_data.shape[0] == 0:
return html.Div("No data selected.", id=ids.DATA_TABLE)
def create_pivot_table() -> pd.DataFrame:
pt = filtered_data.pivot_table(
values=DataSchema.AMOUNT,
index=[DataSchema.CATEGORY],
aggfunc="sum",
fill_value=0,
dropna=False,
)
return pt.reset_index().sort_values(DataSchema.AMOUNT, ascending=False)
return html.Div("No data selected.")
return html.Div(dcc.Data_table(data=create_pivot_table(data), id=ids.DATA_TABLE)
pt = filtered_data.pivot_table(
values=DataSchema.AMOUNT,
index=[DataSchema.CATEGORY],
aggfunc="sum",
fill_value=0,
dropna=False,
).reset_index().sort_values(DataSchema.AMOUNT, ascending=False)
return html.Div(id=ids.DATA_TABLE)
columns = [{"name": i.capitalize(), "id": i} for i in pt.columns]
return dash_table.DataTable(
id=ids.CATEGORY_TABLE,
data=pt.to_dict("records"),
columns=columns,
row_selectable='single',
selected_rows=[]
)
@app.callback(
Output(ids.FEEDBACK_MODAL, "is_open"),
Output(ids.FEEDBACK_MESSAGE, "children"),
Input(ids.CATEGORY_TABLE, "selected_rows"),
Input(ids.SAVE_FEEDBACK_BUTTON_POPUP, "n_clicks"),
Input(ids.CLOSE_FEEDBACK_BUTTON_POPUP, "n_clicks"),
State(ids.FEEDBACK_MODAL, "is_open"),
State(ids.CATEGORY_TABLE, "data"),
State(ids.FEEDBACK_INPUT, "value"),
)
def handle_feedback_modal(selected_rows, save_clicks, close_clicks, is_open, data, comment):
ctx = callback_context
if not ctx.triggered:
return False, ""
triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
if triggered_id == ids.CATEGORY_TABLE and selected_rows:
return True, ""
if triggered_id == ids.SAVE_FEEDBACK_BUTTON_POPUP and selected_rows:
selected_row_data = data[selected_rows[0]]
category = selected_row_data[DataSchema.CATEGORY]
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if not comment:
comment = ""
feedback_data = f'{timestamp},{category},"{comment}"\n'
file_path = "data/feedback.csv"
try:
is_new_file = not os.path.exists(file_path) or os.path.getsize(file_path) == 0
with open(file_path, "a") as f:
if is_new_file:
f.write("timestamp,category,comment\n")
f.write(feedback_data)
message = f"Feedback for category '{category}' has been saved successfully at {timestamp}."
return False, message
except Exception as e:
return True, "An error occurred while saving feedback."
if triggered_id == ids.CLOSE_FEEDBACK_BUTTON_POPUP:
return False, ""
return is_open, ""
@app.callback(
Output("feedback-category-label", "children"),
Input(ids.CATEGORY_TABLE, "selected_rows"),
State(ids.CATEGORY_TABLE, "data"),
prevent_initial_call=True
)
def update_feedback_category_label(selected_rows, data):
if selected_rows:
category = data[selected_rows[0]][DataSchema.CATEGORY]
return f"Category: {category}"
return "Category: "
modal = dbc.Modal(
[
dbc.ModalHeader(dbc.ModalTitle("Submit Feedback")),
dbc.ModalBody([
html.H6("Category: ", id="feedback-category-label"),
dcc.Input(id=ids.FEEDBACK_INPUT, type='text', placeholder='Enter feedback...', style={'width': '100%'}),
]),
dbc.ModalFooter([
dbc.Button("Save", id=ids.SAVE_FEEDBACK_BUTTON_POPUP, className="ms-auto", n_clicks=0),
dbc.Button("Close", id=ids.CLOSE_FEEDBACK_BUTTON_POPUP, className="ms-auto", n_clicks=0)
]),
],
id=ids.FEEDBACK_MODAL,
is_open=False,
)
return html.Div([
html.Div(id=ids.DATA_TABLE),
html.Div(id=ids.FEEDBACK_MESSAGE),
modal
])

View File

@ -0,0 +1,22 @@
from dash import Dash, dcc, html
def render(_: Dash) -> html.Div:
return html.Div([
dcc.Markdown("""
### How to use this Dashboard
**Dashboard Tab:**
- Use the dropdowns at the top to filter the data by year, week, and category.
- The bar chart shows the total amount per category for the selected period.
- The data table shows the detailed data.
**Feedback:**
- In the data table on the Dashboard tab, you can select a row to provide feedback.
- A popup will appear where you can enter your comments for the selected category.
- Click "Save" to store your feedback.
**Feedback Tab:**
- This tab displays all the feedback that has been submitted.
- The table on this tab updates automatically every 5 seconds.
""")
])

View File

@ -0,0 +1,27 @@
from dash import Dash, dcc, html, dash_table
from dash.dependencies import Input, Output
import pandas as pd
def render(app: Dash) -> html.Div:
@app.callback(
Output("feedback-table-content", "children"),
Input("feedback-interval", "n_intervals")
)
def update_feedback_table(_):
try:
feedback_df = pd.read_csv("data/feedback.csv")
return dash_table.DataTable(
data=feedback_df.to_dict("records"),
columns=[{"name": i, "id": i} for i in feedback_df.columns],
page_size=10,
)
except FileNotFoundError:
return html.P("No feedback submitted yet.")
except Exception as e:
return html.P(f"An error occurred: {e}")
return html.Div([
html.H4("Submitted Feedback"),
html.Div(id="feedback-table-content"),
dcc.Interval(id="feedback-interval", interval=5 * 1000, n_intervals=0) # 5 seconds
])

View File

@ -5,8 +5,16 @@ DATA_TABLE = "data-table"
SELECT_ALL_CATEGORIES_BUTTON = "select-all-categories-button"
CATEGORY_DROPDOWN = "category-dropdown"
SELECT_ALL_MONTHS_BUTTON = "select-all-months-button"
MONTH_DROPDOWN = "month-dropdown"
YEAR_DROPDOWN = "year-dropdown"
SELECT_ALL_YEARS_BUTTON = "select-all-years-button"
WEEK_DROPDOWN = "week-dropdown"
SELECT_ALL_WEEKS_BUTTON = "select-all-weeks-button"
CATEGORY_TABLE = "category-table"
FEEDBACK_INPUT = "feedback-input"
FEEDBACK_MESSAGE = "feedback-message"
FEEDBACK_MODAL = "feedback-modal"
SAVE_FEEDBACK_BUTTON_POPUP = "save-feedback-button-popup"
CLOSE_FEEDBACK_BUTTON_POPUP = "close-feedback-button-popup"

View File

@ -1,29 +1,48 @@
import pandas as pd
from dash import Dash, html
from dash import Dash, dcc, html
from src.components import (
bar_chart,
category_dropdown,
month_dropdown,
data_table,
year_dropdown,
week_dropdown,
category_dropdown,
feedback_tab,
explanation_tab,
)
def create_layout(app: Dash, data: pd.DataFrame) -> html.Div:
tab_content_style = {'height': 'calc(100vh - 220px)', 'overflowY': 'auto', 'padding': '15px'}
return html.Div(
className="app-div",
children=[
html.H1(app.title),
html.Hr(),
html.Div(
className="dropdown-container",
children=[
year_dropdown.render(app, data),
month_dropdown.render(app, data),
category_dropdown.render(app, data),
],
),
bar_chart.render(app, data),
data_table.render(app, data),
dcc.Tabs(id="tabs", value='tab-dashboard', children=[
dcc.Tab(label='Dashboard', value='tab-dashboard', children=[
html.Div(style=tab_content_style, children=[
html.Div(
className="dropdown-container",
children=[
year_dropdown.render(app, data),
week_dropdown.render(app, data),
category_dropdown.render(app, data),
],
),
bar_chart.render(app, data),
data_table.render(app, data),
])
]),
dcc.Tab(label='Feedback', value='tab-feedback', children=[
html.Div(style=tab_content_style, children=[
feedback_tab.render(app)
])
]),
dcc.Tab(label='Explanation', value='tab-explanation', children=[
html.Div(style=tab_content_style, children=[
explanation_tab.render(app)
])
]),
]),
],
)

View File

@ -1,40 +0,0 @@
import pandas as pd
from dash import Dash, dcc, html
from dash.dependencies import Input, Output
from ..data.loader import DataSchema
from . import ids
def render(app: Dash, data: pd.DataFrame) -> html.Div:
all_months: list[str] = data[DataSchema.MONTH].tolist()
unique_months = sorted(set(all_months))
@app.callback(
Output(ids.MONTH_DROPDOWN, "value"),
[
Input(ids.YEAR_DROPDOWN, "value"),
Input(ids.SELECT_ALL_MONTHS_BUTTON, "n_clicks"),
],
)
def select_all_months(years: list[str], _: int) -> list[str]:
filtered_data = data.query("year in @years")
return sorted(set(filtered_data[DataSchema.MONTH].tolist()))
return html.Div(
children=[
html.H6("Month"),
dcc.Dropdown(
id=ids.MONTH_DROPDOWN,
options=[{"label": month, "value": month} for month in unique_months],
value=unique_months,
multi=True,
),
html.Button(
className="dropdown-button",
children=["Select All"],
id=ids.SELECT_ALL_MONTHS_BUTTON,
n_clicks=0,
),
]
)

View File

@ -0,0 +1,53 @@
import pandas as pd
from dash import Dash, dcc, html
from dash.dependencies import Input, Output
from datetime import datetime, timedelta
from ..data.loader import DataSchema
from . import ids
def render(app: Dash, data: pd.DataFrame) -> html.Div:
all_weeks: list[str] = data[DataSchema.WEEK].tolist()
unique_weeks = sorted(set(all_weeks), reverse=True)
# determine default weeks (last 13)
today = datetime.now()
last_13_weeks = []
for i in range(13):
date = today - timedelta(weeks=i)
year, week, _ = date.isocalendar()
last_13_weeks.append(f"{year}{week:02d}")
default_weeks = [week for week in last_13_weeks if week in unique_weeks]
if not default_weeks and unique_weeks:
default_weeks = unique_weeks[:13]
@app.callback(
Output(ids.WEEK_DROPDOWN, "value"),
[
Input(ids.YEAR_DROPDOWN, "value"),
Input(ids.SELECT_ALL_WEEKS_BUTTON, "n_clicks"),
],
)
def select_all_weeks(years: list[str], _: int) -> list[str]:
filtered_data = data.query("year in @years")
return sorted(set(filtered_data[DataSchema.WEEK].tolist()))
return html.Div(
children=[
html.H6("Week"),
dcc.Dropdown(
id=ids.WEEK_DROPDOWN,
options=[{"label": f"{week[:4]}-{week[4:]}", "value": week} for week in unique_weeks],
value=default_weeks,
multi=True,
),
html.Button(
className="dropdown-button",
children=["Select All"],
id=ids.SELECT_ALL_WEEKS_BUTTON,
n_clicks=0,
),
]
)

View File

@ -7,6 +7,7 @@ class DataSchema:
DATE = "date"
MONTH = "month"
YEAR = "year"
WEEK = "week"
def load_transaction_data(path: str) -> pd.DataFrame:
@ -22,4 +23,8 @@ def load_transaction_data(path: str) -> pd.DataFrame:
)
data[DataSchema.YEAR] = data[DataSchema.DATE].dt.year.astype(str)
data[DataSchema.MONTH] = data[DataSchema.DATE].dt.month.astype(str)
data[DataSchema.WEEK] = (
data[DataSchema.DATE].dt.isocalendar().year.astype(str)
+ data[DataSchema.DATE].dt.isocalendar().week.astype(str).str.zfill(2)
)
return data

View File

@ -6,6 +6,7 @@ class DataSchema:
DATE = "date"
MONTH = "month"
YEAR = "year"
WEEK = "week"
class SPC_Schema:
FC = "FC"
@ -30,4 +31,8 @@ def load_spc_data(path: str) -> pd.DataFrame:
)
data[DataSchema.YEAR] = data[DataSchema.DATE].dt.year.astype(str)
data[DataSchema.MONTH] = data[DataSchema.DATE].dt.month.astype(str)
data[DataSchema.WEEK] = (
data[DataSchema.DATE].dt.isocalendar().year.astype(str)
+ data[DataSchema.DATE].dt.isocalendar().week.astype(str).str.zfill(2)
)
return data