Correu i WhatsApp

L’app de gestió ja pot enviar i rebre correu i enviar i rebre WhatsApp. Tot es configura i es consulta des de dues pantalles noves dins de /compta/, i els avisos automàtics (confirmacions, recordatoris, tiquets) surten sols quan es creen cites, vendes o fitxes de client.

Què hem muntat

Tres serveis en segon pla i un connector dins de WordPress:

Peça Què fa On escolta
Mailpit Servidor de correu local: rep tot el correu, el desa i en mostra la safata 127.0.0.1:1025 (SMTP) i 127.0.0.1:8025 (safata i API)
Pont de WhatsApp Es vincula a WhatsApp Web amb un codi QR i envia/rep missatges 127.0.0.1:3001
Tasques de WordPress Importa el correu i envia els recordatoris cada minut
Plugin compt-comunicacions Connecta tot això amb l’app: ajustos, bústia, plantilles i automatitzacions

Per què no hi ha Postfix

En aquest entorn no hi ha root: sudo està bloquejat pel flag no new privileges, així que no s’hi poden instal·lar paquets de sistema com Postfix o Dovecot. La solució és fer servir serveis que corren en espai d’usuari, gestionats amb systemd d’usuari (amb Linger=yes, o sigui que sobreviuen al tancament de sessió i als reinicis).

Mailpit és un binari únic que fa d’MTA de proves: accepta el correu pel port 1025 i el desa en una base de dades SQLite, amb safata web i API HTTP incloses.

El servidor de correu local

Servei Unitat systemd Comanda
Mailpit mailpit.service systemctl --user restart mailpit
Pont de WhatsApp whatsapp-bridge.service systemctl --user restart whatsapp-bridge
Tasques de WordPress compt-comms-cron.timer systemctl --user list-timers

La safata de Mailpit es pot obrir directament a http://127.0.0.1:8025. Els registres són a /var/www/html/comms/logs/.

# Estat dels tres serveis
systemctl --user status mailpit whatsapp-bridge compt-comms-cron.timer

# Registres
tail -f /var/www/html/comms/logs/mailpit.log
tail -f /var/www/html/comms/logs/whatsapp.log
tail -f /var/www/html/comms/logs/wp-cron.log

Enviar correu: local o SMTP extern

El connector intercepta phpmailer_init i decideix per on surt el correu segons el mode triat a la pantalla de configuració:

  • Servidor local — tot el correu va a Mailpit (127.0.0.1:1025). És el mode per defecte i serveix per desenvolupar i provar sense enviar res de veritat.
  • SMTP extern — s’envia pel proveïdor que triïs (Gmail amb contrasenya d’aplicació, Brevo, Mailgun…): host, port, xifrat (STARTTLS o SSL), usuari i contrasenya.

També es filtren wp_mail_from i wp_mail_from_name, i es força Reply-To. Detall important après d’una prova: PHPMailer rebutja adreces sense domini amb punt, així que un remitent no-reply@nitidastudio.synology.me falla; per defecte s’usa un domini vàlid i, si el configurat no ho és, es cau a no-reply@<domini>.

Com que tot passa per wp_mail(), els correus de WooCommerce també surten per aquí.

Rebre correu

Mailpit envia un webhook a WordPress cada cop que arriba un correu nou:

GET/POST https://nitidastudio.synology.me/?compt_comms_mailpit=<testimoni>

El plugin valida el testimoni, demana el missatge sencer a l’API de Mailpit i el desa a la taula wp_comms_missatges. Com a xarxa de seguretat, el cron també fa un sondeig cada minut, de manera que cap correu no es perd si el webhook falla. Si el remitent coincideix amb l’email (o el telèfon) d’un client, el missatge s’enllaça sol a la seva fitxa; si no, es pot vincular a mà des de la bústia.

El pont de WhatsApp

És un petit servei en Node que fa servir la llibreria Baileys (WhatsApp Web). No cal compte de negoci ni API oficial: es vincula el mòbil escanejant un codi QR.

# Vincular (una sola vegada)
Obre /compta/comunicacions/ i escaneja el QR amb:
WhatsApp → Dispositius vinculats → Vincular un dispositiu

# Canviar de mòbil
/var/www/html/comms/reset-whatsapp.sh

El pont exposa una API senzilla, sempre a localhost i protegida amb un testimoni compartit (X-Compt-Token):

Endpoint Què fa
GET /health Estat: connectat, número vinculat, si hi ha QR
GET /qr.png Imatge del codi QR (la que es veu a la pantalla de configuració)
POST /send Enviar un text a un número
POST /send-media Enviar imatge, vídeo, àudio o document
POST /logout Desvincular el dispositiu

Quan arriba un missatge, el pont l’envia a /wp-json/compt-comms/v1/whatsapp/inbound; el plugin n’extreu el text (conversa, text estès, subtítols d’imatge o document, botons i llistes), l’enllaça al client pel telèfon i el deixa a la bústia. Els missatges de grup i els estats/reaixos s’ignoren.

Les pantalles noves

Al menú de l’app hi ha un grup nou, Comunicacions, amb dues entrades:

  • /compta/comunicacions/ — estat dels serveis, QR de WhatsApp, configuració de correu i de WhatsApp (pont local, Meta Cloud API o Twilio), automatitzacions i plantilles, amb botons per enviar correus i WhatsApps de prova i per sincronitzar la bústia.
  • /compta/missatges/ — bústia unificada: filtres per canal, sentit i estat, cerca, conversa per fils, resposta directa, vinculació a client i redacció de missatges nous.

Les rutes funcionen com la resta de l’app: s’afegeixen a astrolus_crud_slugs() i una entitat virtual a astrolus_entities(), i el plugin enganxa un filtre a template_include que carrega les seves plantilles. Els avisos de resultat es mostren amb paràmetres ?compt_msg=.

Automatitzacions

El tema ara dispara accions en desar registres (astrolus_entity_saved_<taula>), i el plugin les escolta:

Avis Quan surt Canal per defecte
Benvinguda En crear la fitxa d’un client Correu i WhatsApp
Confirmació de cita En crear una cita Correu i WhatsApp
Recordatori de cita 24 h abans (configurable) Correu i WhatsApp
Tiquet o factura En registrar una venda Correu
Avís a l’equip Cites i vendes noves Correu (als empleats actius)

Cada automatització es pot activar o desactivar i canviar de canal des de la pantalla de configuració. Els textos són plantilles editables amb variables com {client_nom}, {data}, {hora}, {servei}, {import} o {linies}. Els enviaments queden registrats a wp_comms_log per no repetir-se: el recordatori, per exemple, només s’envia un cop per cita.

Com ho hem verificat

  • Correu de prova enviat des de la pantalla → capturat per Mailpit → importat sol a la bústia.
  • Correu entrant des del correu d’una clienta → enllaçat automàticament a la seva fitxa.
  • Cita creada → confirmació a la clienta i avís a l’equip; el recordatori surt dins la finestra de 24 h i no es duplica si es torna a executar.
  • Venda creada → tiquet a la clienta i avís a l’equip.
  • Pont de WhatsApp amb QR vàlid; sense vincular, l’enviament retorna un error controlat i queda registrat.

Seguretat

  • Els tres serveis escolten només a 127.0.0.1: des d’una altra màquina de la xarxa no s’hi arriba.
  • La carpeta de serveis (/var/www/html/comms/) està bloquejada per web amb un .htaccess que retorna 403; a dins hi ha els testimonis i les credencials de WhatsApp.
  • Els endpoints REST exigeixen el testimoni compartit i retornen 401 sense ell.
  • Les pantalles de Comunicacions només són accessibles amb sessió de WordPress iniciada.
  • La carpeta comms/whatsapp/auth/ conté la sessió de WhatsApp: no s’ha de versionar mai.

Què queda pendent

  1. Vincular el WhatsApp: fins que no s’escaneja el QR, els enviaments de WhatsApp queden registrats com a error.
  2. Enviar correu de veritat: cal omplir el mode SMTP extern amb credencials.
  3. Rebre correu d’Internet (per exemple info@elteudomini.cat) requereix un domini amb registres MX i un servidor amb el port 25 obert; aquí no és possible, però en un VPS es podria instal·lar Postfix i Dovecot o contractar un servei de correu.
  4. El pont de WhatsApp fa servir WhatsApp Web, que no és l’API oficial: va molt bé per a un ús normal, però per a volums alts o números dedicats és millor Meta Cloud API o Twilio, totes dues ja suportades a la configuració.
  5. Els fitxers multimèdia entrants de WhatsApp es registren amb les seves dades (tipus i nom), però encara no es descarrega el contingut.

Fitxers clau

/var/www/html/comms/
├── bin/mailpit                 # servidor de correu
├── mailpit-data/               # missatges de Mailpit
├── whatsapp/server.js          # pont de WhatsApp (Node + Baileys)
├── whatsapp/config.json        # port, testimonis i URLs dels webhooks
├── units/                      # unitats systemd
├── logs/                       # registres
└── README.md                   # documentació completa

wp-content/plugins/compt-comunicacions/
├── includes/class-store.php       # taules de missatges, registre i cua
├── includes/class-settings.php    # ajustos i plantilles
├── includes/class-mailer.php      # SMTP i plantilles de correu
├── includes/class-whatsapp.php    # pont local, Meta i Twilio
├── includes/class-inbound.php     # webhooks i importació de correu
├── includes/class-automations.php # avisos automàtics
├── includes/class-ui.php          # rutes, accions i avisos
└── templates/                     # pantalles de configuració i bústia

Tauler i rutes /compta/

El tauler és la porta de l’app interna. Si ja has entrat, / et porta aquí: /compta/. No és una pàgina de WordPress amb shortcode: és una ruta nostra que acaba a page-dashboard.php.

Icona Dusk de document i calculadora, logo de Nítida
El tauler és la compta: clients, productes, vendes, agenda. El logo Dusk ho resumeix.

Qui pot entrar

Qualsevol URL de /compta/ exigeix sessió WordPress. Si no hi ha login, astrolus_handle_requests() redirigeix a la portada (formulari d’entrar). El Front (/front/) és l’única ruta Nítida pública. Després del login, el destí és sempre /compta/.

El POST astrolus_login fa wp_signon() i, si va bé, wp_safe_redirect( home_url( '/compta/' ) ).

Rutes físiques i plantilles

Nginx/Apache veuen carpetes reals sota compta/. Cada index.php només marca el mòdul i torna a carregar WordPress:

// compta/index.php
$_GET['nitida'] = 'dashboard';
require dirname(__DIR__) . '/index.php';

// compta/clients/index.php
$_GET['nitida'] = 'clients';
require dirname(__DIR__, 2) . '/index.php';

Després, template_include tria la plantilla. Sense això, WordPress intentaria una pàgina o un 404.

Query vars

functions.php llegeix la URL (o ?nitida=) i omple tres vars:

  • nitida — mòdul: dashboard, clients, vendes, cites
  • nitida_actionnew, edit, fitxa (si hi ha id i no hi ha action, assumeix edit)
  • nitida_id — id del registre

També desactivem redirect_canonical a /compta/ i /front/ perquè WP no ens reescrigui les rutes.

Quina plantilla surt

Ruta Plantilla
/compta/ page-dashboard.php
/compta/clients/ (llista / editar) page-compta.php
/compta/clients/?action=fitxa&id= page-client.php
/compta/cites/ (setmana) page-agenda.php
/compta/cites/?action=new page-compta.php
Altres CRUD page-compta.php

Els slugs CRUD els marca astrolus_crud_slugs(): clients, empleats, productes, categories, proveïdors, vendes, cites, cabines.

El tauler

page-dashboard.php pinta vuit targetes amb comptadors de astrolus_nitida_stats() (clients, empleats, cites, cabines, productes, categories, proveïdors, vendes) i una taula de les últimes 25 vendes. Cada targeta va a astrolus_nitida_url( $slug ).

Les URLs les construeix astrolus_nitida_url():

/compta/                      → dashboard
/compta/vendes/               → llista
/compta/vendes/?action=new    → alta
/compta/vendes/?id=12         → editar

Menú de l’app

Al navbar, si hi ha sessió, surten els grups de astrolus_nav_groups():

  • Persones — clients, empleats
  • Catàleg — productes, categories
  • Compta — vendes, proveïdors
  • Agenda — cites, cabines

El logo Nítida (sessió oberta) també torna al tauler, no al Front.

Afegir un mòdul

  1. Crea compta/{slug}/index.php que faci $_GET['nitida'] = '{slug}'.
  2. Afegeix l’slug a astrolus_crud_slugs() (i a astrolus_entities() si és CRUD).
  3. Si cal una vista especial, enganxa-la a astrolus_template(). Si no, ja usarà page-compta.php.
  4. Posa’l al grup del navbar si l’has de veure cada dia.

El CRUD (llistes, altes, esborrats, sync Woo) el tracten els capítols següents. Aquí només importa: ruta → query var → plantilla.

Logo, favicon i mode clar/fosc

Després d’integrar Astrolus, calia una identitat pròpia: un logo a la barra, un favicon a la pestanya i un mode clar/fosc que no parpellegi. Aquest capítol explica on viuen els fitxers i com es canvien.

Logo Nítida: document i calculadora, estil Icons8 Dusk
El logo actual del navbar: cropped-compt.png, icona Dusk de document + calculadora. És el mateix estil que fem servir a les accions de les taules.

Logo al navbar

La barra fixa (header.php) ja no usa el punt + barra d’Astrolus. El logo és una imatge de la mediateca:

<img src=""
     alt="Nítida" class="h-8 w-auto" />

Va a h-8 (32px d’alçada) per no estirar la barra. El text Nítida continua al costat, en Urbanist. Per canviar el logo: puja un PNG nou a wp-content/uploads/ i actualitza aquesta ruta, o substitueix el fitxer cropped-compt.png.

Favicon a la pestanya

El favicon és un SVG del tema, no el PNG del logo. Així escala bé a 16px i es veu nítid a Chrome, Firefox i Safari.

Favicon Nítida: marca índigo amb punt i barra
Fitxer wp-content/themes/astrolus/assets/favicon.svg: rectangle índigo #4f46e5 amb el punt + barra d’Astrolus en blanc.

Al <head>:

<link rel="icon" type="image/svg+xml" href=".../assets/favicon.svg" />
<link rel="apple-touch-icon" href=".../assets/favicon.svg" />

Si vols el mateix dibuix que el logo (document + calculadora) a la pestanya, canvia aquests dos link cap a cropped-compt.png.

Mode clar i fosc

Astrolus ja venia amb les dues versions. Nosaltres no fem servir prefers-color-scheme a cegues: Tailwind està en darkMode: 'class'. La classe dark va a <html>.

Evitar el flash blanc

Un script al principi del <head>, abans de Tailwind, llegeix localStorage.nitida-theme. Si no hi ha valor, mira prefers-color-scheme. Així la classe dark ja hi és quan es pinta la pàgina:

(function () {
  var stored = localStorage.getItem('nitida-theme');
  var dark = stored
    ? stored === 'dark'
    : window.matchMedia('(prefers-color-scheme: dark)').matches;
  document.documentElement.classList.toggle('dark', dark);
})();

El botó de la barra

El botó #nitida-theme inverteix la classe, desa dark o light a localStorage i dispara l’event nitida-theme per si Highcharts ha de redibuixar. La lluna es veu en mode clar; el sol, en mode fosc.

On tocar-ho

Què Fitxer
Logo navbar wp-content/uploads/2026/09/cropped-compt.png + header.php
Favicon themes/astrolus/assets/favicon.svg
Script anti-flash Principi de header.php
Config Tailwind darkMode: 'class' a header.php
Botó clar/fosc #nitida-theme al final de header.php
Estils dark Blocs html.dark … al CSS del tema

Icones Dusk

Si cal un icona (accions de taula, logo), fem servir el set Icons8 Dusk, a 16px a les taules per no pujar l’alçada de la fila. Funció: astrolus_dusk_icon( 'edit' ).

Astrolus: com hem integrat el tema

Aquest blog servirà de manual de Nítida Clàssic. Cada entrada explicarà una peça del projecte: tema, comptabilitat, agenda, botiga o decisions tècniques. Comencem pel que es veu a tot arreu: el tema Astrolus.

Portada original d’Astrolus en mode clar
El tema original (ThemeWagon / Tailus): blobs, Urbanist, botons píndola i la barra amb el punt + barra índigo. Això és el que hem conservat.

Què és Astrolus

Astrolus és un tema HTML de ThemeWagon, basat en Tailus UI. És una landing moderna: Tailwind, tipografia Urbanist, blobs de color al fons i mode clar/fosc. El peu de Nítida encara en reconeix l’origen: Nítida Clàssic · tema Astrolus.

No el vam instal·lar com a tema WordPress acabat. El vam convertir en un tema clàssic propi, adaptat a un centre d’estètica: login, front públic, tauler intern, CRUD i WooCommerce.

Per què aquest tema

  • Interfície neta, sense un builder pesat.
  • Tailwind per CDN: no cal npm ni pas de compilació al NAS.
  • Mode clar/fosc senzill, que hem lligat a localStorage.
  • Llicència MIT, fàcil de modificar.
Portada original d’Astrolus en mode fosc
El mateix HTML en mode fosc. A Nítida el desem a localStorage.nitida-theme i l’apliquem abans de pintar la pàgina, per evitar el flash blanc.

De HTML a tema WordPress

El tema viu a wp-content/themes/astrolus/. El style.css només declara el tema (Astrolus Nítida). La lògica real està a les plantilles PHP i a functions.php: WordPress carrega el tema i nosaltres decidim quina plantilla surt segons la URL.

Estructura del tema

Fitxer Funció
front-page.php Portada: formulari d’entrar
page-front.php Catàleg públic (/front/)
page-dashboard.php Tauler de /compta/
page-compta.php Llistats i formularis CRUD
page-agenda.php Agenda setmanal de cites
page-client.php Fitxa de client amb pestanyes
page.php Cistella, caixa i compte WooCommerce
page-blog.php Índex d’aquest manual
single.php Una entrada del blog
woocommerce/ Botiga amb el mateix llenguatge visual
inc/ Dades Nítida, entitats, CRUD i sync Woo

Identitat visual que hem conservat

El header.php carrega Urbanist, Tailwind CDN i la paleta (primari #4f46e5, secundari taronja, darkMode: 'class'). La barra fixa i el peu són comuns a totes les pantalles. El que canvia és el contingut: login, front, botiga o compta.

Del catàleg Nítida a les targetes Astrolus

Al Front i a la Botiga les targetes 4:3 no són stock de ThemeWagon. Fem servir les fotos del catàleg Nítida (wp-content/uploads/catalog/), les mateixes que es veuen als productes sincronitzats a WooCommerce.

Enrutament: WordPress no mana sol

L’app no va a pàgines WP per cada mòdul. Fem servir rutes pròpies i template_include:

  • / — login (front-page.php). Si ja has entrat, redirigeix a /compta/.
  • /front/ — catàleg públic
  • /compta/ — tauler i CRUD
  • /botiga/, /cistella/, /pagament/ — WooCommerce
  • /blog/ — aquest manual, amb entrades de WordPress

functions.php llegeix la URL, omple les query vars nitida, nitida_action i nitida_id, i tria la plantilla.

WooCommerce amb cara d’Astrolus

La botiga no usa el look per defecte de Woo. Les plantilles del tema treuen sidebar, tabs i related, i pinten targetes 4:3 com el Front. El sync viu a inc/woocommerce.php: si un producte Nítida té sync_woo, es crea o actualitza el producte Woo (nom, preu, imatge del catàleg, virtual si no és cosmètica).

Com continuar el manual

Les properes entrades poden cobrir, una per una: CRUD, agenda, fitxa de client, botiga i convencions del NAS. Per afegir un capítol: WP Admin → Entrades → Afegeix, categoria Manual, amb una imatge destacada. Sortirà sola a /blog/.