feat(sales): admin-configurable EOI signers + richer timeline events
1. Per-port EOI signer config
- New `eoi_signers` system_settings key (JSON: { developer, approver },
each `{ name, email }`). Settings UI exposes it under Admin → Settings.
- getPortEoiSigners(portId) reads the setting with a typed validator;
falls back to the legacy David Mizrahi / Abbie May defaults if the
row is missing or malformed (so older ports keep working until an
admin saves a value).
- Both EOI generation pathways now read from the helper instead of
hardcoded constants:
* documenso-template path (generateAndSignViaDocumensoTemplate)
* in-app PDF-fill path (generateAndSignViaInApp)
2. Timeline upgrades
The interest detail Activity tab now distinguishes the new automation
events that arrived with sessions 1+2:
- Stage auto-advances (userId='system') get a small "Auto" pill and
carry their reason into the description (e.g. "Stage advanced to
EOI Signed (auto-advanced — EOI signed via Documenso)").
- outcome_set events show "Marked as Won" / "Marked as Lost — went
to another marina" with optional reason; trophy/X icons.
- outcome_cleared events show "Reopened to {stage}" with a refresh
icon.
- Document events humanized: "Document 'X' fully signed" instead
of "Document X: completed".
- Stage labels run through stageLabel() so the timeline shows the
human label, not the enum key.
- Timestamps switched to relative-time with full-date tooltip.
- "by system" is rendered plainly (no longer the literal user-id).
tsc clean. vitest 832/832 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { systemSettings } from '@/lib/db/schema/system';
|
||||
import type { EoiContext } from '@/lib/services/eoi-context';
|
||||
|
||||
export interface DocumensoTemplatePayload {
|
||||
@@ -52,6 +56,45 @@ const DEFAULT_APPROVER_NAME = 'Abbie May';
|
||||
const DEFAULT_APPROVER_EMAIL = 'sales@portnimara.com';
|
||||
const DEFAULT_REDIRECT_URL = 'https://portnimara.com';
|
||||
|
||||
export interface EoiSignerConfig {
|
||||
developer: { name: string; email: string };
|
||||
approver: { name: string; email: string };
|
||||
}
|
||||
|
||||
const DEFAULT_EOI_SIGNERS: EoiSignerConfig = {
|
||||
developer: { name: DEFAULT_DEVELOPER_NAME, email: DEFAULT_DEVELOPER_EMAIL },
|
||||
approver: { name: DEFAULT_APPROVER_NAME, email: DEFAULT_APPROVER_EMAIL },
|
||||
};
|
||||
|
||||
function isSignerEntry(v: unknown): v is { name: string; email: string } {
|
||||
return (
|
||||
!!v &&
|
||||
typeof v === 'object' &&
|
||||
typeof (v as Record<string, unknown>).name === 'string' &&
|
||||
typeof (v as Record<string, unknown>).email === 'string' &&
|
||||
!!(v as Record<string, string>).name &&
|
||||
!!(v as Record<string, string>).email
|
||||
);
|
||||
}
|
||||
|
||||
/** Read the per-port `eoi_signers` setting, fall back to legacy hardcoded
|
||||
* defaults if missing or malformed. The fallback exists to keep older
|
||||
* ports working until an admin saves the setting; once saved, the DB row
|
||||
* always wins. */
|
||||
export async function getPortEoiSigners(portId: string): Promise<EoiSignerConfig> {
|
||||
const row = await db.query.systemSettings.findFirst({
|
||||
where: and(eq(systemSettings.key, 'eoi_signers'), eq(systemSettings.portId, portId)),
|
||||
});
|
||||
const value = row?.value as Record<string, unknown> | undefined;
|
||||
if (value && isSignerEntry(value.developer) && isSignerEntry(value.approver)) {
|
||||
return {
|
||||
developer: value.developer,
|
||||
approver: value.approver,
|
||||
};
|
||||
}
|
||||
return DEFAULT_EOI_SIGNERS;
|
||||
}
|
||||
|
||||
function formatAddress(address: EoiContext['client']['address']): string {
|
||||
if (!address) return '';
|
||||
return [address.street, address.city, address.country].filter(Boolean).join(', ');
|
||||
|
||||
Reference in New Issue
Block a user