O problema
A escala está num papel na parede, o ponto num caderno, e o custo de pessoal aparece no fim do mês na contabilidade, quando já não há nada a fazer. Ninguém sabe que a Marta fez catorze minutos a mais todos os dias até serem sete horas no fim do mês. E quando alguém se esquece de picar a saída, o gestor mete uma hora à vista, sem registo de que meteu.
Modelo de dados
Três tabelas. Empregados, com custo-hora e horas contratadas. Turnos, que são o planeado: quem, que dia, de que horas a que horas, com que pausa. Registos de ponto, que são o real: entrada, saída, e se o gestor corrigiu, a correcção ao lado do valor picado, nunca por cima. Separar planeado de real é a decisão que faz o resto funcionar.
| Tabela | Para que serve |
|---|---|
| demo_employees | Quem trabalha na casa: secção, custo-hora e horas contratadas. |
| demo_shifts | O planeado: quem, que dia, de que horas a que horas, com que pausa e em que posto. |
| demo_time_entries | O real: entrada e saída picadas, e ao lado, se houver, a correcção do gestor com motivo, e a aprovação. |
Dois turnos não se sobrepõem
Um empregado não pode estar escalado em dois turnos ao mesmo tempo. A regra não é um aviso no ecrã: é uma restrição de exclusão no Postgres, sobre o intervalo de tempo de cada turno. Um turno das 20h às 23h30 para quem já está das 17h às 23h é recusado pela base de dados. Um das 23h às 23h30 entra, porque não se sobrepõe.
-- Um empregado não está em dois turnos ao mesmo tempo.
-- Restrição de exclusão sobre o intervalo de cada turno, na base de
-- dados, não num aviso no ecrã.
create extension if not exists btree_gist;
alter table demo_shifts add constraint demo_shifts_no_overlap
exclude using gist (
room_code with =,
employee_id with =,
tsrange(day + starts, day + ends) with &&
);As horas calculam-se no servidor
Uma função devolve a folha de ponto já calculada: minutos planeados a partir do turno, minutos reais a partir do ponto, ambos descontada a pausa. O atraso é a entrada contra a hora do turno. As horas extra são o real menos o planeado, nunca negativo. O custo é o real ao custo-hora. Uma só fonte de verdade, e o ecrã só mostra.
-- Planeado vem do turno, real vem do ponto. Uma só fonte de verdade.
scheduled_min := (extract(epoch from (s.ends - s.starts)) / 60) - s.break_min;
actual_min := (extract(epoch from (coalesce(e.adjusted_out, e.clock_out) - e.clock_in)) / 60)
- s.break_min;
late_min := greatest(0, extract(epoch from (e.clock_in - (s.day + s.starts))) / 60);
overtime_min := greatest(0, actual_min - scheduled_min);
cost_cents := round(actual_min / 60.0 * emp.hourly_cents);
-- Repare no coalesce: se o gestor corrigiu a saída, é a corrigida
-- que conta. A picada fica na tabela, ao lado, como estava.Corrigir não é apagar
Quando falta a saída, o gestor mete uma: com motivo obrigatório, assinada e datada. A saída picada fica como estava; a corrigida fica ao lado, e é a corrigida que entra no cálculo. Aprovar exige uma saída, picada ou corrigida, e depois de aprovado não se corrige mais. A restrição está na tabela, não só na função.
-- Corrigir não é apagar. A correcção fica ao lado do valor picado,
-- com motivo obrigatório, assinada e datada.
check (adjusted_out is null or adjust_reason is not null)
-- Não se aprova sem saída, picada ou corrigida:
check (approved_at is null or clock_out is not null or adjusted_out is not null)
-- E depois de aprovado não se corrige mais:
update demo_time_entries set adjusted_out = p_out, ...
where id = p_entry and room_code = p_room and approved_at is null;O que faria diferente num cliente real
Picagem com PIN ou cartão num tablet fixo, para a entrada ser de quem diz que é. Regras de horas extra da convenção colectiva, com majorações. Exportação para o processamento de salários. Escala semanal com arrastar e largar, e aviso quando alguém passa das horas contratadas. E o custo de pessoal contra as vendas do dia, que é o número que o dono quer ver.