ActionCampaign service exposes admin CRUD plus a per-user composer (getActiveForUser) that fans out a per-step completion check against the existing per-user model for each ActionStepKind: VideoView for WATCH_VIDEO, CampaignEmail for SUBMIT_INFLUENCE, PetitionSignature for SIGN_PETITION (matched by signer email), Ticket for RSVP_EVENT, ShiftSignup for SIGNUP_SHIFT, ChallengeTeamMember for JOIN_CHALLENGE. CUSTOM and VISIT_LINK steps complete only via explicit self-report. An existing ActionStepCompletion row also short-circuits the check so manual marking and idempotency both work. Volunteer dashboard aggregator at GET /api/volunteer/dashboard composes the active campaign with the user's profile, referral info, upcoming featured event, training shifts (Shift.kind='TRAINING'), ticketed events the user holds, an engagement-counter point total (placeholder until the Redis engagement score is wired in), and resources tagged 'volunteer-resource' across Document/Video/Photo. Bunker Admin
179 lines
4.8 KiB
TypeScript
179 lines
4.8 KiB
TypeScript
import { Router, Request, Response, NextFunction } from 'express';
|
|
import { actionCampaignsService } from './action-campaigns.service';
|
|
import {
|
|
createActionCampaignSchema,
|
|
updateActionCampaignSchema,
|
|
createActionStepSchema,
|
|
updateActionStepSchema,
|
|
reorderStepsSchema,
|
|
markCompleteSchema,
|
|
} from './action-campaigns.schemas';
|
|
import { validate } from '../../middleware/validate';
|
|
import { authenticate, optionalAuth } from '../../middleware/auth.middleware';
|
|
import { requireRole } from '../../middleware/rbac.middleware';
|
|
import { INFLUENCE_ROLES } from '../../utils/roles';
|
|
|
|
const publicRouter = Router();
|
|
|
|
publicRouter.get(
|
|
'/active',
|
|
optionalAuth,
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
if (req.user?.id) {
|
|
const data = await actionCampaignsService.getActiveForUser(req.user.id);
|
|
res.json(data);
|
|
return;
|
|
}
|
|
const campaign = await actionCampaignsService.getActiveCampaign();
|
|
res.json(campaign);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
publicRouter.post(
|
|
'/:slug/steps/:stepId/complete',
|
|
authenticate,
|
|
validate(markCompleteSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const stepId = req.params.stepId as string;
|
|
const completion = await actionCampaignsService.markStepComplete(
|
|
req.user!.id,
|
|
stepId,
|
|
(req.body as { source?: import('@prisma/client').ActionStepCompletionSource }).source,
|
|
);
|
|
res.status(201).json({ ok: true, completion });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
const adminRouter = Router();
|
|
adminRouter.use(authenticate);
|
|
adminRouter.use(requireRole(...INFLUENCE_ROLES));
|
|
|
|
adminRouter.get('/', async (_req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const campaigns = await actionCampaignsService.listCampaigns();
|
|
res.json(campaigns);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
adminRouter.post(
|
|
'/',
|
|
validate(createActionCampaignSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const campaign = await actionCampaignsService.createCampaign(req.body, req.user!.id);
|
|
res.status(201).json(campaign);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
adminRouter.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const id = req.params.id as string;
|
|
const campaign = await actionCampaignsService.getCampaign(id);
|
|
res.json(campaign);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
adminRouter.put(
|
|
'/:id',
|
|
validate(updateActionCampaignSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const id = req.params.id as string;
|
|
const campaign = await actionCampaignsService.updateCampaign(id, req.body);
|
|
res.json(campaign);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
adminRouter.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const id = req.params.id as string;
|
|
await actionCampaignsService.deleteCampaign(id);
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
adminRouter.post('/:id/activate', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const id = req.params.id as string;
|
|
const campaign = await actionCampaignsService.activateCampaign(id);
|
|
res.json(campaign);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
adminRouter.post(
|
|
'/:id/steps',
|
|
validate(createActionStepSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const id = req.params.id as string;
|
|
const step = await actionCampaignsService.addStep(id, req.body);
|
|
res.status(201).json(step);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
adminRouter.put(
|
|
'/:id/steps/:stepId',
|
|
validate(updateActionStepSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const stepId = req.params.stepId as string;
|
|
const step = await actionCampaignsService.updateStep(stepId, req.body);
|
|
res.json(step);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
adminRouter.delete('/:id/steps/:stepId', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const stepId = req.params.stepId as string;
|
|
await actionCampaignsService.deleteStep(stepId);
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
adminRouter.post(
|
|
'/:id/steps/reorder',
|
|
validate(reorderStepsSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const id = req.params.id as string;
|
|
const { stepIds } = req.body as { stepIds: string[] };
|
|
const steps = await actionCampaignsService.reorderSteps(id, stepIds);
|
|
res.json(steps);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
export { publicRouter as actionCampaignsRouter, adminRouter as actionCampaignsAdminRouter };
|