Files
pn-new-crm/src/app/api/v1/tracked-links/route.ts

51 lines
1.6 KiB
TypeScript
Raw Normal View History

feat(uat-batch): Group O — Umami in-repo polish O48, O51-O54 from the 2026-05-21 plan. Phase 4a / 3 / 5 marketing-site work explicitly deferred — they live in the marketing repo + are blocked on instrumentation that isn't this codebase's to ship. Shipped: O48 Tracked-link composer button. New POST /api/v1/tracked-links mints a redirect-link the rep can drop into an outgoing email. Body { targetUrl, sendId? }; returns { id, slug, targetUrl, url }. Gated on `email.send` (same as the server-side check on existing send routes). `sendId` lets the click-tracker attribute back to a specific document_sends row. <TrackedLinkComposerButton> renders a small inline button (or a sized default variant) that opens a dialog: rep pastes the destination URL → Create → gets the public /q/<slug> URL with a Copy + an "Insert into message" action that calls back to the parent compose surface. Wired into <SendDocumentDialog>'s Message body label row so reps can mint + insert without leaving the dialog. O51 Quiet-range nudge. WebsiteAnalyticsShell surfaces a small amber banner when the active range returned <5 visitors so the rep doesn't think the integration is broken on a fresh port or off-season range. Threshold keeps the banner off legitimate traffic. O52 Apple Mail privacy disclaimer. The sends-log "Not opened" badge carries an inline tooltip explaining that Apple Mail's privacy protection routes opens through Apple's proxy and can suppress this signal even when the recipient read the email. O53 Open-rate column on the document_sends list. SendRow type extended with `trackOpens` / `openCount` / `firstOpenedAt`; the sends-log card chrome renders an "Opened × N" badge with the first-open timestamp in the title, or "Not opened" when tracking is on but no opens yet, or no badge at all when tracking was disabled for that send. O54 Click-to-filter world map. VisitorWorldMap already supported `onCountryClick`; wired it through to copy the `/<portSlug>/clients?nationality=<ISO>` deep-link to the clipboard with a toast on click. Inline filtering of the analytics view itself stays parked alongside Phase 5 — the useUmami* hooks don't yet accept a country filter. Deferred (not in this repo or blocked): O47 Phase 4a marketing-site instrumentation — marketing repo work. O49 Phase 3 Events tab — blocked on 4a. O50 Phase 5 Funnels + Journeys — blocked on 4a. Verified: tsc clean, vitest 1454/1454. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:39:19 +02:00
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { withAuth } from '@/lib/api/helpers';
import { parseBody } from '@/lib/api/route-helpers';
import { buildTrackedUrl, createTrackedLink } from '@/lib/services/tracked-links.service';
import { errorResponse } from '@/lib/errors';
/**
* POST /api/v1/tracked-links
*
* Mints a new tracked redirect-link the rep can drop into an outgoing
* email or chat. Body: { targetUrl, sendId? }. Returns the slug + the
* full public URL (`<APP_URL>/q/<slug>`) - caller pastes the URL into
feat(uat-batch): Group O — Umami in-repo polish O48, O51-O54 from the 2026-05-21 plan. Phase 4a / 3 / 5 marketing-site work explicitly deferred — they live in the marketing repo + are blocked on instrumentation that isn't this codebase's to ship. Shipped: O48 Tracked-link composer button. New POST /api/v1/tracked-links mints a redirect-link the rep can drop into an outgoing email. Body { targetUrl, sendId? }; returns { id, slug, targetUrl, url }. Gated on `email.send` (same as the server-side check on existing send routes). `sendId` lets the click-tracker attribute back to a specific document_sends row. <TrackedLinkComposerButton> renders a small inline button (or a sized default variant) that opens a dialog: rep pastes the destination URL → Create → gets the public /q/<slug> URL with a Copy + an "Insert into message" action that calls back to the parent compose surface. Wired into <SendDocumentDialog>'s Message body label row so reps can mint + insert without leaving the dialog. O51 Quiet-range nudge. WebsiteAnalyticsShell surfaces a small amber banner when the active range returned <5 visitors so the rep doesn't think the integration is broken on a fresh port or off-season range. Threshold keeps the banner off legitimate traffic. O52 Apple Mail privacy disclaimer. The sends-log "Not opened" badge carries an inline tooltip explaining that Apple Mail's privacy protection routes opens through Apple's proxy and can suppress this signal even when the recipient read the email. O53 Open-rate column on the document_sends list. SendRow type extended with `trackOpens` / `openCount` / `firstOpenedAt`; the sends-log card chrome renders an "Opened × N" badge with the first-open timestamp in the title, or "Not opened" when tracking is on but no opens yet, or no badge at all when tracking was disabled for that send. O54 Click-to-filter world map. VisitorWorldMap already supported `onCountryClick`; wired it through to copy the `/<portSlug>/clients?nationality=<ISO>` deep-link to the clipboard with a toast on click. Inline filtering of the analytics view itself stays parked alongside Phase 5 — the useUmami* hooks don't yet accept a country filter. Deferred (not in this repo or blocked): O47 Phase 4a marketing-site instrumentation — marketing repo work. O49 Phase 3 Events tab — blocked on 4a. O50 Phase 5 Funnels + Journeys — blocked on 4a. Verified: tsc clean, vitest 1454/1454. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:39:19 +02:00
* the message draft.
*
* Gated on `email.send` since this surface is consumed from compose UIs.
* Tracked links aren't sensitive on their own but reps shouldn't be
* able to mint click-trackers without the underlying send permission.
*/
const createSchema = z.object({
targetUrl: z.string().url(),
sendId: z.string().min(1).optional(),
});
export const POST = withAuth(async (req, ctx) => {
try {
const body = await parseBody(req, createSchema);
const allowed = ctx.isSuperAdmin ? true : !!ctx.permissions?.email?.send;
if (!allowed) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const row = await createTrackedLink({
portId: ctx.portId,
targetUrl: body.targetUrl,
sendId: body.sendId,
createdByUserId: ctx.userId,
});
return NextResponse.json({
data: {
id: row.id,
slug: row.slug,
targetUrl: row.targetUrl,
url: buildTrackedUrl(row.slug),
},
});
} catch (error) {
return errorResponse(error);
}
});