diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..78ad520
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+FROM python:3.11-slim
+
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV PYTHONUNBUFFERED 1
+
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends build-essential libpq-dev gcc \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY requirements-docker.txt /app/requirements-docker.txt
+
+RUN python -m pip install --upgrade pip setuptools wheel \
+ && pip install -r /app/requirements-docker.txt
+
+COPY . /app
+
+RUN chmod +x /app/entrypoint.sh || true
+
+EXPOSE 8000
+
+ENTRYPOINT ["/app/entrypoint.sh"]
diff --git a/READMEimplementation.md b/READMEimplementation.md
new file mode 100644
index 0000000..847d008
--- /dev/null
+++ b/READMEimplementation.md
@@ -0,0 +1,293 @@
+# Implementação em Produção - SGA LSL Univesp
+
+## Pré-requisitos
+- Servidor Linux com Docker e Docker Compose instalados
+- PostgreSQL (via Docker)
+- Porta 80 disponível para Nginx
+
+## Passo a Passo
+
+### 1. Clonagem e Configuração Inicial
+
+- Adicione os arquivos do sistema no diretório do servidor (Por ex. /home/user/python-sga-lsl-univesp)
+- Entre no diretório (cd /caminho/do/sistema)
+
+```bash
+# No diretório do projeto (onde está entrypoint.sh)
+chmod +x entrypoint.sh
+```
+
+### 2. Configuração do Ambiente (.env)
+Edite o arquivo `.env` com valores seguros para produção:
+
+```env
+POSTGRES_USER=sga_prod_user
+POSTGRES_PASSWORD=SuaSenhaSuperSeguraAqui123!
+POSTGRES_DB=sga_prod_db
+SECRET_KEY=SuaSecretKeySuperSeguraDePeloMenos50CaracteresAqui
+DEBUG=0
+DATABASE_URL=postgres://sga_prod_user:SuaSenhaSuperSeguraAqui123!@db:5432/sga_prod_db
+
+# Superuser para produção
+DJANGO_SUPERUSER_USERNAME=admin_cpf # CPF válido sem máscara
+DJANGO_SUPERUSER_EMAIL=admin@seudominio.com
+DJANGO_SUPERUSER_PASSWORD=SenhaSuperSeguraParaAdmin
+```
+
+**Recomendações de Segurança:**
+- Use senhas fortes e únicas (mínimo 16 caracteres, com letras, números e símbolos)
+- Mantenha o `DEBUG=0` em produção
+- Use um `SECRET_KEY` gerado aleatoriamente (pode usar `openssl rand -hex 32`)
+- Restrinja acesso ao arquivo `.env` (chmod 600)
+- Considere usar variáveis de ambiente do sistema em vez de arquivo `.env` para maior segurança
+
+### 3. Build e Inicialização
+```bash
+# Build das imagens
+docker-compose -f docker-compose.prod.yml build
+
+# Inicialização (cria banco, superuser, guichês)
+docker-compose -f docker-compose.prod.yml up -d
+```
+
+### 4. Verificação
+Acesse `http://seu-servidor` e faça login com o superuser configurado.
+
+Sugestão de fluxo inicial após o primeiro login:
+
+- **Login do administrador:** faça login usando as credenciais do superuser definidas no arquivo `.env` (campo `DJANGO_SUPERUSER_USERNAME` e `DJANGO_SUPERUSER_PASSWORD`).
+- **Ajuste do perfil:** após o login, acesse a interface de administrador e edite suas próprias informações de contato/perfil para que constem corretamente no sistema.
+- **Cadastrar funcionários:** comece a adicionar os funcionários no sistema pela interface de administração. Recomendamos chamar cada funcionário até o computador do administrador para que o próprio funcionário preencha ou confirme seus dados na tela — a interface é responsiva e também pode ser usada a partir de um smartphone caso prefira levar o dispositivo até o funcionário.
+- **Informar acesso aos funcionários:** depois de criar a conta, informe ao funcionário a URL que ele deve digitar no navegador para acessar o sistema (ex.: `http://seu-servidor/` ou `http://seu-servidor/login`).
+
+- **IMPORTANTE:** Esses passos garantem que o cadastro seja feito com supervisão e que cada funcionário saiba imediatamente como acessar a aplicação.
+
+- **Preparar as tvs:** Cada tv tem uma url especifica (`http://seu-servidor/guiche/tv1` e `http://seu-servidor/profissional_saude/tv2`).
+Instale os cabos hdmi em quaisquer computadores proximos (maximo 10m de distancia) ou alguma outra solução para que as tvs tenham acesso remoto a algum computador para exibirem a interface web (É possivel testar o navegador da própria TV caso seja Smart).
+Esses endpoints nao precisam de login.
+
+### 5. Configuração para Iniciar Automaticamente no Boot
+
+#### Opção 1: Usando systemd (Recomendado)
+Crie um serviço systemd:
+
+```bash
+sudo nano /etc/systemd/system/sga.service
+```
+
+Conteúdo do arquivo:
+```ini
+[Unit]
+Description=SGA LSL Univesp
+Requires=docker.service
+After=docker.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+WorkingDirectory=/caminho/para/python-sga-LSL-Univesp
+ExecStart=/usr/bin/docker-compose -f docker-compose.prod.yml up -d
+ExecStop=/usr/bin/docker-compose -f docker-compose.prod.yml down
+TimeoutStartSec=0
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Habilite e inicie:
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable sga.service
+sudo systemctl start sga.service
+```
+
+#### Opção 2: Usando cron (@reboot)
+Adicione ao crontab do root:
+```bash
+sudo crontab -e
+```
+
+Adicione a linha:
+```cron
+@reboot cd /caminho/para/python-sga-LSL-Univesp && /usr/bin/docker-compose -f docker-compose.prod.yml up -d
+```
+
+### 6. Manutenção
+- **Logs:** `docker-compose -f docker-compose.prod.yml logs -f`
+- **Backup do banco:** Use volumes Docker ou scripts externos
+- **Atualizações:** Pare o serviço, atualize o código, rebuild e reinicie
+
+### 7. Segurança Adicional
+
+#### Firewall e Restrição de Acesso
+Para garantir que apenas dispositivos autorizados (ex.: rede do hospital) acessem a aplicação, configure restrições por IP no Nginx e no firewall do servidor.
+
+##### Opção 1: Restrição no Nginx (Recomendado para Controle Fino)
+Edite o arquivo `nginx.conf` e adicione as linhas `allow` e `deny` no bloco `server`:
+
+```nginx
+server {
+ listen 80;
+ server_name localhost;
+
+ # Restringir acesso por IP (ajuste a sub-rede do hospital)
+ allow 192.168.1.0/24; # Permite sub-rede específica (ex.: rede interna do hospital)
+ deny all; # Bloqueia todos os outros IPs
+
+ location / {
+ proxy_pass http://django;
+ # ... resto da configuração
+ }
+ # ... outras locations
+}
+```
+
+- Substitua `192.168.1.0/24` pela sub-rede real da rede do hospital (ex.: `10.0.0.0/8` para redes privadas).
+- Após editar, reinicie o Nginx: `docker-compose -f docker-compose.prod.yml restart nginx`.
+
+##### Opção 2: Firewall no Servidor (ufw - Ubuntu/Debian)
+Use `ufw` para restringir acesso na porta do Nginx (padrão 80, ou mude para outra como 8080 no `docker-compose.prod.yml`).
+
+Instale e configure o ufw (se não estiver instalado):
+```bash
+sudo apt update
+sudo apt install ufw
+sudo ufw enable
+```
+
+Permita apenas a sub-rede específica:
+```bash
+sudo ufw allow from 192.168.1.0/24 to any port 80 # Permite sub-rede na porta 80
+sudo ufw deny 80 # Bloqueia todos os outros na porta 80
+```
+
+- Ajuste a sub-rede e porta conforme necessário.
+- Verifique o status: `sudo ufw status`.
+- Isso adiciona uma camada extra de segurança além do Nginx.
+
+#### Outras Medidas de Segurança
+- **HTTPS**: Configure SSL no Nginx para criptografar o tráfego (use Let's Encrypt ou certificado próprio).
+- **Monitoramento**: Monitore logs do Nginx e Docker regularmente: `docker-compose -f docker-compose.prod.yml logs -f nginx`.
+- **Atualizações**: Mantenha Docker, Nginx, Django e dependências atualizadas para patches de segurança.
+- **Backup**: Faça backups regulares do banco de dados (use volumes Docker ou ferramentas como `pg_dump`).
+
+### 8. HTTPS / Certificados (Passo-a-passo)
+
+Para produção recomendamos usar HTTPS para criptografar o tráfego mesmo em redes internas. Abaixo estão 3 opções com passos concretos.
+
+Opção A — Self-signed (rápido, gera aviso no navegador)
+
+1. Gere certificados no servidor (ex.: dentro do diretório `nginx/ssl` no repo):
+
+```bash
+mkdir -p nginx/ssl
+openssl req -x509 -nodes -days 365 \
+ -newkey rsa:2048 \
+ -keyout nginx/ssl/nginx.key \
+ -out nginx/ssl/nginx.crt \
+ -subj "/CN=sga.hospital.local"
+```
+
+2. Monte os arquivos no serviço `nginx` do `docker-compose.prod.yml` (exemplo):
+
+```yaml
+services:
+ nginx:
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ - ./nginx/ssl:/etc/nginx/ssl:ro
+ - staticfiles:/app/staticfiles
+ ports:
+ - "443:443"
+ - "80:80"
+```
+
+3. Adicione um bloco `server` em `nginx.conf` para TLS (exemplo):
+
+```nginx
+server {
+ listen 443 ssl;
+ server_name sga.hospital.local;
+
+ ssl_certificate /etc/nginx/ssl/nginx.crt;
+ ssl_certificate_key /etc/nginx/ssl/nginx.key;
+
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+
+ location / {
+ proxy_pass http://django;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /static/ { alias /app/staticfiles/; }
+ location /media/ { alias /app/media/; }
+}
+
+server {
+ listen 80;
+ server_name sga.hospital.local;
+ return 301 https://$host$request_uri;
+}
+```
+
+4. Reinicie o nginx container:
+
+```bash
+docker-compose -f docker-compose.prod.yml up -d --no-deps --build nginx
+# ou
+docker-compose -f docker-compose.prod.yml restart nginx
+```
+
+Nota: navegadores mostrarão aviso de certificado não confiável. Use apenas para testes ou quando for aceitável confiar manualmente.
+
+Opção B — mkcert (boa para LAN gerenciada)
+
+- `mkcert` cria um CA local e pode gerar certificados confiáveis se você instalar a CA em todas as máquinas clientes (TI do hospital pode distribuir a CA via políticas):
+
+1. Instale `mkcert` (veja docs: https://github.com/FiloSottile/mkcert).
+2. No host de administração gere os certificados:
+
+```bash
+mkcert -install
+mkcert sga.hospital.local 192.168.1.10
+# Isso gera algo como sga.hospital.local+IP.pem e key file
+```
+
+3. Monte as chaves em `nginx` como no passo A e reinicie. Instale a CA raiz `mkcert` nos navegadores/hosts clientes (TI).
+
+Opção C — Let's Encrypt (melhor se tiver domínio público)
+
+1. Se seu servidor tem um domínio público válido (`sga.hospital.example`) e aponta para o IP, use `certbot`:
+
+```bash
+sudo apt update
+sudo apt install certbot
+sudo certbot certonly --standalone -d sga.hospital.example
+```
+
+2. Monte `/etc/letsencrypt/live/sga.hospital.example/fullchain.pem` e `privkey.pem` no container `nginx` (somente se terminar TLS no container). Use `certbot renew` em cron/systemd timer.
+
+Firewall e HSTS
+- Após habilitar HTTPS, ajuste o `ufw` para permitir apenas a sub-rede hospitalar nas portas 443/80:
+
+```bash
+sudo ufw allow from 192.168.1.0/24 to any port 443
+sudo ufw deny 443
+```
+
+- Cuidado com HSTS: só ative `add_header Strict-Transport-Security` quando tiver certificados confiáveis e controle sobre os clientes (HSTS impede acesso HTTP por período configurado).
+
+### 9. Desinstalação completa
+
+- Caso tenha interesse em desinstalar completamente o sistema rode o comando
+
+```bash
+docker-compose -f docker-compose.prod.yml down -v
+```
+(Use com cuidado, remove também todos os dados do Banco de dados)
+
+---
diff --git a/administrador/templates/administrador/alterar_senha_funcionario.html b/administrador/templates/administrador/alterar_senha_funcionario.html
new file mode 100644
index 0000000..8956851
--- /dev/null
+++ b/administrador/templates/administrador/alterar_senha_funcionario.html
@@ -0,0 +1,63 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block content %}
+
+
+
Alterar Senha do Funcionário
+
Altere a senha do funcionário selecionado: {{ funcionario.first_name }} {{ funcionario.last_name }} (CPF: {{ funcionario.cpf }})
-
- Editar
+ Editar Dados
+
+
+
+ Alterar Senha
diff --git a/administrador/urls.py b/administrador/urls.py
index b83db60..93018ff 100644
--- a/administrador/urls.py
+++ b/administrador/urls.py
@@ -17,6 +17,16 @@
views.editar_funcionario,
name="editar_funcionario",
),
+ path(
+ "alterar_senha_funcionario//",
+ views.alterar_senha_funcionario,
+ name="alterar_senha_funcionario",
+ ),
+ path(
+ "editar_dados_funcionario//",
+ views.editar_dados_funcionario,
+ name="editar_dados_funcionario",
+ ),
path(
"excluir_funcionario//",
views.excluir_funcionario,
diff --git a/administrador/views.py b/administrador/views.py
index fffb39e..2e6191e 100644
--- a/administrador/views.py
+++ b/administrador/views.py
@@ -10,8 +10,9 @@
from django.contrib.auth.decorators import login_required
from core.decorators import admin_required
-from core.forms import CadastrarFuncionarioForm
+from core.forms import CadastrarFuncionarioForm, EditarFuncionarioForm
from core.models import CustomUser, RegistroDeAcesso # Importe o modelo CustomUser
+from django.contrib.auth.forms import SetPasswordForm
@admin_required
@@ -170,3 +171,45 @@ def excluir_funcionario(request, pk):
funcionario.delete()
messages.success(request, "Funcionário excluído com sucesso!")
return redirect(reverse("administrador:listar_funcionarios"))
+
+
+@admin_required
+def alterar_senha_funcionario(request, pk):
+ funcionario = get_object_or_404(CustomUser, pk=pk)
+ if request.method == "POST":
+ form = SetPasswordForm(funcionario, request.POST)
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Senha alterada com sucesso para o funcionário.")
+ return redirect(reverse("administrador:listar_funcionarios"))
+ else:
+ messages.error(request, "Erro ao alterar a senha. Verifique os dados.")
+ else:
+ form = SetPasswordForm(funcionario)
+
+ return render(
+ request,
+ "administrador/alterar_senha_funcionario.html",
+ {"form": form, "funcionario": funcionario},
+ )
+
+
+@admin_required
+def editar_dados_funcionario(request, pk):
+ funcionario = get_object_or_404(CustomUser, pk=pk)
+ if request.method == "POST":
+ form = EditarFuncionarioForm(request.POST, instance=funcionario)
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Dados do funcionário atualizados com sucesso!")
+ return redirect(reverse("administrador:listar_funcionarios"))
+ else:
+ messages.error(request, "Erro ao atualizar os dados. Verifique os campos.")
+ else:
+ form = EditarFuncionarioForm(instance=funcionario)
+
+ return render(
+ request,
+ "administrador/editar_dados_funcionario.html",
+ {"form": form, "funcionario": funcionario},
+ )
diff --git a/core/forms.py b/core/forms.py
index 3be9228..aa319c0 100644
--- a/core/forms.py
+++ b/core/forms.py
@@ -3,6 +3,7 @@
from django.contrib.auth import authenticate
from django.contrib.auth.forms import UserCreationForm
from django.utils import timezone
+from typing import Dict, Optional
import re
from django.core.exceptions import ValidationError
@@ -82,7 +83,7 @@ class Meta:
"telefone_celular",
]
- help_texts = {
+ help_texts: Dict[str, Optional[str]] = {
"telefone_celular": None,
}
@@ -147,12 +148,25 @@ def validate_cpf(value):
help_text="Selecione a função do funcionário.",
)
+ def clean_cpf(self):
+ cpf = self.cleaned_data.get("cpf")
+ if CustomUser.objects.filter(username=cpf).exists():
+ raise forms.ValidationError("Este CPF já está cadastrado.")
+ return cpf
+
class Meta(UserCreationForm.Meta):
model = CustomUser
- fields = ("cpf", "username", "first_name", "last_name", "email", "funcao")
- help_texts = {
- "username": None,
- }
+ # include password fields so admin can set initial password
+ fields = (
+ "cpf",
+ "first_name",
+ "last_name",
+ "email",
+ "funcao",
+ "password1",
+ "password2",
+ )
+ help_texts: Dict[str, Optional[str]] = {}
def clean_first_name(self):
first_name = self.cleaned_data.get("first_name")
@@ -161,9 +175,13 @@ def clean_first_name(self):
return first_name
def save(self, commit=True):
+ # Use UserCreationForm handling for password, but ensure username becomes CPF
user = super().save(commit=False)
- user = super(UserCreationForm, self).save(commit=False)
- user.username = self.cleaned_data["cpf"] # Define o username como o CPF
+ user.username = self.cleaned_data.get("cpf")
+ # If password1 provided, set it explicitly (UserCreationForm normally handles this in its save)
+ pwd = self.cleaned_data.get("password1")
+ if pwd:
+ user.set_password(pwd)
if commit:
user.save()
return user
@@ -173,11 +191,27 @@ class LoginForm(forms.Form):
cpf = forms.CharField(
label="CPF",
max_length=14,
- widget=forms.TextInput(attrs={"placeholder": "Digite seu CPF"}),
+ widget=forms.TextInput(
+ attrs={
+ "placeholder": "Digite seu CPF",
+ "autocomplete": "username",
+ "autocorrect": "off",
+ "autocapitalize": "off",
+ "spellcheck": "false",
+ }
+ ),
)
password = forms.CharField(
label="Senha",
- widget=forms.PasswordInput(attrs={"placeholder": "Digite sua senha"}),
+ widget=forms.PasswordInput(
+ attrs={
+ "placeholder": "Digite sua senha",
+ "autocomplete": "current-password",
+ "autocorrect": "off",
+ "autocapitalize": "off",
+ "spellcheck": "false",
+ }
+ ),
)
def clean_cpf(self):
@@ -195,7 +229,9 @@ def clean(self):
if cpf and password:
try:
user = CustomUser.objects.get(cpf=cpf)
+ print(f"DEBUG clean: user found {user.username}")
except CustomUser.DoesNotExist:
+ print(f"DEBUG clean: user not found for cpf {cpf}")
user = None
if user:
@@ -233,3 +269,23 @@ def clean(self):
raise ValidationError("CPF ou senha incorretos.")
return cleaned_data
+
+
+class EditarFuncionarioForm(forms.ModelForm):
+ class Meta:
+ model = CustomUser
+ # Excluir CPF e campos de senha — apenas editar dados administrativos
+ fields = (
+ "first_name",
+ "last_name",
+ "email",
+ "funcao",
+ "data_admissao",
+ "sala",
+ "is_active",
+ )
+ widgets = {
+ "data_admissao": forms.DateInput(attrs={"type": "date"}),
+ "sala": forms.NumberInput(),
+ "email": forms.EmailInput(),
+ }
diff --git a/core/views.py b/core/views.py
index 9f102cb..472d77d 100644
--- a/core/views.py
+++ b/core/views.py
@@ -18,6 +18,9 @@ def login_view(request):
logger.info("A view de login foi chamada.")
if request.method == "POST":
form = LoginForm(request.POST)
+ print("DEBUG: form.is_valid() =", form.is_valid())
+ print("DEBUG: form.errors =", form.errors)
+ print("DEBUG: form.data =", form.data)
logger.info("Formulário POST recebido.")
if form.is_valid():
cpf = form.cleaned_data["cpf"]
@@ -50,10 +53,11 @@ def login_view(request):
else:
return redirect("pagina_inicial")
else:
- logger.warning("Credenciais incorretas.")
+ print(f"Autenticação falhou para CPF: {cpf}")
form.add_error(None, "CPF ou senha incorretos.")
else:
- logger.warning("Formulário inválido.")
+ print(f"Form inválido: {form.errors}")
+ form.add_error(None, "Dados inválidos. Verifique o CPF.")
else:
form = LoginForm()
return render(request, "login.html", {"form": form})
@@ -66,6 +70,18 @@ def pagina_inicial(request):
@login_required
def logout_view(request):
+ # Liberar guichê se o usuário for do tipo guiche
+ if hasattr(request.user, "funcao") and request.user.funcao == "guiche":
+ guiche_id = request.session.get("guiche_id")
+ if guiche_id:
+ try:
+ from core.models import Guiche
+
+ guiche = Guiche.objects.get(id=guiche_id)
+ guiche.funcionario = None
+ guiche.save()
+ except Guiche.DoesNotExist:
+ pass
RegistroDeAcesso.objects.create(
usuario=request.user,
tipo_de_acesso="logout",
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..47b6312
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,50 @@
+services:
+ db:
+ image: postgres:15
+ env_file:
+ - .env
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+ POSTGRES_DB: ${POSTGRES_DB}
+ volumes:
+ - postgres_data_prod:/var/lib/postgresql/data
+ networks:
+ - sga_network
+
+ web:
+ build: .
+ volumes:
+ - .:/app
+ - staticfiles:/app/staticfiles
+ env_file:
+ - .env
+ environment:
+ DJANGO_SETTINGS_MODULE: sga.settings
+ DEBUG: '0'
+ SECRET_KEY: ${SECRET_KEY}
+ DJANGO_ENV: production
+ depends_on:
+ - db
+ networks:
+ - sga_network
+
+ nginx:
+ image: nginx:alpine
+ ports:
+ - "80:80"
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf
+ - staticfiles:/app/staticfiles
+ depends_on:
+ - web
+ networks:
+ - sga_network
+
+volumes:
+ postgres_data_prod:
+ staticfiles:
+
+networks:
+ sga_network:
+ driver: bridge
\ No newline at end of file
diff --git a/entrypoint.sh b/entrypoint.sh
new file mode 100755
index 0000000..996198c
--- /dev/null
+++ b/entrypoint.sh
@@ -0,0 +1,120 @@
+#!/bin/sh
+set -e
+
+echo "Entrypoint: aguardando banco de dados..."
+
+# Simple wait-for-postgres loop using psql (if available) or python fallback
+if command -v pg_isready >/dev/null 2>&1; then
+ until pg_isready -h "${DB_HOST:-db}" -p "${DB_PORT:-5432}" -U "${POSTGRES_USER:-postgres}"; do
+ echo "Aguardando Postgres..."
+ sleep 1
+ done
+else
+ python - <Histórico de Chamadas
function falarSoletracao(nomeCompleto, numeroGuiche) {
console.log("Função falarSoletracao chamada!");
+ if (!('speechSynthesis' in window)) {
+ console.log("Speech Synthesis não suportado neste navegador.");
+ return;
+ }
+
if (!synth) {
console.error("speechSynthesis não está disponível!");
return;
@@ -282,7 +287,11 @@
Histórico de Chamadas
}
utterThis.onerror = function(event) {
- console.error('Erro ao falar:', event.error);
+ if (event.error === 'not-allowed') {
+ console.log('Fala bloqueada pelo navegador. Possivelmente requer interação do usuário ou HTTPS.');
+ } else {
+ console.error('Erro ao falar:', event.error);
+ }
}
synth.speak(utterThis);
diff --git a/guiche/views.py b/guiche/views.py
index e6d06dc..3489c2e 100644
--- a/guiche/views.py
+++ b/guiche/views.py
@@ -345,19 +345,42 @@ def tv1_historico_api_view(request) -> JsonResponse:
class SelecionarGuicheForm(forms.Form):
guiche = forms.ModelChoiceField(
- queryset=Guiche.objects.all().order_by("numero"),
+ queryset=Guiche.objects.none(),
empty_label="Selecione um guichê",
label="Guichê do dia",
)
+ def __init__(self, *args, user=None, **kwargs):
+ super().__init__(*args, **kwargs)
+ # Mostrar guichês não atribuídos OU já atribuídos ao usuário atual
+ if user is not None:
+ self.fields["guiche"].queryset = Guiche.objects.filter(
+ funcionario__isnull=True
+ ).order_by("numero") | Guiche.objects.filter(funcionario=user)
+ else:
+ self.fields["guiche"].queryset = Guiche.objects.filter(
+ funcionario__isnull=True
+ ).order_by("numero")
+
@login_required
@guiche_required
def selecionar_guiche(request):
if request.method == "POST":
- form = SelecionarGuicheForm(request.POST)
+ form = SelecionarGuicheForm(request.POST, user=request.user)
if form.is_valid():
g = form.cleaned_data["guiche"]
+ # Liberar guichê anterior se existir
+ old_guiche_id = request.session.get("guiche_id")
+ if old_guiche_id:
+ try:
+ old_guiche = Guiche.objects.get(id=old_guiche_id)
+ old_guiche.funcionario = None
+ old_guiche.save()
+ except Guiche.DoesNotExist:
+ pass
+ g.funcionario = request.user
+ g.save()
request.session["guiche_id"] = g.id
request.session.modified = True
return redirect("guiche:painel_guiche")
@@ -366,6 +389,6 @@ def selecionar_guiche(request):
gid = request.session.get("guiche_id")
if gid:
initial["guiche"] = gid
- form = SelecionarGuicheForm(initial=initial)
+ form = SelecionarGuicheForm(initial=initial, user=request.user)
return render(request, "guiche/selecionar_guiche.html", {"form": form})
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..bffc751
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,51 @@
+events {
+ worker_connections 1024;
+}
+
+# Segurança: Restringir acesso por IP
+# Para permitir apenas dispositivos específicos (ex.: rede do hospital), adicione as linhas abaixo no bloco 'server'.
+# Exemplo:
+# allow 192.168.1.0/24; # Permite sub-rede do hospital (ajuste o IP/sub-rede real)
+# deny all; # Bloqueia todos os outros acessos
+#
+# Nota: Se o servidor usar ufw (Ubuntu/Debian), configure o firewall no host:
+# sudo ufw allow from 192.168.1.0/24 to any port 8080 # Permite sub-rede na porta do Nginx (ajuste porta se mudar)
+# sudo ufw deny 8080 # Bloqueia outros na mesma porta
+# Isso adiciona uma camada extra de segurança além do Nginx.
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ sendfile on;
+ keepalive_timeout 65;
+
+ upstream django {
+ server web:8000;
+ }
+
+ server {
+ listen 80;
+ server_name localhost;
+
+ # Exemplo de restrição por IP (descomente e ajuste):
+ # allow 192.168.1.0/24; # Permite sub-rede do hospital
+ # deny all; # Bloqueia todos os outros
+
+ location / {
+ proxy_pass http://django;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /static/ {
+ alias /app/staticfiles/;
+ }
+
+ location /media/ {
+ alias /app/media/;
+ }
+ }
+}
\ No newline at end of file
diff --git a/profissional_saude/templates/profissional_saude/tv2.html b/profissional_saude/templates/profissional_saude/tv2.html
index c47d74b..5db863c 100644
--- a/profissional_saude/templates/profissional_saude/tv2.html
+++ b/profissional_saude/templates/profissional_saude/tv2.html
@@ -7,9 +7,12 @@
Painel de Chamadas
@@ -217,7 +211,7 @@
Histórico de Confirmações
$('#senha-chamada-texto').data('nome', data.nome_completo); // Atualiza o data-nome
$('#senha-chamada-texto').data('sala', data.sala_profissional); // Atualiza o data-sala
- falarSenhaTresVezes(data.nome_completo, data.sala_profissional);
+ falarSenhaTresVezes(data.nome_completo, data.sala_profissional, data.profissional_nome);
ultimaChamadaId = data.id; // Atualiza o ID da última chamada
} else {
@@ -232,7 +226,7 @@
Histórico de Confirmações
// Função para chamar o paciente
- function falarSoletracao(nomeCompleto, profissionalNome) {
+ function falarSoletracao(nomeCompleto, sala, profissionalNome) {
console.log("Função falarSoletracao chamada!");
if (!synth) {
@@ -240,13 +234,13 @@
Histórico de Confirmações
return;
}
- if (!nomeCompleto || !profissionalNome) {
- console.log("Nome ou Profissional não definidos. Não há nada para falar.");
+ if (!nomeCompleto || !sala || !profissionalNome) {
+ console.log("Nome, sala ou profissional não definidos. Não há nada para falar.");
return;
}
var utterThis = new SpeechSynthesisUtterance();
- utterThis.text = "Paciente " + nomeCompleto + ", dirija-se à sala " + salaConsulta + " para consulta com o Doutor " + profissionalNome + "."; // Formata a frase
+ utterThis.text = "Paciente " + nomeCompleto + ", dirija-se à sala " + sala + "."; // Formata a frase
console.log("String de fala:", utterThis.text);
utterThis.lang = 'pt-BR';
@@ -270,7 +264,11 @@
Histórico de Confirmações
}
utterThis.onerror = function(event) {
- console.error('Erro ao falar:', event.error);
+ if (event.error === 'not-allowed') {
+ console.log('Fala bloqueada pelo navegador. Clique na página para permitir.');
+ } else {
+ console.error('Erro ao falar:', event.error);
+ }
}
synth.speak(utterThis);
@@ -297,9 +295,9 @@