Add outreach candidate editing controls
This commit is contained in:
1 parent
c75e71f26e
commit
79c508be2b
19 files changed
+434
-12
No files matched your search
@@ -1,8 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import type { CandidateStatus } from '../../database/candidate-queue-repository.js';
|
||||
import { isSafeOutboundMessage } from '../../services/outbound-message-safety.js';
|
||||
import type { WebContext } from '../context.js';
|
||||
|
||||
const ACTIVE_STATUSES = ['discovered', 'screening', 'ready', 'sending', 'retryable'] as const;
|
||||
const MessageUpdateSchema = z.strictObject({ message: z.string().trim().min(1).max(2000) });
|
||||
type ActiveCandidateStatus = Exclude<CandidateStatus, 'sent' | 'rejected'>;
|
||||
|
||||
interface OutreachCandidateView {
|
||||
@@ -96,6 +99,7 @@ export function createOutreachRouter(ctx: WebContext): Router {
|
||||
modelId: candidate.prepared.modelId,
|
||||
toneId: candidate.prepared.toneId,
|
||||
scripted: candidate.prepared.scripted,
|
||||
operatorEdited: candidate.prepared.operatorEdited ?? false,
|
||||
} : null,
|
||||
note: candidate.lastError,
|
||||
owner: candidate.assignedAccountId
|
||||
@@ -148,5 +152,38 @@ export function createOutreachRouter(ctx: WebContext): Router {
|
||||
});
|
||||
});
|
||||
|
||||
router.patch('/outreach/:postId/message', (req, res) => {
|
||||
const queue = ctx.services.candidateQueue;
|
||||
if (!queue) return res.status(503).json({ error: 'Outreach queue not initialized' });
|
||||
const parsed = MessageUpdateSchema.safeParse(req.body);
|
||||
if (!parsed.success) return res.status(400).json({ error: 'Message must contain 1–2000 characters' });
|
||||
if (!isSafeOutboundMessage(parsed.data.message)) {
|
||||
return res.status(400).json({ error: 'Message fails location or in-person meeting safeguards' });
|
||||
}
|
||||
const result = queue.updatePreparedMessage(req.params.postId, parsed.data.message);
|
||||
if (result === 'not-found') return res.status(404).json({ error: 'Outreach candidate not found' });
|
||||
if (result === 'not-editable') {
|
||||
return res.status(409).json({ error: 'Only ready or retrying messages can be edited' });
|
||||
}
|
||||
const message = `Edited prepared outreach message for candidate ${req.params.postId}`;
|
||||
ctx.activity.push('info', message);
|
||||
ctx.broadcaster.push('info', message);
|
||||
return res.json({ success: true, postId: req.params.postId, message: parsed.data.message });
|
||||
});
|
||||
|
||||
router.delete('/outreach/:postId', (req, res) => {
|
||||
const queue = ctx.services.candidateQueue;
|
||||
if (!queue) return res.status(503).json({ error: 'Outreach queue not initialized' });
|
||||
const result = queue.cancelUnsent(req.params.postId);
|
||||
if (result === 'not-found') return res.status(404).json({ error: 'Outreach candidate not found' });
|
||||
if (result === 'not-cancellable') {
|
||||
return res.status(409).json({ error: 'Candidate is already sending or has finished' });
|
||||
}
|
||||
const message = `Removed outreach candidate ${req.params.postId} from the funnel`;
|
||||
ctx.activity.push('info', message);
|
||||
ctx.broadcaster.push('warn', message);
|
||||
return res.json({ success: true, postId: req.params.postId });
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in new issue
Block a user