-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathroute.ts
More file actions
112 lines (101 loc) · 4.83 KB
/
Copy pathroute.ts
File metadata and controls
112 lines (101 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* A realistic booking export, so anyone can see the radar work in ten seconds
* without having to dig their real one out of Fresha first.
*
* It is generated rather than stored as a static file because every risk call is
* relative to today. A fixed CSV would drift: the first-timer inside the 30-day
* cliff today would read as long gone in two months, and the demo would quietly
* stop making its own point.
*/
export const dynamic = 'force-dynamic'
type Visit = { name: string; email: string; service: string; price: number; daysAgo: number }
const SERVICES: Record<string, number> = {
'Cut and colour': 165,
'Balayage': 240,
'Root touch-up': 95,
'Cut and style': 75,
'Blow dry': 45,
'Highlights': 190,
'Beard trim': 30,
}
/** Someone who comes every `cadence` days, `count` times, ending `lastSeen` days ago. */
function regular(name: string, service: string, cadence: number, count: number, lastSeen: number): Visit[] {
const price = SERVICES[service]
return Array.from({ length: count }, (_, i) => ({
name,
email: `${name.toLowerCase().replace(/[^a-z]/g, '.')}@example.com`,
service,
// A little price drift, because real books are never perfectly uniform.
price: price + (i % 3) * 5,
daysAgo: lastSeen + (count - 1 - i) * cadence,
}))
}
/**
* `?returned=1` is the same book a few days later, with one difference: Nina,
* the first-timer who was about to fall off the 30-day cliff, has booked a
* second visit. Uploading it after a win-back is what proves a save, since
* recovery is only ever measured against the owner's own fresh export.
*/
function buildRows(returned: boolean): Visit[] {
const rows: Visit[] = [
// On rhythm: a 5-week regular seen 4 weeks ago. The radar must leave her alone.
...regular('Priya Raman', 'Cut and colour', 35, 6, 28),
// The thesis, as a matched pair. Both were last in 44 days ago, so any
// "flag anyone past 30/60 days" rule treats them identically and gets one of
// them wrong. Judged against their OWN rhythms the calls invert:
// Aisha is an 8-week regular, so 44 days is early. She is fine, leave her alone.
...regular('Aisha Bello', 'Highlights', 56, 4, 44),
// Jane is a 4-week regular, so the same 44 days means she is slipping away.
...regular('Jane Whitfield', 'Root touch-up', 28, 8, 44),
// Slipping away: a 3-week regular now 6 weeks out, double his own rhythm.
...regular('Marcus Doyle', 'Beard trim', 21, 11, 44),
// Drifting: a 5-week regular at 7 weeks. Not an emergency yet, worth a nudge.
...regular('Elena Sokolov', 'Cut and style', 35, 7, 47),
// The flagship case: came once, 18 days ago, never rebooked. 12 days left on
// the cliff. Highest priority in the system, and the one nobody watches.
{ name: 'Nina Kowalski', email: 'nina.kowalski@example.com', service: 'Balayage', price: 240, daysAgo: 18 },
// Another first-timer, day 24. The window is nearly shut.
{ name: 'Tom Ashby', email: 'tom.ashby@example.com', service: 'Cut and style', price: 75, daysAgo: 24 },
// Too early: first visit 3 days ago. Contacting her now would be pestering.
{ name: 'Freda Lam', email: 'freda.lam@example.com', service: 'Blow dry', price: 45, daysAgo: 3 },
// Long gone: one visit, 5 months ago. Worth one honest attempt, not a priority.
{ name: 'Colin Grady', email: 'colin.grady@example.com', service: 'Cut and style', price: 75, daysAgo: 154 },
]
if (returned) {
// Nina answered the note and rebooked two days ago. Her second visit is what
// flips her from "about to be lost" to a measured save.
rows.push({
name: 'Nina Kowalski',
email: 'nina.kowalski@example.com',
service: 'Root touch-up',
price: 95,
daysAgo: 2,
})
}
return rows
}
function toDate(daysAgo: number): string {
const d = new Date(Date.now() - daysAgo * 86_400_000)
return d.toISOString().slice(0, 10)
}
/** The sample as a CSV string, so the import route can seed it without a round
* trip through HTTP. */
export function sampleCsv(returned: boolean): string {
const rows = buildRows(returned)
.sort((a, b) => b.daysAgo - a.daysAgo)
// Deliberately messy headers and column order: this is what a real export looks
// like, and the importer is supposed to cope with it.
.map((v) => `${toDate(v.daysAgo)},${v.name},${v.email},${v.service},${v.price.toFixed(2)},Completed`)
return ['Appointment Date,Client Name,Client Email,Service,Total,Status', ...rows].join('\r\n')
}
export async function GET(request: Request) {
const returned = new URL(request.url).searchParams.get('returned') === '1'
const csv = sampleCsv(returned)
return new Response(csv, {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': `attachment; filename="sample-bookings${returned ? '-week-later' : ''}.csv"`,
'Cache-Control': 'no-store',
},
})
}