Plugin de agendamento para manutenção hidráulica preventiva em prédios: guia prático

Como desenvolver plugin WordPress para agendamento de manutenção hidráulica preventiva em condomínios. Arquitetura, hooks, CPTs e integração com calendário.

O síndico do Edifício Vitória, na Zona Sul de São Paulo, recebeu a terceira reclamação no mês sobre vazamento na coluna principal. A causa: uma junta desgastada que uma inspeção trimestral teria detectado. O problema não era falta de verba, mas a ausência de um sistema que garantisse a execução no prazo — planilhas perdidas, e-mails ignorados, prestadores sem acesso ao histórico. Esse cenário se repete em centenas de condomínios e é exatamente o gatilho para construir um plugin de agendamento robusto, nativo no WordPress, que transforme a manutenção hidráulica preventiva em processo rastreável e auditável.

Desenvolver essa solução exige mais do que criar um calendário bonito. Precisamos de arquitetura de dados que suporte recorrências complexas, validação de conflitos em tempo real, notificações assíncronas e uma API extensível para integrações futuras com ERPs de facilities ou aplicativos de moradores. Neste guia, percorremos as decisões técnicas essenciais, do modelagem de Custom Post Types à publicação de endpoints REST, com código pronto para adaptar.

Modelagem de dados: CPT, taxonomias e meta fields

Comece registrando o Custom Post Type plano_manutencao com show_in_rest => true e capacidades dedicadas (manage_maintenance_plans, view_maintenance_calendar). Isso isola permissões e evita colisão com posts normais. Registre duas taxonomias hierárquicas: janela_horario (manhã, tarde, noite, madrugada) e tipo_servico (preventiva, corretiva, inspeção, emergencial). O gancho está em init com prioridade 0 para garantir disponibilidade antes de outros plugins.

add_action('init', function() {
    register_post_type('plano_manutencao', [
        'labels' => [
            'name' => 'Planos de Manutenção',
            'singular_name' => 'Plano de Manutenção',
        ],
        'public' => true,
        'show_in_rest' => true,
        'capability_type' => 'maintenance_plan',
        'map_meta_cap' => true,
        'supports' => ['title', 'editor', 'custom-fields', 'revisions'],
        'menu_icon' => 'dashicons-wrench',
    ]);

    register_taxonomy('janela_horario', 'plano_manutencao', [
        'labels' => ['name' => 'Janelas de Horário'],
        'hierarchical' => true,
        'show_in_rest' => true,
    ]);

    register_taxonomy('tipo_servico', 'plano_manutencao', [
        'labels' => ['name' => 'Tipos de Serviço'],
        'hierarchical' => true,
        'show_in_rest' => true,
    ]);
}, 0);

Para recorrência, evite serializar arrays em um único meta. Crie meta fields atômicos: _recurrence_freq (daily, weekly, monthly, quarterly, yearly), _recurrence_interval (inteiro), _recurrence_byday (MO,TU,WE… para weekly), _recurrence_bymonthday (1-31 para monthly), _next_occurrence (datetime UTC indexável). Essa granularidade permite queries performáticas via meta_query em _next_occurrence e _status (agendado, concluído, cancelado, atrasado).

Validação de conflitos e janelas permitidas

A regra de negócio: nenhum agendamento pode cair fora da janela de horário do bloco/torre nem sobrepor outro serviço do mesmo tipo no mesmo dia. No meta box do editor (Gutenberg ou clássico), use wp.apiFetch para consultar endpoint customizado /wp-json/meu-plugin/v1/disponibilidade?bloco=A&data=2024-07-15&janela=manha. O endpoint faz WP_Query com meta_query em _next_occurrence e tax_query em janela_horario, retornando slots livres.

No back-end, valide novamente no hook save_post_plano_manutencao:

add_action('save_post_plano_manutencao', function($post_id) {
    if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) return;

    $janela = wp_get_post_terms($post_id, 'janela_horario', ['fields' => 'slugs'])[0] ?? '';
    $data_inicio = get_post_meta($post_id, '_next_occurrence', true);
    $bloco = get_post_meta($post_id, '_bloco_torre', true);

    $conflito = new WP_Query([
        'post_type' => 'plano_manutencao',
        'post_status' => ['publish', 'future'],
        'post__not_in' => [$post_id],
        'meta_query' => [
            ['key' => '_next_occurrence', 'value' => $data_inicio, 'compare' => '='],
            ['key' => '_bloco_torre', 'value' => $bloco, 'compare' => '='],
        ],
        'tax_query' => [
            ['taxonomy' => 'janela_horario', 'field' => 'slug', 'terms' => $janela],
        ],
        'fields' => 'ids',
        'no_found_rows' => true,
    ]);

    if ($conflito->have_posts()) {
        wp_die('Já existe agendamento para este bloco, data e janela.', 'Conflito de agenda', ['response' => 409]);
    }
});

Essa validação dupla (front + back) elimina race conditions. Para condomínios grandes, considere criar tabela customizada wp_maintenance_slots com índice composto (bloco, data, janela) e usar $wpdb para lock otimista.

Notificações assíncronas com Action Scheduler

Nunca envie e-mails ou dispare webhooks no save_post — isso trava a resposta e falha silenciosamente se o provedor de e-mail demorar. Instale a biblioteca Action Scheduler (já inclusa no WooCommerce ou disponível via Composer woocommerce/action-scheduler) e agende a notificação:

add_action('save_post_plano_manutencao', function($post_id) {
    if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) return;

    as_enqueue_async_action('notificar_agendamento_manutencao', [
        'post_id' => $post_id,
        'tipo' => 'criado',
    ], 'ASAP');
});

add_action('notificar_agendamento_manutencao', function($args) {
    $post = get_post($args['post_id']);
    if (!$post) return;

    $destinatarios = get_option('manutencao_notificacao_emails', []);
    $assunto = "Novo agendamento: {$post->post_title}";
    $corpo = "Agendado para " . get_post_meta($post->ID, '_next_occurrence', true) . " no bloco " . get_post_meta($post->ID, '_bloco_torre', true);

    foreach ($destinatarios as $email) {
        wp_mail($email, $assunto, $corpo);
    }

    // Integração WhatsApp via webhook externo
    $webhook_url = get_option('manutencao_whatsapp_webhook');
    if ($webhook_url) {
        wp_remote_post($webhook_url, [
            'body' => json_encode(['mensagem' => $corpo, 'telefone' => get_option('manutencao_whatsapp_grupo')]),
            'headers' => ['Content-Type' => 'application/json'],
            'timeout' => 5,
        ]);
    }
});

Agende também lembretes automáticos: 24h antes (+23 hours) e 2h antes (+22 hours) usando as_schedule_recurring_action ou duas actions únicas calculadas no momento do agendamento. Isso garante idempotência — se o agendamento for alterado, cancele as actions antigas (as_unschedule_all_actions) e crie novas.

Endpoint REST para consulta de disponibilidade

Exponha um endpoint leve para o front-end consumir sem autenticação pesada (use permission_callback com nonce ou Application Password para painéis administrativos).

add_action('rest_api_init', function() {
    register_rest_route('meu-plugin/v1', '/disponibilidade', [
        'methods' => 'GET',
        'callback' => function(WP_REST_Request $request) {
            $bloco = sanitize_text_field($request->get_param('bloco'));
            $data = sanitize_text_field($request->get_param('data'));
            $janela = sanitize_text_field($request->get_param('janela'));

            if (!$bloco || !$data || !$janela) {
                return new WP_Error('parametros_faltando', 'bloco, data e janela são obrigatórios', ['status' => 400]);
            }

            $ocupados = get_posts([
                'post_type' => 'plano_manutencao',
                'post_status' => ['publish', 'future'],
                'meta_query' => [
                    ['key' => '_next_occurrence', 'value' => $data, 'compare' => '='],
                    ['key' => '_bloco_torre', 'value' => $bloco, 'compare' => '='],
                ],
                'tax_query' => [
                    ['taxonomy' => 'janela_horario', 'field' => 'slug', 'terms' => $janela],
                ],
                'fields' => 'ids',
                'no_found_rows' => true,
            ]);

            $slots_totais = (int) get_option('manutencao_slots_por_janela', 3);
            $livres = max(0, $slots_totais - count($ocupados));

            return rest_ensure_response([
                'bloco' => $bloco,
                'data' => $data,
                'janela' => $janela,
                'slots_livres' => $livres,
                'slots_totais' => $slots_totais,
            ]);
        },
        'permission_callback' => '__return_true', // ou validação de nonce
    ]);
});

Com esse endpoint, o bloco Gutenberg ou painel React pode mostrar disponibilidade em tempo real ao usuário selecionar a data. Adicione cache de 30 segundos via set_transient chaveado por bloco_data_janela para aliviar carga em horários de pico.

Migrações de banco e versionamento

Plug-ins que criam tabelas ou alteram schema precisam de migrações versionadas. Crie a opção meu_plugin_db_version e, no hook de ativação/atualização, execute apenas o delta necessário:

register_activation_hook(__FILE__, 'meu_plugin_maybe_migrate');
add_action('admin_init', 'meu_plugin_maybe_migrate');

function meu_plugin_maybe_migrate() {
    $versao_atual = get_option('meu_plugin_db_version', 0);
    $versao_alvo = 3;

    if ($versao_atual >= $versao_alvo) return;

    global $wpdb;
    $charset_collate = $wpdb->get_charset_collate();

    if ($versao_atual < 1) {
        $sql = "CREATE TABLE {$wpdb->prefix}maintenance_slots (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            bloco varchar(50) NOT NULL,
            data date NOT NULL,
            janela varchar(20) NOT NULL,
            slots_totais tinyint(3) unsigned NOT NULL DEFAULT 3,
            slots_usados tinyint(3) unsigned NOT NULL DEFAULT 0,
            PRIMARY KEY (id),
            UNIQUE KEY bloco_data_janela (bloco, data, janela)
        ) $charset_collate;";
        dbDelta($sql);
        update_option('meu_plugin_db_version', 1);
    }

    if ($versao_atual < 2) {
        $wpdb->query("ALTER TABLE {$wpdb->prefix}maintenance_slots ADD INDEX idx_data (data);");
        update_option('meu_plugin_db_version', 2);
    }

    if ($versao_atual < 3) {
        $wpdb->query("ALTER TABLE {$wpdb->prefix}maintenance_slots ADD COLUMN updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;");
        update_option('meu_plugin_db_version', 3);
    }
}

Teste cada migração em staging com wp eval-file migration-test.php antes do deploy. Mantenha scripts de rollback no repositório para emergências.

Extensibilidade: hooks públicos para integrações

Um plugin profissional expõe actions e filters documentados para que outros desenvolvedores estendam sem forks. Publique no cabeçalho do plugin e no readme.txt:

  • manutencao_antes_salvar_plano (action) — recebe $post_id, $data; ideal para validações customizadas.
  • manutencao_depois_salvar_plano (action) — recebe $post_id; gatilho para ERPs externos.
  • manutencao_slots_por_janela (filter) — retorna inteiro; permite ajustar capacidade por bloco via outro plugin.
  • manutencao_notificacao_canais (filter) — array de canais (email, whatsapp, push, sms); facilita adicionar Slack, Teams, etc.

Exemplo de uso no seu código:

$slots = apply_filters('manutencao_slots_por_janela', 3, $bloco, $data, $janela);

// No save_post
do_action('manutencao_antes_salvar_plano', $post_id, $_POST);
// ... validações ...
do_action('manutencao_depois_salvar_plano', $post_id);

Essa abordagem permite que uma agência WordPress que gerencia tempo para reparo de direção hidráulica integre seu plugin ao sistema de frotas dela sem tocar no core. Da mesma forma, uma área do cliente para histórico de manutenções em Moema pode consumir os endpoints REST e exibir cronograma aos moradores.

Para quem atende condomínios na Grande São Paulo, a integração com uma desentupidora de esgoto Grande São Paulo permite disparar ordens de serviço automáticas quando o plugin detecta atraso superior a 48h em inspeções de colunas.

Boas práticas de performance e segurança

  • Nonces em todo AJAX/REST mutante: wp_create_nonce('manutencao_salvar') e verificação via check_ajax_referer ou wp_verify_nonce.
  • Sanitização rigorosa: sanitize_text_field, sanitize_email, wp_kses_post conforme contexto.
  • Capacidades personalizadas: não use edit_posts; crie manage_maintenance_plans e atribua via add_cap no role administrator e sindico (role customizado).
  • Índices de banco: _next_occurrence, _bloco_torre, _status em postmeta; ou migre para tabela customizada acima de 10k agendamentos.
  • Cache de disponibilidade: transient manutencao_disp_{$bloco}_{$data}_{$janela} com 30s TTL; invalide no save_post e delete_post.
  • Logs de auditoria: use WP_Activity_Log ou grave em tabela própria (wp_maintenance_audit) com user_id, action, old_data, new_data, timestamp.

Conclusão prática

Um plugin de agendamento para manutenção hidráulica preventiva deixa de ser “apenas um calendário” quando adota CPTs com capacidades isoladas, validação dupla de conflitos, notificações assíncronas via Action Scheduler, endpoints REST cacheados e migrações versionadas. A extensibilidade via hooks públicos garante que a solução evolua junto com as necessidades do condomínio — seja integrando ao ERP da administradora, ao app dos moradores ou ao sistema de chamados da prestadora. Comece pelo modelo de dados enxuto, teste migrações em staging e exponha apenas o necessário na API; o resto é iteração segura.

Checklist prático

0 de 8 concluídos

Perguntas frequentes

Qual a melhor forma de armazenar recorrências complexas (mensal, trimestral, semestral) no WordPress?

Use um Custom Post Type para os planos de manutenção e armazene a recorrência como meta fields estruturados (frequência, intervalo, dia da semana/mês). Para escalabilidade, crie uma tabela customizada com índices nas colunas de data_próxima_execução e status, evitando queries pesadas em postmeta.

Como garantir que o agendamento não conflite com horários de pico do condomínio?

Implemente um sistema de janelas de horário permitidas por torre/bloco, salvando como taxonomy termos (manhã, tarde, noite) e validando via JavaScript no front-end e PHP no back-end antes de confirmar o agendamento. Use transients para cache de disponibilidade em tempo real.

É melhor usar REST API ou AJAX admin para o painel de agendamento?

Prefira REST API com autenticação via Application Passwords ou nonces, pois desacopla o front-end do back-end e permite usar React/Vue no painel administrativo. O AJAX admin ainda funciona, mas gera código mais acoplado e difícil de testar.

Como lidar com notificações automáticas (e-mail, WhatsApp) sem travar o salvamento do post?

Despache notificações via Action Scheduler ou WP-Cron assíncrono. No hook save_post do CPT, agende uma action para 1 minuto depois que dispara os envios, garantindo resposta rápida ao usuário e resiliência em falhas de envio.

Qual estratégia de versionamento de banco de dados usar ao atualizar o plugin?

Adote migrations com a biblioteca WP-CLI Migration ou crie seu próprio sistema baseado em versão no option 'meu_plugin_db_version'. No hook de ativação/atualização, compare versões e execute SQLs de ALTER TABLE/CREATE INDEX em transação, com rollback em caso de erro.