diff --git a/api/drizzle/20260226025302_medical_king_bedlam/migration.sql b/api/drizzle/20260226025302_medical_king_bedlam/migration.sql
new file mode 100644
index 00000000..4f369c2f
--- /dev/null
+++ b/api/drizzle/20260226025302_medical_king_bedlam/migration.sql
@@ -0,0 +1 @@
+ALTER TABLE "beep" ALTER COLUMN "beeper_id" DROP NOT NULL;
\ No newline at end of file
diff --git a/api/drizzle/20260226025302_medical_king_bedlam/snapshot.json b/api/drizzle/20260226025302_medical_king_bedlam/snapshot.json
new file mode 100644
index 00000000..c29e0895
--- /dev/null
+++ b/api/drizzle/20260226025302_medical_king_bedlam/snapshot.json
@@ -0,0 +1,1685 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "e1034aaf-9976-4087-91b3-7d97b341545a",
+ "prevIds": [
+ "9b17fd7b-d6ec-4d5b-bb51-7bb621d0f0aa"
+ ],
+ "ddl": [
+ {
+ "values": [
+ "canceled",
+ "denied",
+ "waiting",
+ "accepted",
+ "on_the_way",
+ "here",
+ "in_progress",
+ "complete"
+ ],
+ "name": "beep_status",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "top_of_beeper_list_1_hour",
+ "top_of_beeper_list_2_hours",
+ "top_of_beeper_list_3_hours"
+ ],
+ "name": "payment_product",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "play_store",
+ "app_store"
+ ],
+ "name": "payment_store",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "sha256",
+ "bcrypt"
+ ],
+ "name": "user_password_type",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "user",
+ "admin"
+ ],
+ "name": "user_role",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "beep",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "car",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "feedback",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "forgot_password",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "payment",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "rating",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "report",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "token",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "user",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "verify_email",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "beeper_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rider_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "origin",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "destination",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "group_size",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "start",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "end",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "beep_status",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'waiting'",
+ "generated": null,
+ "identity": null,
+ "name": "status",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "make",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "model",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "color",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "photo",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "year",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "default",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "updated",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "message",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "forgot_password"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "forgot_password"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "time",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "forgot_password"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "store_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "payment_product",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "product_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "numeric",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "price",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "payment_store",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "store",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rater_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rated_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "stars",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "message",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "timestamp",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "beep_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "reporter_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "reported_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "handled_by_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "reason",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "notes",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "timestamp",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "handled",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "beep_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "token"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tokenid",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "token"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "token"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "first",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "last",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "username",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "phone",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "venmo",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "cashapp",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "password",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "user_password_type",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'bcrypt'",
+ "generated": null,
+ "identity": null,
+ "name": "password_type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "is_beeping",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "is_email_verified",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "is_student",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "4",
+ "generated": null,
+ "identity": null,
+ "name": "group_rate",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "3",
+ "generated": null,
+ "identity": null,
+ "name": "singles_rate",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "4",
+ "generated": null,
+ "identity": null,
+ "name": "capacity",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "0",
+ "generated": null,
+ "identity": null,
+ "name": "queue_size",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "numeric",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rating",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "user_role",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'user'",
+ "generated": null,
+ "identity": null,
+ "name": "role",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "push_token",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "photo",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "geometry",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "location",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "time",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "beeper_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "beeper_id_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "beeper_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "rider_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "beeper_id_rider_id_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "rider_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "rider_id_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "start",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "start_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "status",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "status_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "is_beeping",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "is_beeping_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "beeper_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "beep_beeper_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "rider_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "beep_rider_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "car_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "feedback_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "forgot_password_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "forgot_password"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "payment_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "rater_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "rating_rater_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "rated_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "rating_rated_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "beep_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "beep",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "rating_beep_id_beep_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "reporter_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "report_reporter_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "reported_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "report_reported_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "handled_by_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "SET NULL",
+ "name": "report_handled_by_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "beep_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "beep",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "SET NULL",
+ "name": "report_beep_id_beep_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "token_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "token"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "verify_email_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "beep_pkey",
+ "schema": "public",
+ "table": "beep",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "car_pkey",
+ "schema": "public",
+ "table": "car",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "feedback_pkey",
+ "schema": "public",
+ "table": "feedback",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "forgot_password_pkey",
+ "schema": "public",
+ "table": "forgot_password",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "payment_pkey",
+ "schema": "public",
+ "table": "payment",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "rating_pkey",
+ "schema": "public",
+ "table": "rating",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "report_pkey",
+ "schema": "public",
+ "table": "report",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "token_pkey",
+ "schema": "public",
+ "table": "token",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "user_pkey",
+ "schema": "public",
+ "table": "user",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "verify_email_pkey",
+ "schema": "public",
+ "table": "verify_email",
+ "entityType": "pks"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ "rater_id",
+ "beep_id"
+ ],
+ "nullsNotDistinct": false,
+ "name": "rating_beep_id_rater_id_unique",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ "username"
+ ],
+ "nullsNotDistinct": false,
+ "name": "user_username_unique",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ "email"
+ ],
+ "nullsNotDistinct": false,
+ "name": "user_email_unique",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "user"
+ }
+ ],
+ "renames": []
+}
\ No newline at end of file
diff --git a/api/drizzle/relations.ts b/api/drizzle/relations.ts
index 5efc1666..03fe5670 100644
--- a/api/drizzle/relations.ts
+++ b/api/drizzle/relations.ts
@@ -118,7 +118,7 @@ export const relations = defineRelations(
from: r.beep.beeper_id,
to: r.user.id,
alias: "beeper",
- optional: false,
+ optional: true,
}),
rider: r.one.user({
from: r.beep.rider_id,
diff --git a/api/drizzle/schema.ts b/api/drizzle/schema.ts
index ecf7bae0..9b2d10c6 100644
--- a/api/drizzle/schema.ts
+++ b/api/drizzle/schema.ts
@@ -173,7 +173,6 @@ export const beep = pgTable(
{
id: varchar("id", { length: 255 }).primaryKey().notNull(),
beeper_id: varchar("beeper_id", { length: 255 })
- .notNull()
.references(() => user.id, { onUpdate: "cascade", onDelete: "cascade" }),
rider_id: varchar("rider_id", { length: 255 })
.notNull()
diff --git a/api/src/logic/beep.ts b/api/src/logic/beep.ts
index aa0b2991..2d0d7e2b 100644
--- a/api/src/logic/beep.ts
+++ b/api/src/logic/beep.ts
@@ -83,7 +83,7 @@ export async function getRidersCurrentRide(userId: string) {
return null;
}
- const queue = await getBeeperQueue(beep.beeper_id)
+ const queue = beep.beeper_id ? await getBeeperQueue(beep.beeper_id) : [];
return { ...beep, ...getDerivedRiderFields(beep, queue) };
}
diff --git a/api/src/routers/rider.ts b/api/src/routers/rider.ts
index 49694e74..3e17eb4a 100644
--- a/api/src/routers/rider.ts
+++ b/api/src/routers/rider.ts
@@ -91,7 +91,7 @@ export const riderRouter = router({
.use(withLock)
.input(
z.object({
- beeperId: z.string(),
+ beeperId: z.string().optional(),
origin: z.string(),
destination: z.string(),
groupSize: z.number().min(1).max(25),
@@ -117,35 +117,94 @@ export const riderRouter = router({
});
}
- const beeper = await db.query.user.findFirst({
- where: { id: input.beeperId },
+ const currentRide = await db.query.beep.findFirst({
+ where: { AND: [{ rider_id: ctx.user.id }, inProgressBeepNew] }
});
- const queue = await getBeeperQueue(input.beeperId);
-
- if (!beeper) {
+ if (currentRide) {
throw new TRPCError({
- code: "NOT_FOUND",
- message: "Beeper not found",
+ code: "BAD_REQUEST",
+ message: "You are already in an active beep. You can't start another beep until your current one is done."
});
}
- if (!beeper.isBeeping) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "That user is not beeping. Maybe they stopped beeping.",
+ if (input.beeperId) {
+ const beeper = await db.query.user.findFirst({
+ where: { id: input.beeperId },
});
- }
- if (queue.some((beep) => beep.rider_id === ctx.user.id)) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "You are already in that beeper's queue.",
+ const queue = await getBeeperQueue(input.beeperId);
+
+ if (!beeper) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Beeper not found",
+ });
+ }
+
+ if (!beeper.isBeeping) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "That user is not beeping. Maybe they stopped beeping.",
+ });
+ }
+
+ if (queue.some((beep) => beep.rider_id === ctx.user.id)) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "You are already in that beeper's queue.",
+ });
+ }
+
+
+ const newBeep = {
+ beeper_id: beeper.id,
+ rider_id: ctx.user.id,
+ destination: input.destination,
+ origin: input.origin,
+ groupSize: input.groupSize,
+ id: crypto.randomUUID(),
+ start: new Date(),
+ status: "waiting",
+ end: null,
+ } as const;
+
+
+ await db.insert(beep).values(newBeep);
+
+ queue.push({
+ ...newBeep,
+ rider: ctx.user,
+ beeper,
});
+
+ pubSub.publish("queue", beeper.id, { queue });
+
+ for (const beep of queue) {
+ pubSub.publish("ride", beep.rider_id, {
+ ride: { ...beep, ...getDerivedRiderFields(beep, queue) },
+ });
+ }
+
+ if (beeper.pushToken) {
+ sendNotification({
+ to: beeper.pushToken,
+ title: `${ctx.user.first} ${ctx.user.last} has entered your queue 🚕`,
+ body: "Please accept or deny this rider.",
+ categoryId: "newbeep",
+ data: { id: newBeep.id },
+ });
+ }
+
+ return {
+ ...newBeep,
+ ...getDerivedRiderFields(newBeep, queue),
+ beeper,
+ };
}
const newBeep = {
- beeper_id: beeper.id,
+ beeper_id: null,
rider_id: ctx.user.id,
destination: input.destination,
origin: input.origin,
@@ -156,48 +215,14 @@ export const riderRouter = router({
end: null,
} as const;
- const currentRide = await db.query.beep.findFirst({
- where: { AND: [{ rider_id: ctx.user.id }, inProgressBeepNew] }
- });
-
- if (currentRide) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "You are already in an active beep. You can't start another beep until your current one is done."
- });
- }
await db.insert(beep).values(newBeep);
- queue.push({
- ...newBeep,
- rider: ctx.user,
- beeper,
- });
-
- pubSub.publish("queue", beeper.id, { queue });
+ const ride = { ...newBeep, ...getDerivedRiderFields(newBeep, []), beeper: null };
- for (const beep of queue) {
- pubSub.publish("ride", beep.rider_id, {
- ride: { ...beep, ...getDerivedRiderFields(beep, queue) },
- });
- }
-
- if (beeper.pushToken) {
- sendNotification({
- to: beeper.pushToken,
- title: `${ctx.user.first} ${ctx.user.last} has entered your queue 🚕`,
- body: "Please accept or deny this rider.",
- categoryId: "newbeep",
- data: { id: newBeep.id },
- });
- }
+ pubSub.publish("ride", ctx.user.id, { ride });
- return {
- ...newBeep,
- ...getDerivedRiderFields(newBeep, queue),
- beeper,
- };
+ return ride;
}),
currentRide: authedProcedure
.input(z.string().optional())
@@ -206,7 +231,7 @@ export const riderRouter = router({
const userId = input ?? ctx.user.id;
if (ctx.user.role === 'user' && userId !== ctx.user.id) {
- throw new TRPCError({ code: "FORBIDDEN", message: "You must be an admin to view the current ride of another user"});
+ throw new TRPCError({ code: "FORBIDDEN", message: "You must be an admin to view the current ride of another user" });
}
return getRidersCurrentRide(userId);
@@ -354,69 +379,78 @@ export const riderRouter = router({
}
}),
leaveQueue: authedProcedure
- .input(
- z.object({
- beeperId: z.string(),
- }),
- )
.mutation(async ({ ctx, input }) => {
- const beeper = await db.query.user.findFirst({
- where: { id: input.beeperId },
+ const ride = await db.query.beep.findFirst({
+ where: { AND: [{ rider_id: ctx.user.id }, inProgressBeepNew] },
});
- if (!beeper) {
+ if (!ride) {
throw new TRPCError({
code: "NOT_FOUND",
- message: "Beeper not found.",
+ message: "You are not in a beep.",
});
}
- let queue = await getBeeperQueue(input.beeperId);
+ if (ride.beeper_id) {
- if (!beeper) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "Beeper not found.",
- });
- }
+ let queue = await getBeeperQueue(ride.beeper_id);
- const entry = queue.find((beep) => beep.rider.id === ctx.user.id);
+ const beeper = await db.query.user.findFirst({
+ where: { id: ride.beeper_id }
+ })
- if (!entry) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "You are not in that beepers queue.",
- });
- }
+ if (!beeper) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Beeper not found.",
+ });
+ }
- if (beeper.pushToken) {
- sendNotification({
- to: beeper.pushToken,
- title: `${ctx.user.first} ${ctx.user.last} left your queue 🥹`,
- body: "They decided they did not want a beep from you!",
- });
- }
+ const entry = queue.find((beep) => beep.rider.id === ctx.user.id);
- await db
- .update(beep)
- .set({ status: "canceled", end: new Date() })
- .where(eq(beep.id, entry.id));
+ if (!entry) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "You are not in that beepers queue.",
+ });
+ }
- queue = queue.filter((beep) => beep.id !== entry.id);
+ if (beeper.pushToken) {
+ sendNotification({
+ to: beeper.pushToken,
+ title: `${ctx.user.first} ${ctx.user.last} left your queue 🥹`,
+ body: "They decided they did not want a beep from you!",
+ });
+ }
- pubSub.publish("ride", ctx.user.id, { ride: null });
- pubSub.publish("queue", beeper.id, { queue });
+ await db
+ .update(beep)
+ .set({ status: "canceled", end: new Date() })
+ .where(eq(beep.id, entry.id));
- for (const beep of queue) {
- pubSub.publish("ride", beep.rider_id, {
- ride: { ...beep, ...getDerivedRiderFields(beep, queue) },
- });
- }
+ queue = queue.filter((beep) => beep.id !== entry.id);
- await db
- .update(user)
- .set({ queueSize: getQueueSize(queue) })
- .where(eq(user.id, beeper.id));
+ pubSub.publish("ride", ctx.user.id, { ride: null });
+ pubSub.publish("queue", beeper.id, { queue });
+
+ for (const beep of queue) {
+ pubSub.publish("ride", beep.rider_id, {
+ ride: { ...beep, ...getDerivedRiderFields(beep, queue) },
+ });
+ }
+
+ await db
+ .update(user)
+ .set({ queueSize: getQueueSize(queue) })
+ .where(eq(user.id, beeper.id));
+ } else {
+ await db
+ .update(beep)
+ .set({ status: "canceled", end: new Date() })
+ .where(eq(beep.id, ride.id));
+
+ pubSub.publish("ride", ctx.user.id, { ride: null });
+ }
return true;
}),
diff --git a/api/src/schemas/beep.ts b/api/src/schemas/beep.ts
index baa43f2a..3492ee19 100644
--- a/api/src/schemas/beep.ts
+++ b/api/src/schemas/beep.ts
@@ -18,7 +18,7 @@ export const rideResponseSchema = z.object({
groupRate: z.number(),
singlesRate: z.number(),
photo: z.string().nullable(),
- }),
+ }).nullable(),
position: z.number(),
queue: z.array(z.object({ status: z.enum(beepStatuses) })),
riders_waiting: z.number()
diff --git a/app/app/(app)/(drawer)/index.tsx b/app/app/(app)/(drawer)/index.tsx
index 54cbcf54..ca5f3880 100644
--- a/app/app/(app)/(drawer)/index.tsx
+++ b/app/app/(app)/(drawer)/index.tsx
@@ -1,15 +1,15 @@
import React, { useEffect } from "react";
import { useTRPC } from "@/utils/trpc";
-import { skipToken, useQuery } from "@tanstack/react-query";
+import { skipToken, useMutation, useQuery } from "@tanstack/react-query";
import { useSubscription } from "@trpc/tanstack-react-query";
import { useQueryClient } from "@tanstack/react-query";
import { RideDetails } from "../../../components/RideDetails";
import { BottomSheet } from "@/components/BottomSheet";
-import { View } from "react-native";
+import { Switch, View } from "react-native";
import { RideMap } from "../../../components/RideMap";
import { BottomSheetView } from "@gorhom/bottom-sheet";
import { SplashScreen, useLocalSearchParams, useRouter } from "expo-router";
-import { Controller, useForm } from "react-hook-form";
+import { Controller, useForm, useWatch } from "react-hook-form";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
import { Label } from "@/components/Label";
import { Text } from "@/components/Text";
@@ -18,10 +18,12 @@ import { LocationInput } from "@/components/LocationInput";
import { Button } from "@/components/Button";
import { BeepersMap } from "@/components/BeepersMap";
import { RateLastBeeper } from "@/components/RateLastBeeper";
+import { useLocation } from "@/utils/location";
export default function MainFindBeepScreen() {
const trpc = useTRPC();
const queryClient = useQueryClient();
+ const { getLocation } = useLocation(false);
const { data: beep } = useQuery(trpc.rider.currentRide.queryOptions());
@@ -47,7 +49,7 @@ export default function MainFindBeepScreen() {
const { data: beepersLocation } = useSubscription(
trpc.rider.beeperLocationUpdates.subscriptionOptions(
- beep ? beep.beeper.id : skipToken,
+ beep?.beeper ? beep.beeper.id : skipToken,
{
enabled: isAcceptedBeep,
},
@@ -69,17 +71,42 @@ export default function MainFindBeepScreen() {
control,
handleSubmit,
setFocus,
+ watch,
formState: { errors },
} = useForm({
values: {
groupSize: params.groupSize ? String(params.groupSize) : "",
origin: params.origin ?? "",
destination: params.destination ?? "",
+ chooseBeeper: true,
},
});
- const findBeep = handleSubmit((values) => {
- router.navigate({ pathname: '/ride/pick', params: values });
+ const { mutate: startBeep, isPending } = useMutation(
+ trpc.rider.startBeep.mutationOptions({
+ onSuccess(data) {
+ queryClient.setQueryData(trpc.rider.currentRide.queryKey(), data);
+ },
+ onError(error) {
+ alert(error.message);
+ },
+ }),
+ );
+
+ const chooseBeeper = watch('chooseBeeper');
+
+ const findBeep = handleSubmit(async ({ chooseBeeper, ...values }) => {
+ if (chooseBeeper) {
+ router.navigate({ pathname: '/ride/pick', params: values });
+ } else {
+ const location = await getLocation();
+
+ startBeep({
+ ...values,
+ groupSize: Number(values.groupSize),
+ ...location.coords,
+ });
+ }
});
if (!beep) {
@@ -158,7 +185,17 @@ export default function MainFindBeepScreen() {
/>
{errors.destination?.message}
-
+
+ Choose a beeper
+ (
+
+ )}
+ />
+
+
diff --git a/app/components/Beep.tsx b/app/components/Beep.tsx
index 3b849186..2fb95b48 100644
--- a/app/components/Beep.tsx
+++ b/app/components/Beep.tsx
@@ -26,11 +26,11 @@ export function Beep({ item }: Props) {
const otherUser = user?.id === item.rider.id ? item.beeper : item.rider;
const isRider = user?.id === item.rider.id;
- const isBeeper = user?.id === item.beeper.id;
+ const isBeeper = item.beeper !== null && user?.id === item.beeper.id;
const myRating = item.ratings.find((r) => r.rater_id === user?.id);
const otherUsersRating = item.ratings.find(
- (r) => r.rater_id === otherUser.id,
+ (r) => otherUser && r.rater_id === otherUser.id,
);
const isAcceptedOrComplete =
diff --git a/app/components/RideDetails.tsx b/app/components/RideDetails.tsx
index 401bc810..d3fb4bad 100644
--- a/app/components/RideDetails.tsx
+++ b/app/components/RideDetails.tsx
@@ -33,7 +33,7 @@ export function RideDetails(props: Props) {
const { data: car } = useQuery(
trpc.user.getUsersDefaultCar.queryOptions(
- beep ? beep.beeper.id : skipToken,
+ beep?.beeper ? beep.beeper.id : skipToken,
{ enabled: isAcceptedBeep },
),
);
@@ -42,6 +42,34 @@ export function RideDetails(props: Props) {
return null;
}
+ if (beep && !beep.beeper) {
+ return (
+
+ Waiting on a beeper to claim your ride request.
+
+ Pick Up
+ {beep.origin}
+
+
+ Destination
+ {beep.destination}
+
+
+ Number of Riders
+ {beep.groupSize}
+
+
+ );
+ }
+
return (
mutate({ beeperId: beep.beeper.id }),
+ onPress: () => mutate(),
style: "destructive",
},
],
{ cancelable: true },
);
} else {
- mutate({ beeperId: beep.beeper.id });
+ mutate();
}
};
@@ -83,12 +83,13 @@ export function RideMenu() {
}
options={[
+ ...(beep.beeper ? [
{
title: "Contact",
show: beep.status !== "waiting",
options: [
- { title: "Call", onClick: () => call(beep.beeper.id) },
- { title: "Text", onClick: () => sms(beep.beeper.id) },
+ { title: "Call", onClick: () => call(beep.beeper!.id) },
+ { title: "Text", onClick: () => sms(beep.beeper!.id) },
],
},
{
@@ -100,10 +101,10 @@ export function RideMenu() {
show: Boolean(beep.beeper.venmo),
onClick: () =>
openVenmo(
- beep.beeper.venmo,
+ beep.beeper!.venmo,
beep.groupSize,
- beep.beeper.groupRate,
- beep.beeper.singlesRate,
+ beep.beeper!.groupRate,
+ beep.beeper!.singlesRate,
"pay",
),
},
@@ -112,14 +113,15 @@ export function RideMenu() {
show: Boolean(beep.beeper.cashapp),
onClick: () =>
openCashApp(
- beep.beeper.cashapp,
+ beep.beeper!.cashapp,
beep.groupSize,
- beep.beeper.groupRate,
- beep.beeper.singlesRate,
+ beep.beeper!.groupRate,
+ beep.beeper!.singlesRate,
),
},
],
},
+ ] : []),
{
title: "Cancel Ride",
destructive: true,
diff --git a/website/src/components/BasicUser.tsx b/website/src/components/BasicUser.tsx
index 9eaf83f1..e6f58105 100644
--- a/website/src/components/BasicUser.tsx
+++ b/website/src/components/BasicUser.tsx
@@ -8,12 +8,23 @@ interface Props {
photo: string | null | undefined;
first: string;
last: string;
- };
+ } | null;
}
export function BasicUser(props: Props) {
const { user } = props;
+ if (!user) {
+ return (
+
+
+ None
+
+
+
+ );
+ }
+
return (
diff --git a/website/src/components/TableCellUser.tsx b/website/src/components/TableCellUser.tsx
index c20482b4..ddea8238 100644
--- a/website/src/components/TableCellUser.tsx
+++ b/website/src/components/TableCellUser.tsx
@@ -4,11 +4,22 @@ import { LinkProps } from '@tanstack/react-router';
import { Link } from './Link';
interface Props {
- user: { first: string, last: string, id: string, photo: string | null };
+ user: { first: string, last: string, id: string, photo: string | null } | null;
linkProps?: Partial;
}
export function TableCellUser(props: Props) {
+ if (!props.user) {
+ return (
+
+
+
+ None
+
+
+ );
+ }
+
return (
diff --git a/website/src/routes/admin/beeps/Beep.tsx b/website/src/routes/admin/beeps/Beep.tsx
index 44bdfd9e..90794174 100644
--- a/website/src/routes/admin/beeps/Beep.tsx
+++ b/website/src/routes/admin/beeps/Beep.tsx
@@ -40,7 +40,7 @@ export function Beep() {
} = useQuery(trpc.beep.beep.queryOptions(beepId));
const { data: beeper } = useSubscription(
- trpc.user.updates.subscriptionOptions(beep ? beep.beeper_id : skipToken)
+ trpc.user.updates.subscriptionOptions(beep?.beeper ? beep.beeper.id : skipToken)
);
const { data: route } = useQuery(
diff --git a/website/src/routes/admin/users/Ride.tsx b/website/src/routes/admin/users/Ride.tsx
index b89937b3..fe0808c7 100644
--- a/website/src/routes/admin/users/Ride.tsx
+++ b/website/src/routes/admin/users/Ride.tsx
@@ -34,7 +34,7 @@ export function Ride() {
);
const { data: beeper } = useSubscription(
- trpc.user.updates.subscriptionOptions(ride ? ride.beeper.id : skipToken)
+ trpc.user.updates.subscriptionOptions(ride?.beeper ? ride.beeper.id : skipToken)
);
const { data: route } = useQuery(