-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathschema.ts
More file actions
347 lines (285 loc) · 11.8 KB
/
schema.ts
File metadata and controls
347 lines (285 loc) · 11.8 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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/**
* database/schema.ts
* Database schema definitions using Drizzle ORM for PostgreSQL
*/
import {
decimal,
integer,
jsonb,
pgEnum,
pgTable,
text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
export const STATUS_ENUM = pgEnum("status", [
"PENDING",
"APPROVED",
"REJECTED",
]);
export const ROLE_ENUM = pgEnum("role", ["USER", "ADMIN"]);
export const EVENT_STATUS_ENUM = pgEnum("event_status", [
"DRAFT", // Event is being created, not visible to users
"PUBLISHED", // Event is live and visible to users
"CANCELLED", // Event has been cancelled
"COMPLETED", // Event has been completed
]);
export const REGISTRATION_STATUS_ENUM = pgEnum("registration_status", [
"REGISTERED", // User has registered for the event
"ATTENDED", // User attended the event (marked by admin)
"NO_SHOW", // User didn't show up
"CANCELLED", // User cancelled their registration
]);
export const ATTENDANCE_STATUS_ENUM = pgEnum("attendance_status", [
"CHECKED_IN",
"CHECKED_OUT",
"INCOMPLETE", // User checked in but never checked out
]);
export const TRANSACTION_TYPE_ENUM = pgEnum("transaction_type", [
"EARNED", // Earned from event
"REDEEMED", // Spent on rewards
"ADJUSTED", // Manual adjustment by admin
]);
export const REWARD_STATUS_ENUM = pgEnum("reward_status", [
"ACTIVE", // Reward is available for redemption
"INACTIVE", // Reward is temporarily unavailable
"ARCHIVED", // Reward has been archived/deleted
]);
export const REDEMPTION_STATUS_ENUM = pgEnum("redemption_status", [
"PENDING", // Redemption requested, awaiting fulfillment
"FULFILLED", // Reward has been given to user
"CANCELLED", // Redemption was cancelled
]);
/* Users table - stores user information */
export const usersTable = pgTable("users", {
id: uuid("id").notNull().primaryKey().defaultRandom(),
fullName: varchar("full_name", { length: 255 }).notNull(),
email: varchar("email", { length: 255 }).notNull().unique(),
passwordHash: text("password_hash").notNull(),
status: STATUS_ENUM("status").notNull().default("PENDING"),
role: ROLE_ENUM("role").notNull().default("USER"),
// ʻĀina Bucks tracking
totalAinaBucksEarned: integer("total_aina_bucks_earned").notNull().default(0),
totalAinaBucksRedeemed: integer("total_aina_bucks_redeemed")
.notNull()
.default(0),
currentAinaBucks: integer("current_aina_bucks").notNull().default(0), // earned - redeemed
totalHoursVolunteered: decimal("total_hours_volunteered", {
precision: 8,
scale: 2,
})
.notNull()
.default("0"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
});
/* Events table - stores all volunteer event data */
export const eventsTable = pgTable("events", {
// Primary Key
id: uuid("id").notNull().primaryKey().defaultRandom(),
// Basic Information
title: varchar("title", { length: 255 }).notNull(),
category: varchar("category", { length: 100 }).notNull(),
description: text("description").notNull(),
imageUrl: text("image_url"), // optional event photo
// Date & Time (stored as strings from form inputs)
date: varchar("date", { length: 50 }).notNull(), // Store as string from form
startTime: varchar("start_time", { length: 50 }).notNull(),
endTime: varchar("end_time", { length: 50 }).notNull(),
// Location
locationName: varchar("location_name", { length: 255 }).notNull(),
address: varchar("address", { length: 255 }).notNull(),
city: varchar("city", { length: 100 }).notNull(),
state: varchar("state", { length: 2 }).notNull(),
zipCode: varchar("zip_code", { length: 10 }).notNull(),
// Volunteers & Rewards
volunteersNeeded: integer("volunteers_needed").notNull(),
duration: decimal("duration", { precision: 4, scale: 2 }).notNull(), // e.g., 4.5 hours
ainaBucks: integer("aina_bucks").notNull(),
bucksPerHour: integer("bucks_per_hour").notNull(),
// Arrays stored as JSONB - Drizzle way to store arrays
whatToBring: jsonb("what_to_bring").$type<string[]>(),
requirements: jsonb("requirements").$type<string[]>(),
// Coordinator
coordinatorName: varchar("coordinator_name", { length: 255 }).notNull(),
coordinatorEmail: varchar("coordinator_email", { length: 255 }).notNull(),
coordinatorPhone: varchar("coordinator_phone", { length: 50 }).notNull(),
// QR Code tokens for check-in/out
checkInToken: uuid("check_in_token").defaultRandom().unique(),
checkOutToken: uuid("check_out_token").defaultRandom().unique(),
// Metadata
// status: EVENT_STATUS_ENUM("status").notNull().default("PUBLISHED"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
});
/* Event Registrations table - links users to events they registered for */
export const eventRegistrationsTable = pgTable("event_registrations", {
// Primary Key
id: uuid("id").notNull().primaryKey().defaultRandom(),
// Foreign Keys
userId: uuid("user_id")
.notNull()
.references(() => usersTable.id, { onDelete: "cascade" }), // Delete registration if user is deleted
eventId: uuid("event_id")
.notNull()
.references(() => eventsTable.id, { onDelete: "cascade" }), // Delete registration if event is deleted
// Registration Status
status: REGISTRATION_STATUS_ENUM("status").notNull().default("REGISTERED"),
// Timestamps
registeredAt: timestamp("registered_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
});
// Track actual attendance with check-in/out times
export const eventAttendanceTable = pgTable("event_attendance", {
id: uuid("id").notNull().primaryKey().defaultRandom(),
// Foreign Keys
userId: uuid("user_id")
.notNull()
.references(() => usersTable.id, { onDelete: "cascade" }),
eventId: uuid("event_id")
.notNull()
.references(() => eventsTable.id, { onDelete: "cascade" }),
registrationId: uuid("registration_id")
.notNull()
.references(() => eventRegistrationsTable.id, { onDelete: "cascade" }),
// Check-in/out times
checkInTime: timestamp("check_in_time", { withTimezone: true }),
checkOutTime: timestamp("check_out_time", { withTimezone: true }),
// Calculated hours worked (set by admin or auto-calculated)
hoursWorked: decimal("hours_worked", { precision: 4, scale: 2 }),
// Status
status: ATTENDANCE_STATUS_ENUM("status").notNull().default("CHECKED_IN"),
// Admin notes (optional)
adminNotes: text("admin_notes"),
// Timestamps
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
});
// Track all ʻĀina Bucks transactions
export const ainaBucksTransactionsTable = pgTable("aina_bucks_transactions", {
id: uuid("id").notNull().primaryKey().defaultRandom(),
// Foreign Keys
userId: uuid("user_id")
.notNull()
.references(() => usersTable.id, { onDelete: "cascade" }),
eventId: uuid("event_id").references(() => eventsTable.id, {
onDelete: "set null",
}), // null if not event-related
attendanceId: uuid("attendance_id").references(
() => eventAttendanceTable.id,
{ onDelete: "set null" },
),
// Transaction details
type: TRANSACTION_TYPE_ENUM("type").notNull(),
amount: integer("amount").notNull(), // Can be negative for redemptions
// Hours worked (for earned transactions)
hoursWorked: decimal("hours_worked", { precision: 4, scale: 2 }),
// Description
description: text("description").notNull(),
// Admin who approved (for earned/adjusted transactions)
approvedBy: uuid("approved_by").references(() => usersTable.id, {
onDelete: "set null",
}),
// Timestamps
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
});
/* Rewards table - stores reward items that users can redeem with ʻĀina Bucks */
export const rewardsTable = pgTable("rewards", {
// Primary Key
id: uuid("id").notNull().primaryKey().defaultRandom(),
// Reward Details
name: varchar("name", { length: 255 }).notNull(), // e.g., "Reusable Water Bottle"
description: text("description").notNull(), // Description of the reward
imageUrl: text("image_url"), // Optional image of the reward
ainaBucksCost: integer("aina_bucks_cost").notNull(), // Cost in ʻĀina Bucks
// Inventory
quantityAvailable: integer("quantity_available").notNull().default(0), // -1 for unlimited
quantityRedeemed: integer("quantity_redeemed").notNull().default(0),
// Status
status: REWARD_STATUS_ENUM("status").notNull().default("ACTIVE"),
// Admin who created the reward
createdBy: uuid("created_by")
.notNull()
.references(() => usersTable.id, { onDelete: "set null" }),
// Timestamps
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
});
/* Reward Redemptions table - tracks when users redeem rewards */
export const rewardRedemptionsTable = pgTable("reward_redemptions", {
// Primary Key
id: uuid("id").notNull().primaryKey().defaultRandom(),
// Foreign Keys
userId: uuid("user_id")
.notNull()
.references(() => usersTable.id, { onDelete: "cascade" }),
rewardId: uuid("reward_id")
.notNull()
.references(() => rewardsTable.id, { onDelete: "cascade" }),
transactionId: uuid("transaction_id")
.notNull()
.references(() => ainaBucksTransactionsTable.id, { onDelete: "cascade" }),
// Redemption Details
ainaBucksSpent: integer("aina_bucks_spent").notNull(),
quantity: integer("quantity").notNull().default(1),
// Status
status: REDEMPTION_STATUS_ENUM("status").notNull().default("PENDING"),
// Admin who fulfilled the redemption
fulfilledBy: uuid("fulfilled_by").references(() => usersTable.id, {
onDelete: "set null",
}),
fulfilledAt: timestamp("fulfilled_at", { withTimezone: true }),
// Notes
adminNotes: text("admin_notes"),
// Timestamps
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
});
/* Type Exports */
export type User = typeof usersTable.$inferSelect; // For reading user data
export type NewUser = typeof usersTable.$inferInsert; // For inserting new users
export type Event = typeof eventsTable.$inferSelect; // For reading event data
export type NewEvent = typeof eventsTable.$inferInsert; // For inserting new events
export type EventRegistration = typeof eventRegistrationsTable.$inferSelect; // For reading registration data
export type NewEventRegistration = typeof eventRegistrationsTable.$inferInsert; // For inserting new registrations
export type EventAttendance = typeof eventAttendanceTable.$inferSelect; // For reading attendance data
export type NewEventAttendance = typeof eventAttendanceTable.$inferInsert; // For inserting new attendance records
export type AinaBucksTransaction =
typeof ainaBucksTransactionsTable.$inferSelect; // For reading transaction data
export type NewAinaBucksTransaction =
typeof ainaBucksTransactionsTable.$inferInsert; // For inserting new transactions
export type Reward = typeof rewardsTable.$inferSelect; // For reading reward data
export type NewReward = typeof rewardsTable.$inferInsert; // For inserting new rewards
export type RewardRedemption = typeof rewardRedemptionsTable.$inferSelect; // For reading redemption data
export type NewRewardRedemption = typeof rewardRedemptionsTable.$inferInsert; // For inserting new redemptions
/**
* Extended Event type that includes the current registration count
* Used in components that need to display real-time registration data
*/
export type EventWithRegistrations = Event & {
volunteersRegistered: number;
};