Call panel for attendance queue management

Hi,

I developed a system to manage a service queue to sound a bell and call the next person to be attended, which will work as follows:

  1. a single streamlit app will have 3 different view modes: receptionist, attendant (1 to 6) and the. call panel. The receptionist computer will load its view and the call panel on a second screen.

  2. a pre-schedule is loaded into the receptionist computer. As the people will arrive, the receptionist confirms the arrival and/or if it is a prioritary attendance.

  3. each attendant will then click a button to call the next person queued by the receptionist on her computer. Then there are buttons to initiate/finish the attendance.

  4. all actions will change the csv file loaded on the non panel views, which will log times (arrival, service initiated, service finished), forcing a refresh on the app. There are also buttons to cancel confirm arrival, cancel call.

  5. the panel view loops every 10 seconds to see if there is any confirmed arrival with no service initiated waiting, and then displays the name and sounds the bell.

I am attaching the source here so you can have a grasp of what I am trying to do. Still needs the logo file and the sound to be able to run.

My questions, as a wannabe-chatgpt-developer are:

  1. what kinds of problem may arise with this code - specially with regard to many people simultaneously doing changes to the csv file and the loaded onscreen schedule? How to avoid conflicts between session states among different machines and generate impossible states (say, a person has been called to a desk, but the buttons to initiate attendance for this people won’t show up?

Please feel free to criticize everything you don’t agree or find strange or ineffective or could be done in a simpler way… I hope I can make this more robust to use with our public.

Thank you so much!

Mart

import streamlit as st
import pandas as pd
from datetime import datetime
import os
import pygame
import time
import numpy as np

pygame.mixer.init()
CSV_FILE = 'agenda.csv'

def carregar_campainha():
    return "bell_sound.mp3"  # Caminho para o som da campainha

pessoas_chamadas = []

def tocar_campainha_e_dizer_nome(nome, mesa_numero):
    bell_audio = carregar_campainha()
    pygame.mixer.music.load(bell_audio)
    pygame.mixer.music.play()

def format_name(name):
    name_parts = name.split()
    if len(name_parts) > 1:
        return f"{name_parts[0].capitalize()} {name_parts[1][0].upper()}."
    else:
        return name_parts[0].capitalize()
    
def monitorar_agenda_para_novas_chamadas():
    placeholder = st.empty()

    if 'page_loaded' not in st.session_state:
        st.session_state['page_loaded'] = False

    if not st.session_state['page_loaded']:
        with placeholder.container():
            st.write("Carregando a página...")  
        st.session_state['page_loaded'] = True
        st.rerun()  # Rerun após o carregamento

    while True:
        try:
            agenda_df = pd.read_csv(CSV_FILE)

            # Ensure 'Horário de Chegada' exists
            if 'Horário de Chegada' not in agenda_df.columns:
                agenda_df['Horário de Chegada'] = None  # or pd.NaT for datetime
            if 'Mesa' not in agenda_df.columns:
                agenda_df['Mesa'] = None  # or pd.NaT for datetime
            if 'Status' not in agenda_df.columns:
                agenda_df['Status'] = None  # or pd.NaT for datetime

            # Convert 'Horário de Chegada' to datetime format, ignore errors
            agenda_df['Horário de Chegada'] = pd.to_datetime(agenda_df['Horário de Chegada'], errors='coerce', dayfirst=True)

        except Exception as e:
            st.write(f'Sem agenda disponível. Erro: {e}')
            continue

        with placeholder.container():
            try:
                # Sort by 'Horário de Chegada' if available
                sorted_data = agenda_df.sort_values(by='Horário de Chegada').reset_index(drop=True)
            except KeyError:
                st.write("Erro: 'Horário de Chegada' não disponível.")
                continue

            st.markdown(
                """
                <style>
                .full-width {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    width: 100%;
                    margin-top: 0px; /* Reduz espaço acima do primeiro nome */
                }
                .name-column {
                    flex: 1;
                    text-align: center;
                    padding-right: 20px; /* Espaçamento à direita da coluna de nome */
                }
                .mesa-column {
                    flex: 0.5;
                    text-align: right;
                    padding-left: 20px; /* Espaçamento à esquerda da coluna da mesa */
                }
                .big-text {
                    font-size: 250px; /* Aumentar o tamanho da fonte */
                    font-weight: bold;
                    margin-bottom: 0px; /* Reduz o espaço entre o título e o nome grande */
                }
                .mesa-label {
                    font-size: 150px; /* Aumentar o tamanho do texto "Mesa" */
                    font-weight: bold;
                    margin: 0px;
                }
                .mesa-number {
                    font-size: 250px; /* Aumentar o tamanho do número da mesa */
                    font-weight: bold;
                    margin: 0px;
                }
                .small-text {
                    font-size: 40px; /* Aumentar o tamanho do texto para os últimos chamados */
                    text-align: left;
                    margin-bottom: 0px; /* Reduz o espaço entre o título e o primeiro nome menor */
                }
                .stacked-text {
                    font-size: 100px; /* Aumentar o tamanho dos nomes empilhados */
                    text-align: left;
                    padding-right: 20px; /* Espaçamento à direita dos nomes na parte inferior */
                    margin-bottom: 0px; /* Remove o espaço entre os nomes empilhados */
                    line-height: 0.8; /* Reduz o espaço vertical entre as linhas de texto */
                }
                .stacked-table-number {
                    font-size: 80px; /* Aumentar o tamanho dos números de mesa empilhados */
                    text-align: left;
                    padding-left: 20px; /* Espaçamento à esquerda dos números de mesa na parte inferior */
                    margin-bottom: 10px;
                    font-weight: bold;
                }
                </style>
                """, unsafe_allow_html=True
            )

            if pessoas_chamadas:
                nome_grande, mesa_grande = pessoas_chamadas[0]
                st.image('logo.png', width=500)
                st.markdown(f"""
                <div class='full-width'>
                    <div class='name-column'>
                        <div class='big-text'>{nome_grande}</div>
                    </div>
                    <div class='mesa-column'>
                        <div class='mesa-label'>Mesa</div>
                        <div class='mesa-number'>{mesa_grande}</div>
                    </div>
                </div>
                """, unsafe_allow_html=True)

            if len(pessoas_chamadas) > 1:
                st.markdown("<div class='small-text'>Últimos chamados:</div>", unsafe_allow_html=True)
                for i in range(1, min(4, len(pessoas_chamadas))):
                    nome = pessoas_chamadas[i][0]
                    mesa = pessoas_chamadas[i][1]
                    st.markdown(f"""
                    <div class='full-width'>
                        <div class='stacked-text'>{nome}</div>
                        <div class='stacked-table-number'>Mesa {mesa}</div>
                    </div>
                    """, unsafe_allow_html=True)
                
                # Ensure status is fetched only if 'Nome' exists
                if nome_grande in agenda_df['Nome'].values:
                    status_pessoa = agenda_df.loc[agenda_df['Nome'] == nome_grande, 'Status'].values[0]
                    if status_pessoa != "Em atendimento":
                        tocar_campainha_e_dizer_nome(nome_grande, mesa_grande)

            # Fetch new people to call
            pessoas_para_chamar = agenda_df[(agenda_df['Status'] == "Aguardando atendimento") & (agenda_df['Mesa'].notnull())]
            novas_chamadas = []
            for index, pessoa in pessoas_para_chamar.iterrows():
                nome = pessoa['Nome']
                mesa = pessoa['Mesa']
                if (nome, mesa) not in pessoas_chamadas:
                    pessoas_chamadas.insert(0, (nome, mesa))  
                    if len(pessoas_chamadas) > 4:
                        pessoas_chamadas.pop()
                    novas_chamadas.append((nome, mesa))
                    break

        time.sleep(10)

def carregar_agenda():
    try:
        if os.path.exists(CSV_FILE):
            agenda_df = pd.read_csv('agenda.csv', encoding='utf')
            if 'Prioritária' not in agenda_df.columns:
                agenda_df['Prioritária'] = False

            if 'Status' not in agenda_df.columns:
                agenda_df['Status'] = "Aguardando chegada"
            if 'Horário de Chegada' not in agenda_df.columns:
                agenda_df['Horário de Chegada'] = None
            if 'Mesa' not in agenda_df.columns:
                agenda_df['Mesa'] = None
            if 'Posição na fila' not in agenda_df.columns:
                agenda_df['Posição na fila'] = None
            if 'Atendimento iniciado em' not in agenda_df.columns:
                agenda_df['Atendimento iniciado em'] = None  # New column for when service starts
            if 'Atendimento encerrado em' not in agenda_df.columns:
                agenda_df['Atendimento encerrado em'] = None  

            agenda_df['Mesa'] = pd.to_numeric(agenda_df['Mesa'], errors='coerce').fillna(0).astype('Int64')
            agenda_df['Nome'] = agenda_df['Nome'].apply(format_name)
            agenda_df['Atendimento iniciado em'] = agenda_df['Atendimento iniciado em'].astype(str)
            agenda_df['Data'] = agenda_df['Data'].astype(str)
            return agenda_df

        else:
            agenda_df = pd.DataFrame({
                "Nome": ["José", "João", "Ana", "Pedro", "José", "Luiza"],
                "Data": ["17/09/2024", "17/09/2024", "17/09/2024", "17/09/2024", "17/09/2024", "17/09/2024"],
                "Horário": ["08:10:00", "08:30:00", "09:00:00", "09:30:00", "10:00:00", "10:30:00"],
                "Status": ["Aguardando chegada", "Aguardando chegada", "Aguardando chegada", "Aguardando chegada", "Aguardando chegada", "Aguardando chegada"],
                "Horário de Chegada": [None, None, None, None, None, None],
                "Posição na fila": [None, None, None, None, None, None],
                "Mesa": [None, None, None, None, None, None],
                "Atendimento iniciado em": [None, None, None, None, None, None],
                "Prioritária": [False, False, False, False, False, False]
            })
            agenda_df.to_csv(CSV_FILE, index=False)
        return agenda_df
            
    except Exception as e:
        st.error(f"Erro ao carregar a agenda: {e}")
        return pd.DataFrame()  # Return an empty DataFrame on error

def salvar_agenda(df):
    try:
        df_chegada_confirmada = df[df['Status'] == "Chegada confirmada"].sort_values(
            by=['Prioritária', 'Horário de Chegada'], 
            ascending=[False, True]
        ).reset_index(drop=True)
        df.loc[df['Status'] == "Chegada confirmada", 'Posição na fila'] = df_chegada_confirmada.index + 1
        df.to_csv(CSV_FILE, index=False)
        st.success("Agenda salva com sucesso!")
    except Exception as e:
        st.error(f"Erro ao salvar a agenda: {e}")

def iniciar_atendimento(nome):
    agenda_df = carregar_agenda()
    if nome in agenda_df['Nome'].values:
        current_time = datetime.now().strftime("%d/%m/%Y %H:%M:%S")  # Ensure the format 'dd/mm/yyyy hh:mm'
        agenda_df.loc[agenda_df['Nome'] == nome, 'Atendimento iniciado em'] = current_time
        agenda_df.loc[agenda_df['Nome'] == nome, 'Status'] = "Em atendimento"
        salvar_agenda(agenda_df)
        st.success(f"Atendimento iniciado para {nome.capitalize()} às {current_time}")
        st.rerun()
    else:
        st.error(f"Nome {nome.capitalize()} não encontrado na agenda.")
        
def chamar_proxima_pessoa(mesa_numero):
    agenda_df = carregar_agenda()
    pessoas_para_chamar = agenda_df[agenda_df['Status'] == "Chegada confirmada"]
    pessoas_para_chamar = pessoas_para_chamar.sort_values(by=["Horário Agendado", "Horário de Chegada"])
    if not pessoas_para_chamar.empty:
        pessoa_para_chamar = pessoas_para_chamar.iloc[0]
        agenda_df.loc[agenda_df['Nome'] == pessoa_para_chamar['Nome'], 'Status'] = "Aguardando atendimento"
        agenda_df.loc[agenda_df['Nome'] == pessoa_para_chamar['Nome'], 'Mesa'] = mesa_numero
        salvar_agenda(agenda_df)
        return pessoa_para_chamar['Nome'], mesa_numero
    return None, None

st.set_page_config(page_title="Painel de Atendimento", layout='wide')
with st.sidebar:
    funcao_selecionada = st.selectbox("Selecione o tipo de acesso", ["Recepcionista", "Atendente", "Painel de Chamada"])

agenda_df = carregar_agenda()

if funcao_selecionada in ["Recepcionista", "Atendente"]:
    st.markdown("## Agenda do Dia")
    if 'Horário de Chegada' not in agenda_df.columns:
        agenda_df['Horário de Chegada'] = pd.Series(dtype='object')
    else:
        agenda_df['Horário de Chegada'] = agenda_df['Horário de Chegada'].astype('str')
    data_selecionada = st.selectbox("Selecione a Data", agenda_df['Data'].unique())
    agenda_filtrada = agenda_df[agenda_df['Data'] == data_selecionada]
    st.dataframe(agenda_filtrada, use_container_width=True)

if funcao_selecionada == "Recepcionista":
    st.markdown("### Recepcionista")
    col1, col2 = st.columns([3, 1])
    with col1:
        waiting_people = agenda_df[agenda_df['Status'] == "Aguardando chegada"].sort_values(by='Horário')
        if not waiting_people.empty:
            nome_selecionado = st.selectbox(
                "Selecione a pessoa para confirmar chegada",
                waiting_people.apply(lambda row: f"{row['Nome']} - {row['Horário']}", axis=1)
            )
        else:
            nome_selecionado = None
    with col2:
        is_prioritaria = st.checkbox("Prioritária", value=False)
        if st.button("Confirmar chegada") and nome_selecionado:
            selected_name = nome_selecionado.split(" - ")[0]  # Obter nome da pessoa
            agenda_df.loc[agenda_df['Nome'] == selected_name, 'Status'] = "Chegada confirmada"
            agenda_df.loc[agenda_df['Nome'] == selected_name, 'Horário de Chegada'] = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
            agenda_df.loc[agenda_df['Nome'] == selected_name, 'Prioritária'] = is_prioritaria
            salvar_agenda(agenda_df)
            st.success(f"Chegada de {selected_name} confirmada.")
            st.rerun()

    confirmed_people = agenda_df[agenda_df['Status'] == "Chegada confirmada"]
    with st.expander("Desconfirmar chegada"):
        if not confirmed_people.empty:
            nome_desconfirmado = st.selectbox(
                "Selecione a pessoa para desconfirmar chegada",
                confirmed_people.apply(lambda row: f"{row['Nome']} - {row['Horário']}", axis=1)
            )
            if st.button("Desconfirmar chegada") and nome_desconfirmado:
                selected_name = nome_desconfirmado.split(" - ")[0]
                agenda_df.loc[agenda_df['Nome'] == selected_name, 'Status'] = "Aguardando chegada"
                agenda_df.loc[agenda_df['Nome'] == selected_name, 'Horário de Chegada'] = None
                agenda_df.loc[agenda_df['Nome'] == selected_name, 'Posição na fila'] = None
                agenda_df.loc[agenda_df['Nome'] == selected_name, 'Atendimento iniciado em'] = None
                agenda_df.loc[agenda_df['Nome'] == selected_name, 'Prioritária'] = False
                salvar_agenda(agenda_df)
    # Adicionando "Encaixes"
    with st.expander("Adicionar Encaixe"):
        nome_encaixe = st.text_input("Nome")
        if st.button("Adicionar Encaixe") and nome_encaixe:
            new_row = pd.DataFrame({
                'Nome': [nome_encaixe],
                'Data': [datetime.now().strftime("%d/%m/%Y")],
                'Horário': [datetime.now().strftime("%H:%M:%S")],
                'Status': ['Chegada confirmada'],
                'Horário de Chegada': [datetime.now().strftime("%d/%m/%Y %H:%M:%S")],
                'Mesa': [None]
            })
            agenda_df = pd.concat([agenda_df, new_row], ignore_index=True)  # Use pd.concat instead of append
            salvar_agenda(agenda_df)
            st.success(f"Encaixe de {nome_encaixe} adicionado.")
            
elif funcao_selecionada == "Atendente":
    
    st.markdown("### Atendente")
    with st.sidebar:
        atendente_selecionado = st.selectbox("Selecione o atendente", ["Atendente 1", "Atendente 2", "Atendente 3", "Atendente 4", "Atendente 5", "Atendente 6"])
    mesa_numero = int(atendente_selecionado.split()[-1])
    atendimentos_iniciados = agenda_df[(agenda_df['Status'] == "Atendimento encerrado") & (agenda_df['Mesa'] == mesa_numero)]
    total_atendimentos = len(atendimentos_iniciados)
    st.markdown(f"**Total de atendimentos hoje: {total_atendimentos}**")

    atendente_key = f"atendimento_iniciado_{atendente_selecionado}"
    pessoas_chamadas_key = f"pessoas_chamadas_{atendente_selecionado}"
    
    if atendente_key not in st.session_state:
        st.session_state[atendente_key] = None
    
    
    if pessoas_chamadas_key not in st.session_state:
        st.session_state[pessoas_chamadas_key] = []

    if (
        st.session_state[atendente_key] is None or 
        agenda_df.loc[agenda_df['Nome'] == st.session_state[atendente_key], 'Status'].values[0] in ["Atendimento encerrado", "Chegada confirmada"]
    ):
        if st.button("Chamar próxima pessoa"):
            pessoas_prioritarias = agenda_df[
                (agenda_df['Status'] == "Chegada confirmada") & 
                (agenda_df['Prioritária'] == True)
            ].sort_values(by='Horário de Chegada')

            pessoas_nao_prioritarias = agenda_df[
                (agenda_df['Status'] == "Chegada confirmada") & 
                (agenda_df['Prioritária'] == False)
            ].sort_values(by='Horário de Chegada')

            if not pessoas_prioritarias.empty:
                proxima_pessoa = pessoas_prioritarias.iloc[0]
            elif not pessoas_nao_prioritarias.empty:
                proxima_pessoa = pessoas_nao_prioritarias.iloc[0]
            else:
                proxima_pessoa = None

            if proxima_pessoa is not None:
                nome_chamado = proxima_pessoa['Nome']
                agenda_df.loc[agenda_df['Nome'] == nome_chamado, 'Status'] = "Aguardando atendimento"
                agenda_df.loc[agenda_df['Nome'] == nome_chamado, 'Mesa'] = mesa_numero
                salvar_agenda(agenda_df)
                st.session_state[atendente_key] = nome_chamado
                st.session_state[pessoas_chamadas_key].insert(0, (nome_chamado, mesa_numero))
                if len(st.session_state[pessoas_chamadas_key]) > 4:
                    st.session_state[pessoas_chamadas_key].pop()  # Keep only the last 4 entries
                st.success(f"{nome_chamado} foi chamado para a mesa {mesa_numero}.")
                st.rerun()
            else:
                st.warning("Nenhuma pessoa disponível para chamar.")
    else:
        st.warning("Finalize o atendimento atual antes de chamar uma nova pessoa.")

    if len(st.session_state[pessoas_chamadas_key]) > 0:
        ultimas_pessoas_chamadas = st.session_state[pessoas_chamadas_key][:4]
        st.markdown("### Últimas 4 pessoas chamadas:")
        for i, pessoa in enumerate(ultimas_pessoas_chamadas):
            st.markdown(f"{i+1}. {pessoa[0]} - Mesa {pessoa[1]}")

        ultima_pessoa = st.session_state[pessoas_chamadas_key][0][0]
        mesa_ultima_pessoa = st.session_state[pessoas_chamadas_key][0][1]

        if mesa_numero == mesa_ultima_pessoa:
            st.markdown(f"### Última pessoa chamada: **{ultima_pessoa}**")
            if st.button(f"Cancelar chamada de {ultima_pessoa}"):
                agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Status'] = "Chegada confirmada"
                agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Mesa'] = None
                agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Atendimento iniciado em'] = None
                agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Atendimento encerrado em'] = None
                salvar_agenda(agenda_df)
                st.session_state[pessoas_chamadas_key].pop(0)  # Remove from the list
                st.session_state[atendente_key] = None
                st.success(f"Chamada de {ultima_pessoa} foi cancelada.")
                st.rerun()

            if agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Status'].values[0] == "Aguardando atendimento":
                if st.button(f"Iniciar atendimento de {ultima_pessoa}"):
                    agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Status'] = "Em atendimento"
                    agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Atendimento iniciado em'] = pd.Timestamp.now()
                    salvar_agenda(agenda_df)
                    st.session_state[atendente_key] = ultima_pessoa
                    st.success(f"Atendimento de {ultima_pessoa} foi iniciado.")
                    st.rerun()

            if agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Status'].values[0] == "Em atendimento":
                if st.button(f"Encerrar atendimento de {ultima_pessoa}"):
                    agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Status'] = "Atendimento encerrado"
                    agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Posição na fila'] = 0
                    agenda_df.loc[agenda_df['Nome'] == ultima_pessoa, 'Atendimento encerrado em'] = pd.Timestamp.now()
                    salvar_agenda(agenda_df)
                    st.session_state[atendente_key] = None
                    st.success(f"Atendimento de {ultima_pessoa} foi encerrado.")
                    st.rerun()

if funcao_selecionada == "Painel de Chamada":
    monitorar_agenda_para_novas_chamadas()