Planejador de Cobertura de Férias e Licenças da Equipe

De Wikiprompt, a enciclopédia livre de prompts

Fatih Kadir Akın
Contribuído porFatih Kadir AkınFonte

2 de nov. de 2026

Planejador de Cobertura de Férias e Licenças da Equipe Uma habilidade completa de IA para planejar férias de equipe em torno de equipe mínima, com regras de cobertura, prioridade justa, sugestões alternativas e um script de planejador em Python.

Conteúdo do PromptSalvar

🌐
--- name: planejador-de-folgas-e-cobertura-de-feriados-da-equipe description: Planeja as folgas da equipe em torno da cobertura mínima, especialmente durante os feriados - verifica cada dia útil contra as regras de cobertura por função (uma pessoa preenche uma vaga), decide quais solicitações podem ser aprovadas em uma ordem de prioridade justa, oferece datas alternativas sem lacunas, respeita períodos de bloqueio, dias de meio período e meios dias, e acompanha saldos, acúmulo e dias de pico de folga. Inclui um planejador Python offline da biblioteca padrão com --self-test. --- # Planejador de Folgas e Cobertura de Feriados da Equipe Você ajuda líderes de equipe, gerentes de suporte e operações, gerentes de clínicas e lojas a responder a pergunta de dezembro: quem pode folgar, quando, sem deixar o balcão vazio, e isso é justo? As planilhas de folga mostram quem está ausente, mas não se as pessoas restantes podem realmente cobrir as funções (um líder que também é o único engenheiro de nível 2 não pode ser ambos no mesmo dia). Você transforma a equipe, as regras de cobertura e as solicitações de folga em uma verificação dia a dia, decide as solicitações em uma ordem de prioridade declarada, oferece alternativas em vez de um "não" seco, e sinaliza problemas de saldo e justiça antes que se tornem conversas de RH. ## Arquivos nesta habilidade - `scripts/leave_cover.py` - lê um JSON da equipe (pessoas, funções, dias de meio período, saldos, mínimos de cobertura, feriados, exceções, dias de bloqueio e pico) e um CSV de folgas; verifica cada dia útil com um emparelhamento de uma pessoa por vaga, encontra lacunas causadas por folgas aprovadas, decide solicitações em aberto (quem trabalhou no pico no ano passado primeiro, depois por ordem de chegada), sugere as datas sem lacunas mais próximas ou uma viagem mais curta, e relata uma grade de calendário, tabela de saldos e descobertas LV01-LV07 (somente biblioteca padrão Python 3.8+) - `references/coverage-rules.md` - como definir mínimos por função, exceções para dias calmos, funcionários de meio período, meios dias, plantão e o que o emparelhamento faz - `references/fair-leave-policy.md` - regras de prioridade, períodos de bloqueio, saldo e acúmulo, como dizer não educadamente, e o que cada código de descoberta significa - `templates/leave-plan-brief.md` - o plano para enviar à equipe - `examples/support-team.json` - uma equipe de suporte ao cliente de 7 pessoas durante o Natal e Ano Novo: líderes, nível 2, plantão, um funcionário de meio período, feriados, cobertura reduzida em 24 e 31 de dezembro, um bloqueio de fechamento de faturamento e limites de acúmulo - `examples/support-leave.csv` - 3 folgas aprovadas e 7 solicitadas, incluindo um meio dia - `examples/support-plan.md` - a saída real do planejador nesses arquivos ## Fluxo de trabalho ### 1. Colete as entradas Pergunte por: o período (cubra todo o trecho de feriados mais uma semana de cada lado), a equipe com as funções e dias de trabalho de cada pessoa, saldos e dias já usados neste ano de férias, feriados públicos em que a equipe está fechada (ou a cobertura reduzida se estiver aberta), a cobertura mínima por função e no total, dias mais calmos com mínimos menores, períodos de bloqueio (lançamentos, inventários, fechamentos de faturamento, auditorias) e os dias de pico que as pessoas disputam. Depois as folgas: aprovadas, solicitadas (com data de envio), treinamentos, licenças parentais ou médicas conhecidas. Pergunte o que a equipe concordou sobre prioridade; se nada, sugira "quem trabalhou no pico no ano passado primeiro, depois por ordem de chegada". ### 2. Execute o planejador ``` python3 scripts/leave_cover.py examples/support-team.json examples/support-leave.csv python3 scripts/leave_cover.py team.json leave.csv --json python3 scripts/leave_cover.py --self-test ``` Saída 0 = folgas aprovadas mantêm a cobertura e ninguém excede o saldo, 1 = folgas aprovadas já quebram a cobertura ou excedem um saldo, 2 = entrada inválida (a mensagem nomeia a linha). Sem um shell, monte o calendário manualmente: uma linha por pessoa, uma coluna por dia útil, então para cada dia atribua pessoas trabalhando às vagas de função, uma pessoa por vaga, e marque as deficiências. ### 3. Leia o resultado - Lacunas de folgas já aprovadas (LV01) vêm primeiro: elas precisam de uma correção agora (cobertura temporária, uma troca, uma agência ou acordo de plantão), não de uma nova regra. - Problemas de saldo (LV02): folgas aprovadas acima do saldo precisam que o RH confirme acúmulo ou dias não pagos. - Solicitações: aprove as marcadas como aprovar; para conflitos e bloqueios, leve a alternativa à pessoa, não uma recusa (LV04). - Justiça (LV05) e dias de pico de folga: verifique se ninguém fica com os melhores dias dois anos seguidos enquanto alguém que os trabalhou é recusado. - Fragilidade: funções sem reserva (LV03) e dias com folga zero (LV07) precisam de um substituto nomeado. - Use-ou-perca (LV06): pessoas que perderão dias devem reservá-los em semanas calmas. ### 4. Ajuste e re-execute Tente as correções óbvias com os mesmos arquivos: um substituto com treinamento cruzado (adicione uma função a alguém), um mínimo menor em um dia calmo (exceção), uma troca entre duas pessoas, ou dividir uma viagem longa. Re-execute até que as folgas aprovadas não tenham lacunas e cada solicitação em aberto tenha uma decisão ou uma alternativa concreta. ### 5. Entregue Preencha `templates/leave-plan-brief.md`: a regra usada, a decisão por solicitação com o motivo e a alternativa, o calendário, os acordos de cobertura para dias críticos e as notas de saldo. Converse com as pessoas cujas solicitações não foram aprovadas antes de publicar o plano. ## Exemplo de resultado No exemplo da equipe de suporte, a viagem aprovada de Omar já o coloca 3 dias acima de seu saldo de 25 dias (LV02, alto: o RH deve confirmar acúmulo ou dias não pagos). Sam trabalhou no último Natal, então sua solicitação de 24 de dezembro a 4 de janeiro é decidida primeiro e aprovada; Jonas (21-23 de dezembro) e o meio dia de Priya e 21-23 de dezembro também são aprovados. A solicitação de Ade de 29-31 de dezembro deixaria o nível 2 e a cobertura total curtos em 29 e 30 de dezembro; o planejador oferece 16-18 de dezembro em vez disso. A solicitação de Lena de 30 de dezembro a 8 de janeiro cruza o bloqueio de fechamento de faturamento de 5 a 6 de janeiro, e apenas 31 de dezembro pode ser aprovado sem lacuna. A segunda viagem de Jonas precisa de 5 dias com 4 restantes. Lena perderia 10 dias para o limite de acúmulo de 5 dias, e 21-23 e 29 de dezembro não têm folga alguma. ## Regras - Verifique a cobertura por função com uma pessoa por vaga, não apenas pela contagem de cabeças. - Aplique uma regra de prioridade declarada a todos e mostre a ordem; nunca decida por quem pediu mais alto. - Sempre acompanhe um "desta vez não" com uma data alternativa, uma viagem mais curta ou uma troca. - Trate licenças médicas, parentais e outras licenças protegidas como fixas: nunca peça às pessoas para movê-las e mantenha detalhes médicos fora do plano. - Saldos, acúmulo e licenças não pagas seguem o contrato e a lei local; sinalize-os para o RH em vez de decidir. - Compartilhe o plano apenas com a equipe e gerentes que precisam dele. ## Regras de cobertura ### Mínimos - Defina um mínimo por **função** (o que deve ser coberto: líder/aprovador, nível 2, plantão, farmacêutico, chaveiro, licença de empilhadeira) e um total **qualquer** (mãos suficientes para o volume). - Baseie os números na demanda, não no hábito: tickets/chamados/pedidos por dia dividido pelo que uma pessoa lida, mais as funções que devem estar presentes por lei ou contrato. - Dias calmos (24 e 31 de dezembro, a semana entre feriados em muitas equipes B2B) geralmente justificam um mínimo menor: use `coverage_overrides` para essas datas em vez de reduzir todo o período. - Dias em que o negócio está fechado vão em `holidays`: sem necessidade de cobertura, sem cobrança de saldo. ### Uma pessoa, uma vaga Uma pessoa que ocupa várias funções ainda preenche apenas uma vaga por dia. O planejador atribui pessoas às vagas com um emparelhamento bipartido, então ele encontra o arranjo que funciona se existir: - Maya (líder, nível 2) + Lena (nível 1, nível 2) podem cobrir líder 1 + nível 2 1, mas não líder 1 + nível 2 2. - Deficiências são relatadas por função; "qualquer" curto significa poucas pessoas no total. ### Pessoas - `days`: dias úteis da semana dos funcionários de meio período; folgas em seus dias não úteis não custam saldo e não afetam a cobertura. - Meios dias (`half_day=yes`) custam 0,5 dias de saldo; o planejador não conta a pessoa para cobertura naquele dia (conservador). Se uma ausência só pela manhã ainda cobre o turno da tarde, diga isso no briefing. - Treinamento, licença médica, parental e não paga reduzem a cobertura como férias, mas não contam contra o saldo. ### Plantão e cobertura remota - Se alguém cobre o plantão de casa durante as férias, não o marque como em férias para essa função; concorde explicitamente e pague conforme a política. O planejador trata as férias como totalmente ausente. - Um substituto externo ou de agência pode ser adicionado como uma pessoa com apenas essa função e sem saldo. ### Folga - Dias com folga zero (exatamente o mínimo trabalhando) quebram com um dia de licença médica. Nomeie um substituto para cada dia desses antes do período começar. - Uma função com apenas mínimo + 1 titulares não sobrevive a duas ausências; planeje treinamento cruzado no novo ano. ## Decisões justas de folga ### Regra de prioridade (declare-a antes das solicitações chegarem) Ordem comum e defensável: 1. Folgas já aprovadas permanecem (a menos que a pessoa concorde em mudar). 2. Licenças protegidas (parental, médica, estatutária) são fixas. 3. Para dias de pico: pessoas que trabalharam no pico no ano passado vão primeiro. 4. Depois, por ordem de chegada pela data de envio. 5. Empates: rotacione (quem perdeu o último empate vence este). Evite regras somente por antiguidade para dias de pico; elas parecem injustas para funcionários mais novos e muitas vezes perdem bons profissionais. ### Períodos de bloqueio - Mantenha-os curtos e ligados a um evento real (lançamento, fechamento de faturamento, inventário, auditoria, início do período letivo para um negócio de tutoria). Anuncie-os cedo. - Uma solicitação que toca um bloqueio não é automaticamente recusada: o planejador oferece as datas sem lacunas mais próximas ou a parte mais longa antes do bloqueio. Exceções precisam de um aprovador nomeado. ### Saldo e acúmulo - Restante = saldo - usado antes do período - aprovado - recomendado. - Folgas aprovadas acima do saldo (LV02 alto) são uma questão de RH: acúmulo do ano passado, compra de dias extras ou licença não paga. - Limites de acúmulo significam que dias não reservados são perdidos no final do ano (LV06): incentive as pessoas a reservar semanas calmas; em alguns países, o mínimo estatutário de férias não pode simplesmente expirar, então verifique as regras locais. ### Dizer não educadamente - Explique a regra e a lacuna específica ("o nível 2 ficaria uma pessoa curto em 29 e 30 de dezembro"). - Ofereça a alternativa que o planejador encontrou, ou uma troca com um colega nomeado, ou uma viagem mais curta. - Decida cedo (pelo menos 4-6 semanas antes dos feriados) para que as pessoas ainda possam reservar viagens. - Registre a decisão e o motivo; a prioridade do próximo ano depende disso. ### Códigos de descoberta (leave_cover.py) | Código | Gravidade | Significado | |---|---|---| | LV01 | alto | folgas aprovadas já deixam uma função ou o total curto em um dia | | LV02 | alto / médio | folgas aprovadas acima do saldo / solicitação precisa de mais dias do que restam | | LV03 | médio / baixo | função ocupada por não mais pessoas do que o mínimo / apenas uma reserva | | LV04 | médio | solicitação conflita com cobertura ou bloqueio, com a alternativa oferecida | | LV05 | médio | alguém que trabalhou no pico no ano passado é recusado enquanto outros ficam com dias de pico | | LV06 | baixo | dias restantes acima do limite de acúmulo serão perdidos | | LV07 | informação | dias com folga zero após o plano | ## Modelo de plano de férias # Plano de férias: <equipe>, <período> **Regra de cobertura:** <líder 1, nível 2 2 (1 em 24/31 dez), plantão 1, pelo menos 4 pessoas> **Fechado:** <25 dez, 28 dez, 1 jan> **Prioridade usada:** <quem trabalhou no pico no ano passado primeiro, depois por ordem de chegada> **Bloqueio:** <fechamento de faturamento de 5 a 6 de janeiro> ## Decisões | Pessoa | Pediu | Decisão | Porquê | Alternativa oferecida | |---|---|---|---|---| | <Sam> | <24 dez - 4 jan> | <aprovado> | <trabalhou no último Natal> | - | | <Ade> | <29-31 dez> | <não nestas datas> | <nível 2 curto 29-30 dez> | <16-18 dez, ou troca com ...> | ## Calendário ``` <cole a grade do planejador> ``` ## Dias críticos e substitutos | Dia | Porquê crítico | Substituto | |---|---|---| | <21-23 dez> | <exatamente 4 trabalhando> | <plantão de casa: ...> | ## Notas de saldo (para RH) - <Omar: viagem aprovada 3 dias acima do saldo - confirmar acúmulo / não pago> - <Lena: 15 dias restantes, limite de acúmulo 5 - sugerir reservar fevereiro> ## Próximos passos - <conversar com Ade, Lena, Jonas antes de publicar> - <publicar o plano em ...; mudanças por solicitação de troca para ...> - <treinamento cruzado de um segundo substituto de líder no 1º trimestre> ## Exemplo de JSON da equipe { "team": "Suporte ao Cliente Northwind", "period": "2026-12-14..2027-01-08", "workdays": ["mon", "tue", "wed", "thu", "fri"], "holidays": ["2026-12-25", "2026-12-28", "2027-01-01"], "coverage": {"lead": 1, "tier2": 2, "oncall": 1, "any": 4}, "coverage_overrides": [ {"dates": "2026-12-24", "coverage": {"tier2": 1, "any": 3}}, {"dates": "2026-12-31", "coverage": {"tier2": 1, "any": 3}} ], "blackout": [ {"dates": "2027-01-05..2027-01-06", "reason": "Fechamento de faturamento de janeiro, todos os envolvidos"} ], "peak_days": "2026-12-21..2027-01-01", "carryover_max": 5, "people": [ {"name": "Maya", "roles": ["lead", "tier2"], "allowance": 25, "taken": 17, "worked_peak_last_year": true}, {"name": "Ade", "roles": ["lead", "tier2"], "allowance": 25, "taken": 19}, {"name": "Omar", "roles": ["tier2", "oncall"], "allowance": 25, "taken": 20}, {"name": "Lena", "roles": ["tier1", "tier2"], "allowance": 25, "taken": 10}, {"name": "Jonas", "roles": ["tier1", "oncall"], "allowance": 25, "taken": 18}, {"name": "Sam", "roles": ["tier1", "oncall"], "allowance": 25, "taken": 15, "worked_peak_last_year": true}, {"name": "Priya", "roles": ["tier1"], "days": ["mon", "wed", "thu"], "allowance": 15, "taken": 9} ] } ## Exemplo de CSV de folgas person,start,end,type,status,submitted,half_day,notes Maya,2026-12-21,2026-12-24,vacation,approved,2026-09-30,,visita à família Omar,2026-12-29,2027-01-08,vacation,approved,2026-10-02,,"passagens compradas, Lagos" Lena,2026-12-14,2026-12-15,training,approved,2026-11-01,,curso de administração Zendesk Jonas,2026-12-21,2026-12-23,vacation,requested,2026-10-20,, Ade,2026-12-29,2026-12-31,vacation,requested,2026-10-25,, Sam,2026-12-24,2027-01-04,vacation,requested,2026-11-05,,"esqui, trabalhou no último Natal" Lena,2026-12-30,2027-01-08,vacation,requested,2026-11-12,, Priya,2026-12-16,2026-12-16,vacation,requested,2026-11-20,yes,peça de natal da escola Jonas,2027-01-04,2027-01-08,vacation,requested,2026-11-25,, Priya,2026-12-21,2026-12-23,vacation,requested,2026-11-28,, ## Exemplo de saída do planejador # Plano de cobertura de folgas: Suporte ao Cliente Northwind (2026-12-14 a 2027-01-08) Cobertura mínima por dia útil: qualquer 4, líder 1, plantão 1, nível 2 2. Feriados (fechado): 25 dez, 28 dez, 01 jan. **Veredito: acima do saldo em folgas aprovadas** - 4 de 7 solicitações em aberto podem ser aprovadas sem lacuna; 3 precisam de uma decisão. ## Solicitações (em ordem de decisão: quem trabalhou no pico no ano passado primeiro, depois por ordem de chegada) | # | Pessoa | Datas | Dias úteis | Tipo | Enviada | Decisão | Porquê | Alternativa | |---|---|---|---|---|---|---|---|---| | 1 | Sam | Qui 24 dez - Seg 04 jan | 5 | vacation | 2026-11-05 | aprovar | - | - | | 2 | Jonas | Seg 21 dez - Qua 23 dez | 3 | vacation | 2026-10-20 | aprovar | - | - | | 3 | Ade | Ter 29 dez - Qui 31 dez | 3 | vacation | 2026-10-25 | conflito | qualquer/nível 2 curto em 29 dez, 30 dez | Qua 16 dez a Sex 18 dez | | 4 | Lena | Qua 30 dez - Sex 08 jan | 7 | vacation | 2026-11-12 | bloqueio | Fechamento de faturamento de janeiro, todos os envolvidos (05 jan, 06 jan) | encurtar para Qui 31 dez - Qui 31 dez (1 dia(s) útil(eis), de volta antes do bloqueio) | | 5 | Priya | Qua 16 dez - Qua 16 dez (meio) | 0.5 | vacation | 2026-11-20 | aprovar | - | - | | 6 | Jonas | Seg 04 jan - Sex 08 jan | 5 | vacation | 2026-11-25 | acima do saldo | precisa de 5 dias, 4 restantes | - | | 7 | Priya | Seg 21 dez - Qua 23 dez | 2 | vacation | 2026-11-28 | aprovar | - | - | ## Calendário (aprovado + recomendado) ``` D J 11111222222233000000 45678123458901145678 MTWTFMTWTFMTWTFMTWTF Maya .....VVVVHH...H..... Ade .........HHxxxH..... Omar .........HHVVVHVVVVV Lena TT.......HH.xxHxxxxx Jonas .....vvv.HH...Hxxxxx Sam ........vHHvvvHv.... Priya .-h.-v-v.HH-..H.-..- cover any +++++++++ +++ +++++ cover lead +++++++++ +++ +++++ cover oncall +++++++++ +++ +++++ cover tier2 +++++++++ +++ +++++ ``` V/T/S/P/U/O férias/treinamento/licença médica/parental/não paga/outro aprovado, minúsculo = solicitação recomendada para aprovação, h = meio dia, x = solicitação não aprovada, H = feriado, - = não é dia útil, + / ! = função coberta / curta. ## Saldo (dias) | Pessoa | Saldo | Usado antes | Aprovado aqui | Recomendado | Restante | Dias de pico de folga | |---|---|---|---|---|---|---| | Maya | 25 | 17 | 4 | 0 | 4 | 4 | | Ade | 25 | 19 | 0 | 0 | 6 | 0 | | Omar | 25 | 20 | 8 | 0 | -3 | 3 | | Lena | 25 | 10 | 0 | 0 | 15 | 0 | | Jonas | 25 | 18 | 0 | 3 | 4 | 3 | | Sam | 25 | 15 | 0 | 5 | 5 | 4 | | Priya | 15 | 9 | 0 | 2.5 | 3.5 | 2 | ## Descobertas - **ALTO LV02** A folga aprovada de Omar excede o saldo em 3 dias (20 + 8 usados de 25); confirme acúmulo ou licença não paga com o RH. - **MÉDIO LV02** Jonas 04 jan-08 jan: precisa de 5 dias, 4 restantes; aprove uma viagem mais curta ou concorde dias não pagos - **MÉDIO LV04** Ade 29 dez-31 dez: conflito (qualquer/nível 2 curto em 29 dez, 30 dez); ofereça Qua 16 dez a Sex 18 dez - **MÉDIO LV04** Lena 30 dez-08 jan: bloqueio (Fechamento de faturamento de janeiro, todos os envolvidos (05 jan, 06 jan)); ofereça encurtar para Qui 31 dez - Qui 31 dez (1 dia(s) útil(eis), de volta antes do bloqueio) - **BAIXO LV03** líder tem uma pessoa de reserva (Maya, Ade para um mínimo de 1); duas ausências ao mesmo tempo quebram. - **BAIXO LV06** Ade ainda terá 6 dias restantes (limite de acúmulo 5); 1 dia(s) são perdidos a menos que reservados. - **BAIXO LV06** Lena ainda terá 15 dias restantes (limite de acúmulo 5); 10 dia(s) são perdidos a menos que reservados. - **INFO LV07** Sem folga em 4 dia(s) (21 dez, 22 dez, 23 dez, 29 dez): um dia de licença médica quebra a cobertura; alinhe um substituto. ## Script do planejador Python ```python #!/usr/bin/env python3 """Planejador de folgas e cobertura de feriados da equipe (somente biblioteca padrão Python 3.8+). Verifica as solicitações de folga de uma equipe contra as regras de pessoal mínimo dia a dia, decide quais solicitações podem ser aprovadas sem deixar uma lacuna, sugere datas alternativas para o resto, e acompanha saldos e quem fica com os dias de pico de feriados. JSON da equipe: { "team": "Mesa de Suporte", "period": "2026-12-14..2027-01-08", "workdays": ["mon", "tue", "wed", "thu", "fri"], "holidays": ["2026-12-25", "2026-12-28", "2027-01-01"], (escritório fechado, sem necessidade de cobertura) "coverage": {"lead": 1, "tier2": 2, "oncall": 1, "any": 4}, ("any" = total de pessoas trabalhando) "coverage_overrides": [{"dates": "2026-12-24", "coverage": {"tier2": 1, "any": 3}}], "blackout": [{"dates": "2027-01-05..2027-01-06", "reason": "fechamento de faturamento"}], "peak_days": "2026-12-21..2027-01-01", "people": [ {"name": "Maya", "roles": ["lead", "tier2"], "allowance": 25, "taken": 17, "worked_peak_last_year": true}, {"name": "Priya", "roles": ["tier1"], "days": ["mon", "wed", "thu"], "allowance": 15, "taken": 9} ] } Cada pessoa trabalhando preenche UMA vaga de função por dia (um líder que também é nível 2 conta uma vez), resolvido como um emparelhamento bipartido. CSV de folgas (cabeçalho obrigatório): person,start,end,type,status,submitted,half_day,notes type vacation (conta contra o saldo) | sick | training | parental | unpaid | other status approved | requested | declined (ignorado) half_day yes = meio dia de saldo; a pessoa não é contada para cobertura naquele dia Uso: python3 leave_cover.py team.json leave.csv [--json] python3 leave_cover.py --self-test Códigos de saída: 0 = folgas aprovadas mantêm a cobertura e ninguém está acima do saldo, 1 = folgas aprovadas já quebram a cobertura ou excedem um saldo, 2 = entrada inválida. """ import argparse import csv import datetime as dt import json import sys from collections import OrderedDict, defaultdict DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] TYPE_CODE = {"vacation": "V", "sick": "S", "training": "T", "parental": "P", "unpaid": "U", "other": "O"} class InputError(ValueError): pass def d(s, what="data"): try: return dt.date.fromisoformat(str(s).strip()) except ValueError: raise InputError("%s %r não é AAAA-MM-DD" % (what, s)) def date_range(spec): spec = str(spec) a, _, b = spec.partition("..") a, b = d(a), d(b or a) if b < a: raise InputError("intervalo %r termina antes de começar" % spec) return [a + dt.timedelta(days=i) for i in range((b - a).days + 1)] # ----------------------------------------------------------------------------- entradas def load_team(cfg): if "period" not in cfg or not cfg.get("people"): raise InputError("JSON da equipe precisa de 'period' e 'people'") t = dict(cfg) t["days"] = date_range(cfg["period"]) t["workdays"] = [w.lower()[:3] for w in cfg.get("workdays", DAYS[:5])] t["holidays"] = {d(h, "feriado") for h in cfg.get("holidays", [])} t["coverage"] = {k: int(v) for k, v in (cfg.get("coverage") or {}).items()} over = {} for o in cfg.get("coverage_overrides", []): for day in date_range(o["dates"]): over[day] = {k: int(v) for k, v in o["coverage"].items()} t["overrides"] = over t["blackout"] = [(set(date_range(b["dates"])), b.get("reason", "bloqueio")) for b in cfg.get("blackout", [])] t["peak"] = set(date_range(cfg["peak_days"])) if cfg.get("peak_days") else set() people = OrderedDict() for p in cfg["people"]: if "name" not in p: raise InputError("toda pessoa precisa de um nome") q = dict(p) q["roles"] = list(p.get("roles", [])) q["days"] = [w.lower()[:3] for w in p.get("days", t["workdays"])] q["allowance"] = float(p.get("allowance", 0)) q["taken"] = float(p.get("taken", 0)) q["carryover_max"] = p.get("carryover_max", cfg.get("carryover_max")) people[p["name"]] = q t["people"] = people roles = {r for p in people.values() for r in p["roles"]} for r in list(t["coverage"]) + [r for o in over.values() for r in o]: if r != "any" and r not in roles: raise InputError("função de cobertura %r não é ocupada por ninguém" % r) return t def load_leave(path, team): out = [] with open(path, newline="", encoding="utf-8") as f: rows = [r for r in f if r.strip() and not r.lstrip().startswith("#")] for n, row in enumerate(csv.DictReader(rows), 2): name = (row.get("person") or "").strip() if name not in team["people"]: raise InputError("linha de folga %d: pessoa desconhecida %r" % (n, name)) typ = (row.get("type") or "vacation").strip().lower() if typ not in TYPE_CODE: raise InputError("linha de folga %d: tipo deve ser um de %s" % (n, ", ".join(TYPE_CODE))) status = (row.get("status") or "requested").strip().lower() if status not in ("approved", "requested", "declined"): raise InputError("linha de folga %d: status deve ser approved, requested ou declined" % n) start, end = d(row.get("start"), "início"), d(row.get("end") or row.get("start"), "fim") if end < start: raise InputError("linha de folga %d: fim antes do início" % n) out.append({"id": n, "person": name, "start": start, "end": end, "type": typ, "status": status, "submitted": (row.get("submitted") or "").strip() or "9999-12-31", "half": (row.get("half_day") or "").strip().lower() in ("yes", "y", "true", "1"), "notes": (row.get("notes") or "").strip()}) return out # ----------------------------------------------------------------------------- núcleo def is_workday(team, day): return DAYS[day.weekday()] in team["workdays"] and day not in team["holidays"] def person_works(team, p, day): return is_workday(team, day) and DAYS[day.weekday()] in p["days"] def leave_days(team, lv): p = team["people"][lv["person"]] days = [] cur = lv["start"] while cur <= lv["end"]: if person_works(team, p, cur): days.append(cur) cur += dt.timedelta(days=1) return days def charge(team, lv): n = len(leave_days(team, lv)) return 0.5 * n if lv["half"] else float(n) def need_for(team, day): need = dict(team["coverage"]) need.update(team["overrides"].get(day, {})) return need def shortfall(team, day, off): """Retorna {função: faltando} para um dia dado o conjunto de pessoas ausentes.""" if not is_workday(team, day): return {} avail = [p for n, p in team["people"].items() if n not in off and person_works(team, p, day)] need = need_for(team, day) slots = [r for r, k in sorted(need.items()) if r != "any" for _ in range(k)] match = {} # índice da vaga -> nome da pessoa def try_assign(pi, seen): for si, role in enumerate(slots): if role in avail[pi]["roles"] and si not in seen: seen.add(si) if si not in match or try_assign(match[si], seen): match[si] = pi return True return False for pi in range(len(avail)): try_assign(pi, set()) short = defaultdict(int) for si, role in enumerate(slots): if si not in match: short[role] += 1 if need.get("any", 0) > len(avail): short["any"] = need["any"] - len(avail) return dict(short) def off_map(team, leaves): off = defaultdict(set) for lv in leaves: for day in leave_days(team, lv): off[day].add(lv["person"]) return off def gaps(team, leaves): off = off_map(team, leaves) out = OrderedDict() for day in team["days"]: s = shortfall(team, day, off.get(day, set())) if s: out[day] = s return out def total(g): return sum(sum(v.values()) for v in g.values()) def new_gaps(before, after): out = OrderedDict() for day, s in after.items(): b = before.get(day, {}) worse = {r: k - b.get(r, 0) for r, k in s.items() if k > b.get(r, 0)} if worse: out[day] = worse return out def blackout_hit(team, lv): days = set(leave_days(team, lv)) for bdays, reason in team["blackout"]: hit = sorted(days & bdays) if hit: return hit, reason return None, None def suggest(team, current, lv, max_shift=15): """Deslocamento mais próximo da mesma solicitação (mesmo comprimento de calendário) que mantém a cobertura.""" length = (lv["end"] - lv["start"]).days want = len(leave_days(team, lv)) base = gaps(team, current) first, last = team["days"][0], team["days"][-1] for shift in sorted(range(-max_shift, max_shift + 1), key=lambda x: (abs(x), x)): if shift == 0: continue cand = dict(lv, start=lv["start"] + dt.timedelta(days=shift), end=lv["start"] + dt.timedelta(days=shift + length)) if cand["start"] < first or cand["end"] > last: continue if len(leave_days(team, cand)) < want or blackout_hit(team, cand)[0]: continue if not new_gaps(base, gaps(team, current + [cand])): return cand return None def trimmed(team, current, lv): """Parte mais longa de uma solicitação que atinge o bloqueio, antes do primeiro dia de bloqueio, que mantém a cobertura.""" hit, _ = blackout_hit(team, lv) days = [x for x in leave_days(team, lv) if x < hit[0]] base = gaps(team, current) best = None for i in range(len(days)): for j in range(len(days) - 1, i - 1, -1): if best and j - i + 1 <= len(leave_days(team, best)): break cand = dict(lv, start=days[i], end=days[j]) if not new_gaps(base, gaps(team, current + [cand])): best = cand break return best def plan(team, leaves): findings = [] def add(sev, code, msg): findings.append({"severity": sev, "code": code, "message": msg}) approved = [l for l in leaves if l["status"] == "approved"] requests = [l for l in leaves if l["status"] == "requested"] used = defaultdict(float) for l in approved: if l["type"] == "vacation": used[l["person"]] += charge(team, l) g_approved = gaps(team, approved) for day, s in g_approved.items(): add("high", "LV01", "%s %s: folgas aprovadas já deixam %s curto" % ( day.isoformat(), DAYS[day.weekday()].title(), ", ".join("%s por %d" % (r, k) for r, k in sorted(s.items())))) def priority(l): p = team["people"][l["person"]] touches_peak = bool(set(leave_days(team, l)) & team["peak"]) return (0 if touches_peak and p.get("worked_peak_last_year") else 1, l["submitted"], l["id"]) current = list(approved) decisions = [] for l in sorted(requests, key=priority): p = team["people"][l["person"]] days = leave_days(team, l) c = charge(team, l) if l["type"] == "vacation" else 0.0 dec = {"leave": l, "workdays": len(days), "charge": c, "decision": "approve", "reason": "", "suggestion": None} if not days: dec.update(decision="sem dias úteis", reason="cai apenas em feriados / dias não úteis") elif l["type"] == "vacation" and p["allowance"] and p["taken"] + used[l["person"]] + c > p["allowance"] + 1e-9: left = p["allowance"] - p["taken"] - used[l["person"]] dec.update(decision="acima do saldo", reason="precisa de %g dias, %g restantes" % (c, max(left, 0))) else: hit, reason = blackout_hit(team, l) if hit: dec.update(decision="bloqueio", reason="%s (%s)" % (reason, ", ".join(x.strftime("%d %b") for x in hit))) else: ng = new_gaps(gaps(team, current), gaps(team, current + [l])) if ng: roles = sorted({r for s in ng.values() for r in s}) dec.update(decision="conflito", reason="%s curto em %s" % ( "/".join(roles), ", ".join(x.strftime("%d %b") for x in ng))) if dec["decision"] == "approve": current.append(l) used[l["person"]] += c elif dec["decision"] in ("conflito", "bloqueio"): s = suggest(team, current, l) if s: dec["suggestion"] = "%s a %s" % (s["start"].strftime("%a %d %b"), s["end"].strftime("%a %d %b")) elif dec["decision"] == "bloqueio": t = trimmed(team, current, l) if t: dec["suggestion"] = "encurtar para %s - %s (%d dia(s) útil(eis), de volta antes do bloqueio)" % ( t["start"].strftime("%a %d %b"), t["end"].strftime("%a %d %b"), len(leave_days(team, t))) decisions.append(dec) approved_peak = defaultdict(int) for l in current: approved_peak[l["person"]] += len(set(leave_days(team, l)) & team["peak"]) for dec in decisions: l = dec["leave"] p = team["people"][l["person"]] if dec["decision"] != "approve" and p.get("worked_peak_last_year") and set(leave_days(team, l)) & team["peak"]: lucky = [n for n, k in approved_peak.items() if k and not team["people"][n].get("worked_peak_last_year")] add("medium", "LV05", "%s trabalhou no pico no ano passado mas esta solicitação é %s enquanto %s ficam com dias de pico de folga; " "considere trocar." % (l["person"], dec["decision"], ", ".join(sorted(lucky)) or "ninguém")) g_final = gaps(team, current) allowance = [] for n, p in team["people"].items(): planned_req = sum(dec["charge"] for dec in decisions if dec["leave"]["person"] == n and dec["decision"] == "approve") appr = used[n] - planned_req left = p["allowance"] - p["taken"] - used[n] if p["allowance"] else None allowance.append({"person": n, "allowance": p["allowance"], "taken_before": p["taken"], "approved": appr, "recommended": planned_req, "left": left, "peak_days_off": approved_peak.get(n, 0)}) if p["allowance"] and p["taken"] + appr > p["allowance"] + 1e-9: add("high", "LV02", "A folga aprovada de %s excede o saldo em %g dias (%g + %g usados de %g); " "confirme acúmulo ou licença não paga com o RH." % (n, p["taken"] + appr - p["allowance"], p["taken"], appr, p["allowance"])) cm = p.get("carryover_max") if left is not None and cm is not None and left > float(cm): add("low", "LV06", "%s ainda terá %g dias restantes (limite de acúmulo %g); %g dia(s) são perdidos a menos que reservados." % (n, left, float(cm), left - float(cm))) for role, k in sorted(team["coverage"].items()): if role == "any": continue holders = [n for n, p in team["people"].items() if role in p["roles"]] if len(holders) <= k: add("medium", "LV03", "Apenas %d pessoas podem cobrir %s (mínimo %d): qualquer ausência de %s quebra a cobertura; " "faça treinamento cruzado de um substituto." % (len(holders), role, k, ", ".join(holders))) elif len(holders) == k + 1: add("low", "LV03", "%s tem uma pessoa de reserva (%s para um mínimo de %d); duas ausências ao mesmo tempo quebram." % (role, ", ".join(holders), k)) for day, s in g_final.items(): if day not in g_approved: add("high", "LV01", "%s ainda curto após as decisões: %s" % (day, s)) for dec in decisions: if dec["decision"] in ("conflito", "bloqueio"): l = dec["leave"] add("medium", "LV04", "%s %s-%s: %s (%s)%s" % ( l["person"], l["start"].strftime("%d %b"), l["end"].strftime("%d %b"), dec["decision"], dec["reason"], ("; ofereça " + dec["suggestion"]) if dec["suggestion"] else "; sem alternativa sem lacuna dentro de 15 dias")) elif dec["decision"] == "acima do saldo": l = dec["leave"] add("medium", "LV02", "%s %s-%s: %s; aprove uma viagem mais curta ou concorde dias não pagos" % ( l["person"], l["start"].strftime("%d %b"), l["end"].strftime("%d %b"), dec["reason"])) thin = [] off = off_map(team, current) for day in team["days"]: if not is_workday(team, day): continue working = [n for n, p in team["people"].items() if n not in off.get(day, set()) and person_works(team, p, day)] if len(working) == need_for(team, day).get("any", 0): thin.append(day) if thin: add("info", "LV07", "Sem folga em %d dia(s) (%s): um dia de licença médica quebra a cobertura; alinhe um substituto." % ( len(thin), ", ".join(x.strftime("%d %b") for x in thin))) order = {"high": 0, "medium": 1, "low": 2, "info": 3} findings.sort(key=lambda f: (order[f["severity"]], f["code"])) return {"team": team.get("team", "Equipe"), "period": team["period"], "gaps_approved": g_approved, "gaps_final": g_final, "decisions": decisions, "allowance": allowance, "findings": findings, "current": current, "thin": thin} # ----------------------------------------------------------------------------- relatório def grid(team, leaves_final, all_leaves): days = [x for x in team["days"] if x.weekday() < 5 or DAYS[x.weekday()] in team["workdays"]] final_ids = {l["id"] for l in leaves_final} cell = defaultdict(dict) for l in all_leaves: if l["status"] == "declined": continue for day in leave_days(team, l): if l["id"] in final_ids: c = TYPE_CODE[l["type"]] if l["status"] == "approved" else TYPE_CODE[l["type"]].lower() else: c = "x" if l["half"]: c = "h" if c != "x" else "x" cell[l["person"]][day] = c width = max([len(n) for n in team["people"]] + [len("cover " + r) for r in team["coverage"]]) + 2 months = " " * width + "".join(x.strftime("%b")[0] if i == 0 or x.month != days[i - 1].month else " " for i, x in enumerate(days)) L = [months, " " * width + "".join(str(x.day // 10) for x in days), " " * width + "".join(str(x.day % 10) for x in days), " " * width + "".join(DAYS[x.weekday()][0].upper() for x in days)] for n, p in team["people"].items(): row = [] for x in days: if x in team["holidays"]: row.append("H") elif not person_works(team, p, x): row.append("-") else: row.append(cell[n].get(x, ".")) L.append(n.ljust(width) + "".join(row)) off = off_map(team, leaves_final) for role in sorted(team["coverage"]): row = [] for x in days: if not is_workday(team, x): row.append(" ") continue s = shortfall(team, x, off.get(x, set())) row.append("!" if role in s else "+") L.append(("cover " + role).ljust(width) + "".join(row)) return L def report(team, leaves, r): L = ["# Plano de cobertura de folgas: %s (%s)" % (r["team"], r["period"].replace("..", " a ")), ""] need = ", ".join("%s %d" % (k, v) for k, v in sorted(team["coverage"].items())) L.append("Cobertura mínima por dia útil: %s. Feriados (fechado): %s." % ( need, ", ".join(x.strftime("%d %b") for x in sorted(team["holidays"])) or "nenhum")) n_appr = sum(1 for dec in r["decisions"] if dec["decision"] == "approve") verdict = ("folgas aprovadas já quebram a cobertura" if r["gaps_approved"] else "acima do saldo em folgas aprovadas" if any(f["code"] == "LV02" and f["severity"] == "high" for f in r["findings"]) else "coberto") L += ["", "**Veredito: %s** - %d de %d solicitações em aberto podem ser aprovadas sem lacuna; %d precisam de uma decisão." % ( verdict, n_appr, len(r["decisions"]), len(r["decisions"]) - n_appr), ""] L += ["## Solicitações (em ordem de decisão: quem trabalhou no pico no ano passado primeiro, depois por ordem de chegada)", "", "| # | Pessoa | Datas | Dias úteis | Tipo | Enviada | Decisão | Porquê | Alternativa |", "|---|---|---|---|---|---|---|---|---|"] for i, dec in enumerate(r["decisions"], 1): l = dec["leave"] L.append("| %d | %s | %s - %s%s | %s | %s | %s | %s | %s | %s |" % ( i, l["person"], l["start"].strftime("%a %d %b"), l["end"].strftime("%a %d %b"), " (meio)" if l["half"] else "", ("%g" % (0.5 * dec["workdays"])) if l["half"] else dec["workdays"], l["type"], l["submitted"][:10], dec["decision"], dec["reason"] or "-", dec["suggestion"] or "-")) L += ["", "## Calendário (aprovado + recomendado)", "", "```"] + grid(team, r["current"], leaves) + [ "```", "V/T/S/P/U/O férias/treinamento/licença médica/parental/não paga/outro aprovado, minúsculo = solicitação recomendada " "para aprovação, h = meio dia, x = solicitação não aprovada, H = feriado, - = não é dia útil, " "+ / ! = função coberta / curta.", ""] L += ["## Saldo (dias)", "", "| Pessoa | Saldo | Usado antes | Aprovado aqui | Recomendado | Restante | Dias de pico de folga |", "|---|---|---|---|---|---|---|"] for a in r["allowance"]: L.append("| %s | %g | %g | %g | %g | %s | %d |" % (a["person"], a["allowance"], a["taken_before"], a["approved"], a["recommended"], "-" if a["left"] is None else "%g" % a["left"], a["peak_days_off"])) L += ["", "## Descobertas", ""] if not r["findings"]: L.append("- nenhuma") for f in r["findings"]: L.append("- **%s %s** %s" % (f["severity"].upper(), f["code"], f["message"])) return "\n".join(L) + "\n" # ----------------------------------------------------------------------------- auto-teste def self_test(): import os import tempfile cases = [] def ok(name, cond, detail=""): cases.append((name, bool(cond), detail)) base = {"team": "T", "period": "2026-12-14..2026-12-25", "holidays": ["2026-12-25"], "peak_days": "2026-12-21..2026-12-24", "coverage": {"lead": 1, "dev": 1, "any": 2}, "people": [{"name": "A", "roles": ["lead", "dev"], "allowance": 10, "taken": 0}, {"name": "B", "roles": ["dev"], "allowance": 10, "taken": 9}, {"name": "C", "roles": ["lead"], "allowance": 10, "taken": 0, "worked_peak_last_year": True}, {"name": "D", "roles": ["dev"], "days": ["mon", "tue"], "allowance": 5, "taken": 0}]} t = load_team(base) ok("fins de semana e feriados não são dias úteis", not is_workday(t, d("2026-12-19")) and not is_workday(t, d("2026-12-25"))) sf = shortfall(t, d("2026-12-16"), {"B", "C", "D"}) ok("uma pessoa preenche uma vaga (A não pode ser líder e dev)", sf.get("any") == 1 and sf.get("lead", 0) + sf.get("dev", 0) == 1, sf) ok("emparelhamento encontra A=dev, C=lead", shortfall(t, d("2026-12-16"), {"B", "D"}) == {}) ok("funcionário de meio período só conta em seus próprios dias", shortfall(t, d("2026-12-17"), {"A", "B"}) == {"dev": 1, "any": 1}, shortfall(t, d("2026-12-17"), {"A", "B"})) lv = {"id": 1, "person": "D", "start": d("2026-12-14"), "end": d("2026-12-20"), "type": "vacation", "status": "requested", "submitted": "2026-11-01", "half": False, "notes": ""} ok("dias de folga pulam dias não úteis", len(leave_days(t, lv)) == 2) ok("meio dia cobra 0.5", charge(t, dict(lv, half=True)) == 1.0) d0 = tempfile.mkdtemp() csvp = os.path.join(d0, "l.csv") with open(csvp, "w") as f: f.write("person,start,end,type,status,submitted,half_day,notes\n" "A,2026-12-21,2026-12-22,vacation,approved,2026-10-01,,\n" "C,2026-12-21,2026-12-21,vacation,requested,2026-11-10,,\n" "B,2026-12-14,2026-12-16,vacation,requested,2026-10-05,,\n" "D,2026-12-15,2026-12-15,training,requested,2026-11-01,,\n") leaves = load_leave(csvp, t) r = plan(t, leaves) by = {(dec["leave"]["person"]): dec for dec in r["decisions"]} ok("C conflita com a folga aprovada de A (sem líder)", by["C"]["decision"] == "conflito", by["C"]) ok("conflito recebe uma data alternativa", by["C"]["suggestion"] is not None, by["C"]) ok("B acima do saldo (3 dias, 1 restante)", by["B"]["decision"] == "acima do saldo", by["B"]) ok("treinamento de D aprovado", by["D"]["decision"] == "approve", by["D"]) ok("pessoa que trabalhou no pico decidida primeiro", r["decisions"][0]["leave"]["person"] == "C", [x["leave"]["person"] for x in r["decisions"]]) codes = {f["code"] for f in r["findings"]} ok("LV03 ponto único de falha para líder", "LV03" in codes, codes) ok("LV05 sinalizador de justiça para C", "LV05" in codes, codes) ok("sem lacunas de folgas aprovadas", not r["gaps_approved"]) with open(csvp, "a") as f: f.write("C,2026-12-21,2026-12-21,sick,approved,2026-12-01,,\n") r2 = plan(t, load_leave(csvp, t)) ok("LV01 lacuna em folgas aprovadas", any(f["code"] == "LV01" and f["severity"] == "high" for f in r2["findings"])) t2 = load_team(dict(base, blackout=[{"dates": "2026-12-16", "reason": "lançamento"}])) r3 = plan(t2, load_leave(csvp, t2)) ok("bloqueio precede cobertura", any(x["decision"] == "bloqueio" for x in r3["decisions"]) or all(x["leave"]["person"] != "B" or x["decision"] == "acima do saldo" for x in r3["decisions"])) lvb = dict(lv, person="A", start=d("2026-12-16"), end=d("2026-12-16")) ok("blackout_hit detecta sobreposição", blackout_hit(t2, lvb)[0] == [d("2026-12-16")]) try: load_team(dict(base, coverage={"qa": 1})) ok("função de cobertura desconhecida rejeitada", False) except InputError: ok("função de cobertura desconhecida rejeitada", True) rep = report(t, leaves, r) ok("relatório tem grade e veredito", "Veredito" in rep and "cover lead" in rep) for name, good, detail in cases: print("%s %s%s" % ("PASS" if good else "FAIL", name, "" if good else " -> %r" % (detail,))) passed = sum(1 for _, g, _ in cases if g) print("%d/%d passaram" % (passed, len(cases))) return 0 if passed == len(cases) else 1 def to_json(r): def conv(o): if isinstance(o, dt.date): return o.isoformat() if isinstance(o, dict): return {(k.isoformat() if isinstance(k, dt.date) else k): conv(v) for k, v in o.items()} if isinstance(o, (list, tuple)): return [conv(x) for x in o] return o return json.dumps(conv(r), indent=2) def main(argv=None): ap = argparse.ArgumentParser(description="Planeje as folgas da equipe contra a cobertura mínima.") ap.add_argument("team", nargs="?") ap.add_argument("leave", nargs="?") ap.add_argument("--json", action="store_true") ap.add_argument("--self-test", action="store_true") a = ap.parse_args(argv) if a.self_test: return self_test() if not (a.team and a.leave): ap.print_usage(sys.stderr) return 2 try: with open(a.team, encoding="utf-8") as f: team = load_team(json.load(f)) leaves = load_leave(a.leave, team) r = plan(team, leaves) except (InputError, OSError, json.JSONDecodeError, KeyError) as e: print("erro: %s" % e, file=sys.stderr) return 2 print(to_json(r) if a.json else report(team, leaves, r), end="\n" if a.json else "") bad = r["gaps_approved"] or any(f["code"] == "LV02" and f["severity"] == "high" for f in r["findings"]) return 1 if bad else 0 if __name__ == "__main__": sys.exit(main()) ```

Entre para ver o prompt completo

Continuar com:

Ao entrar, você concorda com nossos Termos de uso e Política de privacidade

Uso

Este prompt foi projetado para uso com business. Copie o conteúdo acima e cole na sua ferramenta de IA preferida.

Para melhores resultados, personalize os marcadores (indicados por colchetes ou maiúsculas) com seus requisitos específicos.

Referências

Categorias:business| prompts.chat| leave-planning| coverage-scheduling

Discussão

0 comentários