feat(messages): Phase 3 — editable-template composer, reply + create actions

The CRM action buttons and Reply now open a send-composer, not a silent
templated send.

- New send-composer (MessageComposer): loads the rendered template (via
  previewTemplate) or a reply stub into a fully-editable body — the admin can
  rewrite it or drop a note anywhere before sending. On send it goes out as-is
  (server-sanitized), no template re-render.
- Backend: emailProcessor.sendRawEmail() sends admin-edited HTML via the
  configured SMTP identity; POST /admin/email/send sanitizes + sends + records
  the message in email_queue as a 'manual' send.
- Migration 155: email_queue.origin ('system' default | 'manual'). The Sent
  stream now splits by origin — Automated ▸ Sent = system, Customers ▸ Sent =
  the human/edited messages (which finally populates that folder). /queue gains
  an origin filter + returns origin.
- Toolbar wired: Reply enabled on inbound customer mail (prefilled + quoted);
  Create Quote/Contract/Invoice open the composer with that template loaded;
  Gallery opens a blank compose. Accounting/Forward/Archive/Delete stay disabled
  (later phases). After send, jumps to Customers ▸ Sent.

Deferred to a later phase: two-way IMAP write-back; per-identity SMTP (manual
sends currently use the global from address). Frontend build + migration boot
verified.
This commit is contained in:
Luca
2026-07-07 10:42:54 +02:00
parent ee46cf2125
commit 768e84711f
6 changed files with 324 additions and 28 deletions
+8
View File
@@ -15,6 +15,8 @@ export interface EmailQueueItem {
eventId: number | null;
eventName: string | null;
eventSlug: string | null;
/** 'system' = app-generated (Automated), 'manual' = admin-composed (Customers Sent). */
origin?: 'system' | 'manual';
}
export interface EmailQueueListResponse {
@@ -244,6 +246,7 @@ export const emailService = {
async listQueue(params: {
status?: EmailQueueStatus;
emailType?: string;
origin?: 'system' | 'manual';
q?: string;
from?: string;
to?: string;
@@ -260,6 +263,11 @@ export const emailService = {
return response.data;
},
/** Send a human-composed (edited) email — reply or document message. */
async sendMessage(payload: { to: string; cc?: string; subject: string; html: string; replyToReceivedId?: number }): Promise<void> {
await api.post('/admin/email/send', payload);
},
// Get all email templates
async getTemplates(): Promise<EmailTemplate[]> {
const response = await api.get<EmailTemplate[]>('/admin/email/templates');