Skip to content

Commit bfff02b

Browse files
authored
Merge pull request #93 from hack4impact/new-kids-adults-services-schemas
Feature/83 create schemas to differentiate services
2 parents c48b797 + e7b71ac commit bfff02b

14 files changed

Lines changed: 3000 additions & 185 deletions

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ next-env.d.ts
5050
*.swo
5151
*~
5252

53-
# drizzle
54-
drizzle/meta/
5553

5654
# supabase
5755
.supabase/

app/(authenticated)/forms/_components/form-question-shared.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ExtraQuestionType } from "../queries";
1+
import type { FormQuestionType } from "../queries";
22

33
export type DraftOption = {
44
id: string;
@@ -8,12 +8,12 @@ export type DraftOption = {
88

99
export type DraftQuestion = {
1010
clientId: string;
11-
type: ExtraQuestionType;
11+
type: FormQuestionType;
1212
prompt: string;
1313
options?: DraftOption[];
1414
};
1515

16-
export const QUESTION_TYPE_LABELS: Record<ExtraQuestionType, string> = {
16+
export const QUESTION_TYPE_LABELS: Record<FormQuestionType, string> = {
1717
text: "Text",
1818
multiple_choices: "Multiple choice",
1919
checkboxes: "Checkboxes",
@@ -28,7 +28,7 @@ export function emptyQuestion(): DraftQuestion {
2828
};
2929
}
3030

31-
export function needsOptions(type: ExtraQuestionType): boolean {
31+
export function needsOptions(type: FormQuestionType): boolean {
3232
return type === "multiple_choices" || type === "checkboxes";
3333
}
3434

app/(authenticated)/forms/_components/question-edit-dialog.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
needsOptions,
3030
QUESTION_TYPE_LABELS,
3131
} from "./form-question-shared";
32-
import type { ExtraQuestionType } from "../queries";
32+
import type { FormQuestionType } from "../queries";
3333

3434
function FieldError({ messages }: { messages?: string[] }) {
3535
if (!messages?.length) return null;
@@ -207,7 +207,7 @@ export function QuestionEditDialog({
207207
<Select
208208
value={draft.type}
209209
onValueChange={(v) => {
210-
const type = v as ExtraQuestionType;
210+
const type = v as FormQuestionType;
211211
setDraft((prev) => {
212212
if (!prev) return prev;
213213
return {
@@ -227,7 +227,7 @@ export function QuestionEditDialog({
227227
</SelectTrigger>
228228
<SelectContent>
229229
{(
230-
Object.keys(QUESTION_TYPE_LABELS) as ExtraQuestionType[]
230+
Object.keys(QUESTION_TYPE_LABELS) as FormQuestionType[]
231231
).map((t) => (
232232
<SelectItem key={t} value={t}>
233233
{QUESTION_TYPE_LABELS[t]}

app/(authenticated)/forms/actions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { revalidatePath, updateTag } from "next/cache";
44
import { count, eq } from "drizzle-orm";
55
import { z } from "zod";
66
import { db } from "@/lib/db";
7-
import { extraQuestions, forms, services } from "@/lib/db/schema";
7+
import { formQuestions, forms, services } from "@/lib/db/schema";
88
import { requireAdmin } from "@/lib/auth/require-admin";
99
import {
1010
createFormSchema,
@@ -93,7 +93,7 @@ async function insertQuestions(
9393
) {
9494
if (questions.length === 0) return;
9595

96-
await client.insert(extraQuestions).values(
96+
await client.insert(formQuestions).values(
9797
questions.map((q, index) => ({
9898
formId,
9999
sortOrder: index,
@@ -185,8 +185,8 @@ export async function updateForm(
185185
.where(eq(forms.id, form_id));
186186

187187
await tx
188-
.delete(extraQuestions)
189-
.where(eq(extraQuestions.formId, form_id));
188+
.delete(formQuestions)
189+
.where(eq(formQuestions.formId, form_id));
190190
await insertQuestions(form_id, questions, tx);
191191
});
192192
} catch {

app/(authenticated)/forms/queries.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import {
2-
ExtraQuestionOption,
3-
extraQuestions,
2+
FormQuestionOption,
3+
formQuestions,
44
forms,
55
services,
66
} from "@/lib/db/schema";
77
import { cacheTag } from "next/cache";
88
import { db } from "@/lib/db";
99
import { asc, count, desc, eq, sql } from "drizzle-orm";
1010

11-
export type ExtraQuestionType = "text" | "multiple_choices" | "checkboxes" | "user_agreement";
11+
export type FormQuestionType = "text" | "multiple_choices" | "checkboxes" | "user_agreement";
1212

1313
export type QuestionView = {
1414
id: string;
1515
sortOrder: number;
16-
type: ExtraQuestionType;
16+
type: FormQuestionType;
1717
prompt: string;
18-
options: ExtraQuestionOption[] | null;
18+
options: FormQuestionOption[] | null;
1919
}
2020

2121
export type FormListItem = {
@@ -42,11 +42,11 @@ export async function listForms(): Promise<FormListItem[]> {
4242
name: forms.name,
4343
createdAt: forms.createdAt,
4444
updatedAt: forms.updatedAt,
45-
questionCount: sql<number>`cast(count(distinct ${extraQuestions.id}) as integer)`,
45+
questionCount: sql<number>`cast(count(distinct ${formQuestions.id}) as integer)`,
4646
attachedServiceCount: sql<number>`cast(count(distinct ${services.id}) as integer)`,
4747
})
4848
.from(forms)
49-
.leftJoin(extraQuestions, eq(extraQuestions.formId, forms.id))
49+
.leftJoin(formQuestions, eq(formQuestions.formId, forms.id))
5050
.leftJoin(services, eq(services.formId, forms.id))
5151
.groupBy(forms.id, forms.name, forms.createdAt, forms.updatedAt)
5252
.orderBy(desc(forms.createdAt));
@@ -76,15 +76,15 @@ export async function getForm(id: string): Promise<FormView | null> {
7676

7777
const questionRows = await db
7878
.select({
79-
id: extraQuestions.id,
80-
sortOrder: extraQuestions.sortOrder,
81-
type: extraQuestions.type,
82-
prompt: extraQuestions.prompt,
83-
options: extraQuestions.options,
79+
id: formQuestions.id,
80+
sortOrder: formQuestions.sortOrder,
81+
type: formQuestions.type,
82+
prompt: formQuestions.prompt,
83+
options: formQuestions.options,
8484
})
85-
.from(extraQuestions)
86-
.where(eq(extraQuestions.formId, id))
87-
.orderBy(asc(extraQuestions.sortOrder));
85+
.from(formQuestions)
86+
.where(eq(formQuestions.formId, id))
87+
.orderBy(asc(formQuestions.sortOrder));
8888

8989
const [serviceRow] = await db
9090
.select({ count: count() })

docs/schema-overview.md

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,31 @@ erDiagram
77
text first_name
88
text last_name
99
role role
10+
text stripe_customer_id
11+
timestamp last_login_at
12+
timestamp created_at
13+
timestamp updated_at
14+
}
15+
16+
forms {
17+
uuid id PK
18+
text name
1019
timestamp created_at
1120
timestamp updated_at
1221
}
1322
1423
services {
1524
uuid id PK
16-
text title
17-
text description
1825
service_type type
19-
jsonb scheduled_at "null for coaching_session"
26+
date start_date "null for private_lessons"
27+
date end_date "null for private_lessons"
28+
jsonb slots "array of {dayOfWeek, time}; null for private_lessons"
2029
int duration_minutes
21-
int price
22-
boolean is_active
30+
text stripe_product_id
31+
service_status status
32+
uuid coach_id FK "null for programs"
33+
uuid form_id FK "nullable"
34+
boolean is_for_children
2335
timestamp created_at
2436
timestamp updated_at
2537
}
@@ -28,9 +40,11 @@ erDiagram
2840
uuid id PK
2941
uuid user_id FK
3042
uuid service_id FK
43+
uuid child_id FK "nullable; null means adult registration"
3144
booking_status status
3245
text notes
3346
boolean is_active
47+
text stripe_order_id
3448
timestamp created_at
3549
timestamp updated_at
3650
}
@@ -52,12 +66,41 @@ erDiagram
5266
uuid service_id FK
5367
uuid coach_id FK
5468
uuid user_id FK
55-
timestamp scheduled_at
56-
int duration_minutes
69+
uuid child_id FK "nullable; null means adult registration"
70+
timestamp scheduled_at "set when slot is confirmed"
5771
session_status status
5872
text meeting_url
5973
text notes
6074
jsonb selected_time_slots "array of {start, end} objects"
75+
jsonb coach_time_slots
76+
text coach_token
77+
text client_token
78+
text stripe_order_id
79+
timestamp created_at
80+
timestamp updated_at
81+
}
82+
83+
subscriptions {
84+
uuid id PK
85+
uuid user_id FK
86+
text stripe_subscription_id
87+
text status
88+
text stripe_price_id
89+
boolean cancel_at_period_end
90+
text payment_method_brand
91+
text payment_method_last4
92+
timestamp created_at
93+
timestamp updated_at
94+
}
95+
96+
purchases {
97+
uuid id PK
98+
uuid user_id FK
99+
text stripe_price_id
100+
text stripe_session_id
101+
text product_name
102+
int amount
103+
text currency
61104
timestamp created_at
62105
timestamp updated_at
63106
}
@@ -87,19 +130,20 @@ erDiagram
87130
timestamp updated_at
88131
}
89132
90-
extra_questions {
133+
form_questions {
91134
uuid id PK
92-
uuid service_id FK
93-
extra_question_type type
135+
uuid form_id FK
136+
form_question_type type
94137
text prompt
95138
jsonb options
139+
int sort_order
96140
timestamp created_at
97141
timestamp updated_at
98142
}
99143
100-
extra_question_answers {
144+
form_question_answers {
101145
uuid id PK
102-
uuid extra_question_id FK
146+
uuid form_question_id FK
103147
uuid child_id FK
104148
text answer "text[]"
105149
timestamp created_at
@@ -108,24 +152,37 @@ erDiagram
108152
109153
profiles ||--o{ service_bookings : "books"
110154
services ||--o{ service_bookings : "booked via"
155+
children |o--o{ service_bookings : "registered for"
111156
profiles ||--o{ coaching_sessions : "coaches"
112157
profiles ||--o{ coaching_sessions : "attends"
113158
services ||--o{ coaching_sessions : "fulfilled by"
159+
children |o--o{ coaching_sessions : "registered for"
114160
profiles ||--o{ children : "parent of"
115161
children ||--o{ emergency_contacts : "has"
116-
services ||--o{ extra_questions : "defines"
117-
extra_questions ||--o{ extra_question_answers : "answered via"
118-
children ||--o{ extra_question_answers : "submits"
162+
profiles ||--o| subscriptions : "has"
163+
profiles ||--o{ purchases : "makes"
164+
profiles |o--o{ services : "coaches"
165+
forms ||--o{ form_questions : "contains"
166+
services }o--o| forms : "uses"
167+
form_questions ||--o{ form_question_answers : "answered via"
168+
children ||--o{ form_question_answers : "submits"
119169
```
120170

171+
## Indexes
172+
173+
| Table | Index | Type | Condition |
174+
|---|---|---|---|
175+
| `service_bookings` | `service_bookings_service_id_child_id_idx` | Unique (partial) | `WHERE child_id IS NOT NULL` — prevents the same child from registering for the same program twice |
176+
121177
## Enums
122178

123179
| Enum | Values |
124180
|---|---|
125181
| `role` | `user`, `admin`, `coach` |
126-
| `service_type` | `coaching_session`, `booking` |
127-
| `booking_status` | `pending`, `confirmed`, `cancelled` |
182+
| `service_type` | `private_lessons`, `programs` |
183+
| `service_status` | `active`, `disabled`, `archived`, `deleted` |
184+
| `booking_status` | `awaiting_payment`, `pending`, `confirmed`, `cancelled` |
185+
| `session_status` | `awaiting_payment`, `pending`, `confirmed`, `cancelled`, `completed` |
128186
| `webinar_tier` | `free`, `premium` |
129-
| `session_status` | `pending`, `confirmed`, `cancelled`, `completed` |
130187
| `gender` | `male`, `female`, `prefer_not_to_say` |
131-
| `extra_question_type` | `text`, `multiple_choices`, `checkboxes`, `user_agreement` |
188+
| `form_question_type` | `text`, `multiple_choices`, `checkboxes`, `user_agreement` |

0 commit comments

Comments
 (0)