O problema
O cliente liga a perguntar como vai a obra, e quem atende não sabe dizer sem ligar ao encarregado. Depois manda-se uma foto pelo WhatsApp e fica-se sem registo de nada. O que falta não é uma aplicação bonita: é um sítio onde o estado da obra existe, e uma maneira de o mostrar ao cliente sem lhe mostrar as margens.
As fases saem da medição
Quando um orçamento é adjudicado, a obra nasce dele: uma fase por especialidade que o orçamento tem, e o peso de cada fase é a fatia do custo directo que essa especialidade vale. Uma obra em que a pintura pesa 14% pesa isso porque a medição diz que pesa. A percentagem executada é a soma dos pesos vezes o que está feito em cada fase, e por isso fechar quatro tarefas de pintura mexe menos o ponteiro do que fechar uma de estrutura.
-- O peso de cada fase e a fatia do custo directo que a
-- especialidade vale na medicao. Ninguem o escreve a mao.
for v_cat in
select l.category,
sum(round(l.qty * l.material_cents)
+ round(l.qty * l.labour_hours * l.hourly_cents)
+ round(l.qty * l.equipment_cents)) as cents
from obra_quote_lines l
where l.quote_id = p_quote
group by l.category
having sum(...) > 0
order by min(l.position)
loop
insert into obra_phases (project_id, name, ord, weight_pct, budget_cents)
values (v_p.id, v_cat.category, v_ord,
round(100.0 * v_cat.cents / v_total, 2), v_cat.cents);
end loop;O portal não esconde: projecta
obra_tasks e obra_updates têm RLS ligada e nenhuma política de leitura. Não há select que as devolva, venha de onde vier, incluindo da API pública. A única porta é uma função security definer que devolve um JSON com fases, percentagem e as notas marcadas como visíveis. Se alguém abrir as ferramentas do browser no portal, não encontra colunas escondidas: encontra o que a função devolveu, e mais nada existe do lado de lá.
-- Sem politica de leitura, estas tabelas nao respondem a ninguem.
alter table obra_tasks enable row level security;
alter table obra_updates enable row level security;
-- A unica porta do cliente: uma projeccao, nao um select filtrado.
create function obra_project_public(p_room text, p_code text)
returns jsonb security definer stable as $$
select jsonb_build_object(
'code', v_p.code, 'site', v_p.site,
'progress_pct', (select round(sum(weighted), 1) from obra_progress(p_room)
where project_id = v_p.id),
'phases', (select jsonb_agg(...) from obra_phases where project_id = v_p.id),
'updates', (select jsonb_agg(...) from obra_updates
where project_id = v_p.id and client_visible));
$$;As regras estão na base de dados
Uma fase não fecha com tarefas por fechar, e a verificação está dentro da função, não no botão. Uma tarefa bloqueada não está feita: marcar como feita apaga o motivo do bloqueio, porque as duas coisas ao mesmo tempo são uma contradição que depois ninguém sabe ler.
-- Uma fase nao fecha com tarefas por fechar.
if p_status = 'concluida' and exists (
select 1 from obra_tasks where phase_id = p_phase and done_at is null) then
raise exception 'ha tarefas por fechar';
end if;
-- Feita e bloqueada ao mesmo tempo e uma contradicao: marcar feita
-- apaga o motivo do bloqueio.
update obra_tasks
set done_at = case when p_done then coalesce(done_at, now()) else null end,
blocked_reason = case when p_done then null else nullif(btrim(p_blocked), '') end
where id = p_task and room_code = p_room;As tabelas
Quatro tabelas. Duas respondem a qualquer leitura; as outras duas não respondem a ninguém de fora, e é essa a diferença entre um portal e um ecrã com colunas escondidas.
| Tabela | Para que serve |
|---|---|
| obra_projects | A obra, com o orçamento do momento da adjudicação guardado. |
| obra_phases | Uma por especialidade do orçamento, com peso e orçamento próprios. |
| obra_tasks | RLS ligada, sem política de leitura: só as funções respondem. |
| obra_updates | O diário. Cada registo traz client_visible, e é essa coluna que o portal filtra. |
O que faria diferente num cliente real
Fotografias a sério, com armazenamento e miniaturas, em vez de um tipo de registo chamado foto. Notificação ao cliente quando há novidade, por email ou SMS, em vez de ele ter de ir ver. Autos de medição para facturação por fases, que é como uma obra se paga. Datas planeadas por fase com um Gantt simples. E autenticação, com o cliente a entrar por um link só dele em vez de um código que se adivinha.