Receita para programar: curiosidade e uma pitada de Google
No Jestor, lógicas bem desenhadas e códigos simples já resolvem problemas complexos.
No último texto, contei um pouco da minha jornada montando um aplicativo simples de transferências contábeis dentro do Jestor.
A experiência, para alguém que veio do mundo de Excel e VBA, foi relativamente tranquila. Afinal, a solução utilizava conceitos bem básicos de programação, onde trabalhei basicamente com a coleta de informações simples e criação de registros igualmente simples.
Se eu desejasse criar um fluxograma do processo, nada mais seria do que coletar um input de dados, e gerar dois outputs com essas mesmas informações coletadas. Ou seja: é um processo autocontido, onde o resto do sistema não influencia nas informações que estão sendo manipuladas.
Esse tipo de automatização já resolve uma série de problemas de negócio, mas comecei a me perguntar se conseguiria abordar situações mais complexas, em que fossem necessárias validações e buscas pelo sistema,sem necessitar da ajuda do time técnico. Apenas eu, minha curiosidade e um pouco de Google.
Nesse vídeo eu mostro um pouco dos passos para criar a solução:
Fome: Resolver um problema real
Assim como da última vez, quis resolver algum tipo de problema real. Algo que consigo ver como uma necessidade minha ou de uma empresa real, e não apenas uma situação hipotética sem valor prático.
Pensei aqui em uma situação de venda comum em serviços:
- Time comercial realiza a venda de serviços. No caso, agendamentos de consultoria;
- Um time separado deve marcar esses agendamentos em horários disponíveis dos consultores.
Dessas duas regras simples, fui desmembrando os possíveis problemas/situações que precisam estar contemplados na solução para ser útil:
- O time comercial pode vender mais de um agendamento de consultoria e tipos diferentes de consultoria na mesma venda;
- Tipos diferentes de consultoria podem ter durações diferentes;
- As consultas a serem agendadas devem aparecer de forma fácil ao time de agendamento;
- O sistema deve travar overlap de consultas.
Apesar de não parecer muito complexo, as regras (1) e (4) adicionam uma dificuldade considerável na solução. Por quê? Porque começam a trabalhar com pesquisas que podem trazer mais de um resultado e regras de comparação não tão óbvias.
Abaixo, explico um pouco de como resolvi esses problemas.
Escrevendo a Receita: Criando a estrutura
O que aprendi na primeira solução que criei foi que, antes de se aventurar no código, o passo mais importante é imaginar o processo e montar a estrutura de informações que esse processo necessita.
No caso, entendi que precisaria apenas de duas criações/modificações no sistema:
- Mudar o cadastro de Serviços para conter uma duração em minutos do mesmo;
- Criar um cadastro de Agendamentos que tivesse os seguintes pontos:
- Descrição;
- Serviço;
- Cliente;
- Duração;
- Consultor;
- Data da consulta;
- Status.
Tudo isso pode ser realizado diretamente na interface, então primeiramente criei o campo de duração da consulta no módulo de Serviços, aproveitando para registrar alguns serviços de teste:
Depois disso, criei um módulo novo de Agendamentos clicando em Criar novo cadastro, já configurando os campos necessários:
Em termos de estrutura, isso era realmente tudo o que eu precisava para começar a trabalhar.
Potes, panelas e talheres: Entendendo o sistema
Antes de começar o código, faltava entender um ponto do sistema: onde ficam salvos os serviços atrelados a uma venda?
Essa dúvida surgiu porque o módulo de Vendas é uma parte um pouco diferente do sistema. No fundo, ela nada mais é do que um tipo de cadastro, com campos próprios. No entanto, ela foi modificada de maneira que sua visualização é feita através de um kanban (ou fluxo, como é chamada no sistema):
Além disso, a criação de venda é feita através de um modal personalizado, no qual é possível adicionar mais de um tipo de produto/serviço em quantidades diferentes:
De início, imaginei que os itens da venda estariam em algum tipo de campo personalizado, mas ao entrar na visualização de uma venda fechada no sistema, percebi que era apenas uma reflexão dos campos normais do card:
No entanto, ao examinar um pouco mais de perto toda a área de visualização, percebi que existia um módulo que referenciava a venda em questão, com lançamentos de serviços individuais e quantidade:
Clicando em Ver lista completa e filtros, descobri que o módulo em questão era referenciado no sistema como servico_venda:
Ou seja: a maneira que o sistema atribui diversos produtos ou serviços no sistema é criando lançamentos num módulo separado atrelando quantidade, tipo e venda.
Apesar de parecer complexo, isso tornou a lógica bem mais fácil de trabalhar do que se fosse algum tipo de personalização a nível de código, já que permite que eu trabalhe com esses registros da maneira que eu trabalharia com qualquer outro tipo de cadastro no sistema.
Misturando os ingredientes: Começando o desenvolvimento
Com todas as informações reunidas, chegou a hora de desenvolver de fato as automatizações que eu defini no início.
A primeira delas, em linhas gerais, seria:
- Ao fechar uma venda, gerar agendamentos para todos os serviços contidos dentro dessa venda.
Para iniciar, desenvolvi uma versão mais fácil apenas para validar a criação com as informações corretas (cliente, serviço contratado, etc). Essa simplificação foi trabalhar com apenas 1 serviço na venda.
O processo lógico dessa automatização seria:
- Ao editar ou criar uma Venda, detectar se a venda foi fechada;
- Procurar em servico_venda se existe algum registro que atrele a venda em questão a algum serviço;
- Coletar as informações da venda e do serviço;
- Criar o agendamento.
Como isso é bem similar à solução de transferências contábeis que criei anteriormente, foi bem fácil transpor isso para um código:
E testando a solução com o fechamento de uma Venda, a automatização funcionou da maneira que deveria:
Validado o primeiro conceito, chegou a hora de expandir o código para contemplar vendas que possuam serviços múltiplos.
Azedando o caldo: Primeiro problema
Logo de cara, meu primeiro impulso foi retirar aquele método first() na busca realizada em servico_venda. Afinal, se eu não limitasse o resultado ao primeiro item encontrado, certamente o resultado seriam todos os itens encontrados, certo?
Sim, mas isso não resolveu o meu problema. A variável $servicos me retornou alguma coisa, mas eu não sabia muito bem o que era, nem como trabalhar com ela. Primeiro tentei validar o que exatamente era esse retorno, e descobri que uma maneira rápida de descobrir o que uma variável contém é parar o processo com um Jestor.error e passar a variável que quero analisar, algo como a linha abaixo:
Ao tentar fechar uma venda com dois serviços diferentes, fui contemplado com esse erro:
O que isso quer dizer?
Como a variável tinha vários valores diferentes (a busca de loadData sempre traz um array), o sistema só me mostrou uma mensagem genérica de erro. Isso não me ajudou muito, então o próximo passo foi descobrir o tamanho do array para entender o que poderia ser.
Um pouco de Google me levou a uma função específica chamada count():
Incorporando isso no meu código, ao invés de passar a variável inteira no Jestor.error, passei o seu tamanho utilizando count($servicos). Isso me levou à tela de erro abaixo:
Ou seja, a variável $servicos era, de fato, um array com dois itens. Achei que havia solucionado o problema: bastaria passar o que estava contido em cada posição do array para variáveis separadas. Algo como:
$servico1 = $servicos[0];
No entanto, essa lógica não deu certo.
Na verdade, depois de muito tempo pesquisando sobre arrays e fazendo alguns testes, ainda não descobri como passar essa informação. Talvez um programador conseguisse me passar de forma fácil como isso seria feito mas, como o propósito era tentar sozinho, acabei optando por achar alguma solução alternativa.
Consertando a receita: Loops
A solução foi utilizar uma ferramenta que já tinha utilizado algumas vezes em minhas macros de VBA,loops lógicos como for e while.
Meu raciocínio para isso foi o seguinte: se eu não conseguia trabalhar com uma massa de dados ao mesmo tempo, o que eu faria seria trabalhar com uma informação de qualquer vez. Ou seja, buscando iterativamente cada lançamento e tratando as informações.
Aqui, o que me ajudou muito a pensar nessa solução foi a documentação de desenvolvimento do Jestor. Lendo um pouco do método loadData, descobri que era possível também passar alguns critérios como limitar o número de resultados (limit), e escolher quantos resultados esconder (offset).
Simplificando, com um limit de 1 e um offset de 1, isso me traria o segundo resultado da pesquisa.
A lógica que montei então foi:
- Enquanto (while) a resultado da busca não for vazio, realizar o processo de pegar um registro de servico_venda atrelado a venda em questão e criar os agendamentos, aumentando o offset para cada trazer o próximo resultado da busca;
- Criar um agendamento para cada (for) item do serviço na venda, ou seja, sua quantidade.
O resultado final foi algo assim:
Sucesso! ? Para uma venda com duas Consultorias Smart e uma Consultoria Pro, os agendamentos foram criados corretamente.
Recheio: Agendamentos sem overlap
O segundo passo era criar uma forma de agendar as consultas para consultores garantindo que não há overlap de agenda. Assim, comecei a pensar: quais as regras que eu poderia criar para validar que um horário está em uso?
Afinal, não poderia apenas conferir se duas consultas tem a data de início na mesma data e hora. Nesse critério, seria válido marcar uma consulta às 19h45 e outra às 19h46, mas isso claramente está errado.
Depois de um tempo desenhando o processo, cheguei nas seguintes regras:
- Ao agendar uma consulta para um consultor, varrer todas as consultas que o mesmo possui;
- Fazer a diferença entre o horário das consultas já marcadas e o horário que se quer agendar;
- Caso a diferença seja negativa (ou seja, o horário que eu quero agendar está no futuro em relação à consulta já marcada) e menor do que o tempo de duração da consulta que quero agendar, barrar o agendamento;
- Caso a diferença seja positiva (ou seja, o horário que eu quero agendar está no passado em relação à consulta já marcada) e menor do que o tempo de duração da consulta que já foi agendada, barrar o agendamento.
É uma lógica muito simples e que talvez não seja uma boa maneira de desenhar o código (com milhares de consultas, talvez o agendamento fique lento devido ao número de comparações), mas é funcional.
Como a ideia era criar uma solução válida e não com as melhores práticas de desenvolvimento, segui em frente com esses critérios.
Tempo do forno: Datas e fusos
Tive dois grandes problemas aqui:
- O Jestor trata datas de input ou seja, como elas vêm ao salvar o registro, e datas já salvas no sistema de formas diferentes. Por exemplo, o último tipo já está tratado e convertido para um texto;
- Minhas datas, por algum motivo, estavam sempre erradas (descobri jogando a variável no Jestor.error, como fiz anteriormente para validar outras variáveis).
Sobre o primeiro ponto, a maneira que resolvi o problema foi mudando um pouco os gatilhos da automatização: agora, o código rodaria após salvar o registro, e não antes. Isso garantiria que as datas estivessem sempre no mesmo formato.
O que fiz depois foi utilizar a função strtotime (novamente, pesquisando coisas simples no Google como “php convert string to date”) para transformar essas datas em valores numéricos que pudessem ser subtraídos uns dos outros. Assim, retirar uma data de outra me daria um resultado em segundos.
O segundo problema foi algo que talvez um programador já soubesse de imediato que iria acontecer: minhas datas convertidas sempre vinham com o valor errado.
Por quê? Notei que era sempre uma diferença de três horas em relação ao horário correto. Por acaso, sei que o fuso horário de São Paulo é de -3h, então suspeitei que o problema fosse algo nesse sentido. No entanto, mesmo uma pesquisa genérica de motivos para um strtotime resultar em valores errados me apontaria nessa direção: aparentemente, esse é um problema extremamente comum em que o horário do servidor pode não coincidir com o horário do seu computador.
A solução foi adicionar alguns parâmetros na função strtotime para corrigir o fuso horário. O código final, depois disso, foi comparar o resultado das buscas dentro das regras que defini anteriormente.
Realizar o teste para todas situações necessárias me deu o resultado esperado. Horários inválidos eram bloqueados, e o agendamento cancelado:
É só servir: Finalizando
Por fim, criei apenas um painel para visualizar as novas consultas a serem agendadas, para que, a medida que o time comercial fechasse vendas, o time de agendamento tivesse de forma fácil as informações consolidadas e pudessem agir:
Com esse toque final, o projeto pessoal chegou ao fim. Apesar de ter falhado diversas vezes e ter visto a janela de erro mais vezes do que gostaria, fico com uma sensação de conquista de ter conseguido criar, de maneira fácil, algo que considero uma feature relativamente complexa e totalmente funcional para resolver um problema de negócio entre áreas.
Mesmo com pouco conhecimento de programação, consegui criar uma solução que não apenas trabalhava com as informações do próprio processo, mas interagia com partes diferentes do sistema para validar se o processo era válido ou não. E, no fim, saio pensando em quão difícil realmente seria trabalhar com informações fora do sistema (como um Google Forms ou algo do tipo).
Mas isso fica para os próximos experimentos :)
Lógicas bem desenhadas e códigos simples já resolvem problemas complexos.