refactor(db): separate workout plans from completed sessions
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
CREATE TABLE `workout_sessions` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`workout_id` text,
|
||||||
|
`name` text,
|
||||||
|
`pool_length` real DEFAULT 25 NOT NULL,
|
||||||
|
`started_at` text NOT NULL,
|
||||||
|
`finished_at` text,
|
||||||
|
`total_distance` integer,
|
||||||
|
`total_duration` real,
|
||||||
|
`calories` real,
|
||||||
|
`avg_heart_rate` integer,
|
||||||
|
`source` text DEFAULT 'manual' NOT NULL,
|
||||||
|
FOREIGN KEY (`workout_id`) REFERENCES `workouts`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `workout_sessions` (`id`, `workout_id`, `name`, `pool_length`, `started_at`, `finished_at`, `total_distance`, `total_duration`, `calories`, `avg_heart_rate`, `source`)
|
||||||
|
SELECT `id`, `id`, `name`, `pool_length`, `started_at`, `finished_at`, `total_distance`, `total_duration`, `calories`, `avg_heart_rate`, `source`
|
||||||
|
FROM `workouts`
|
||||||
|
WHERE `finished_at` IS NOT NULL;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `laps` RENAME COLUMN `workout_id` TO `session_id`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `workouts` DROP COLUMN `started_at`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `workouts` DROP COLUMN `finished_at`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `workouts` DROP COLUMN `total_distance`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `workouts` DROP COLUMN `total_duration`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `workouts` DROP COLUMN `calories`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `workouts` DROP COLUMN `avg_heart_rate`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `workouts` DROP COLUMN `source`;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_laps` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`session_id` text NOT NULL,
|
||||||
|
`set_position` integer NOT NULL,
|
||||||
|
`rep_number` integer NOT NULL,
|
||||||
|
`distance` integer NOT NULL,
|
||||||
|
`duration` real NOT NULL,
|
||||||
|
`stroke_count` integer,
|
||||||
|
`heart_rate` integer,
|
||||||
|
`swolf` real,
|
||||||
|
FOREIGN KEY (`session_id`) REFERENCES `workout_sessions`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_laps`("id", "session_id", "set_position", "rep_number", "distance", "duration", "stroke_count", "heart_rate", "swolf") SELECT "id", "session_id", "set_position", "rep_number", "distance", "duration", "stroke_count", "heart_rate", "swolf" FROM `laps`;--> statement-breakpoint
|
||||||
|
DROP TABLE `laps`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_laps` RENAME TO `laps`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_workout_sessions` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`workout_id` text,
|
||||||
|
`name` text,
|
||||||
|
`pool_length` real DEFAULT 25 NOT NULL,
|
||||||
|
`started_at` text NOT NULL,
|
||||||
|
`finished_at` text,
|
||||||
|
`total_distance` integer,
|
||||||
|
`total_duration` real,
|
||||||
|
`calories` real,
|
||||||
|
`avg_heart_rate` integer,
|
||||||
|
`source` text DEFAULT 'manual' NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_workout_sessions`("id", "workout_id", "name", "pool_length", "started_at", "finished_at", "total_distance", "total_duration", "calories", "avg_heart_rate", "source") SELECT "id", "workout_id", "name", "pool_length", "started_at", "finished_at", "total_distance", "total_duration", "calories", "avg_heart_rate", "source" FROM `workout_sessions`;--> statement-breakpoint
|
||||||
|
DROP TABLE `workout_sessions`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_workout_sessions` RENAME TO `workout_sessions`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `workouts` ADD `created_at` text NOT NULL;
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "1a621e4a-ea8a-4479-a98c-0f02fc8e14e3",
|
||||||
|
"prevId": "a9688cc9-9314-4d59-9b68-b7e202f3e3ff",
|
||||||
|
"tables": {
|
||||||
|
"laps": {
|
||||||
|
"name": "laps",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"name": "workout_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"set_position": {
|
||||||
|
"name": "set_position",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rep_number": {
|
||||||
|
"name": "rep_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"distance": {
|
||||||
|
"name": "distance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"name": "duration",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"stroke_count": {
|
||||||
|
"name": "stroke_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"heart_rate": {
|
||||||
|
"name": "heart_rate",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"swolf": {
|
||||||
|
"name": "swolf",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"laps_workout_id_workouts_id_fk": {
|
||||||
|
"name": "laps_session_id_workout_sessions_id_fk",
|
||||||
|
"tableFrom": "laps",
|
||||||
|
"tableTo": "workout_sessions",
|
||||||
|
"columnsFrom": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"sets": {
|
||||||
|
"name": "sets",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workout_id": {
|
||||||
|
"name": "workout_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"name": "position",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"stroke": {
|
||||||
|
"name": "stroke",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"distance_per_rep": {
|
||||||
|
"name": "distance_per_rep",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rep_count": {
|
||||||
|
"name": "rep_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rest_type": {
|
||||||
|
"name": "rest_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'fixed_rest'"
|
||||||
|
},
|
||||||
|
"rest_duration": {
|
||||||
|
"name": "rest_duration",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"target_time": {
|
||||||
|
"name": "target_time",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sets_workout_id_workouts_id_fk": {
|
||||||
|
"name": "sets_workout_id_workouts_id_fk",
|
||||||
|
"tableFrom": "sets",
|
||||||
|
"tableTo": "workouts",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workout_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"name": "templates",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pool_length": {
|
||||||
|
"name": "pool_length",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 25
|
||||||
|
},
|
||||||
|
"json_data": {
|
||||||
|
"name": "json_data",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_favorite": {
|
||||||
|
"name": "is_favorite",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"workouts": {
|
||||||
|
"name": "workouts",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"template_id": {
|
||||||
|
"name": "template_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pool_length": {
|
||||||
|
"name": "pool_length",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"workout_sessions": {
|
||||||
|
"name": "workout_sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workout_id": {
|
||||||
|
"name": "workout_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pool_length": {
|
||||||
|
"name": "pool_length",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 25
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"finished_at": {
|
||||||
|
"name": "finished_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"total_distance": {
|
||||||
|
"name": "total_distance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"total_duration": {
|
||||||
|
"name": "total_duration",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"calories": {
|
||||||
|
"name": "calories",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"avg_heart_rate": {
|
||||||
|
"name": "avg_heart_rate",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'manual'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"workout_sessions_workout_id_workouts_id_fk": {
|
||||||
|
"name": "workout_sessions_workout_id_workouts_id_fk",
|
||||||
|
"tableFrom": "workout_sessions",
|
||||||
|
"tableTo": "workouts",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workout_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "3e3e9dde-1e8c-4a6a-94ac-b5a750829d2b",
|
||||||
|
"prevId": "1a621e4a-ea8a-4479-a98c-0f02fc8e14e3",
|
||||||
|
"tables": {
|
||||||
|
"laps": {
|
||||||
|
"name": "laps",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"name": "session_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"set_position": {
|
||||||
|
"name": "set_position",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rep_number": {
|
||||||
|
"name": "rep_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"distance": {
|
||||||
|
"name": "distance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"name": "duration",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"stroke_count": {
|
||||||
|
"name": "stroke_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"heart_rate": {
|
||||||
|
"name": "heart_rate",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"swolf": {
|
||||||
|
"name": "swolf",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"laps_session_id_workout_sessions_id_fk": {
|
||||||
|
"name": "laps_session_id_workout_sessions_id_fk",
|
||||||
|
"tableFrom": "laps",
|
||||||
|
"tableTo": "workout_sessions",
|
||||||
|
"columnsFrom": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"sets": {
|
||||||
|
"name": "sets",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workout_id": {
|
||||||
|
"name": "workout_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"name": "position",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"stroke": {
|
||||||
|
"name": "stroke",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"distance_per_rep": {
|
||||||
|
"name": "distance_per_rep",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rep_count": {
|
||||||
|
"name": "rep_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rest_type": {
|
||||||
|
"name": "rest_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'fixed_rest'"
|
||||||
|
},
|
||||||
|
"rest_duration": {
|
||||||
|
"name": "rest_duration",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"target_time": {
|
||||||
|
"name": "target_time",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sets_workout_id_workouts_id_fk": {
|
||||||
|
"name": "sets_workout_id_workouts_id_fk",
|
||||||
|
"tableFrom": "sets",
|
||||||
|
"tableTo": "workouts",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workout_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"name": "templates",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pool_length": {
|
||||||
|
"name": "pool_length",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 25
|
||||||
|
},
|
||||||
|
"json_data": {
|
||||||
|
"name": "json_data",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_favorite": {
|
||||||
|
"name": "is_favorite",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"workout_sessions": {
|
||||||
|
"name": "workout_sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workout_id": {
|
||||||
|
"name": "workout_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pool_length": {
|
||||||
|
"name": "pool_length",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 25
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"finished_at": {
|
||||||
|
"name": "finished_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"total_distance": {
|
||||||
|
"name": "total_distance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"total_duration": {
|
||||||
|
"name": "total_duration",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"calories": {
|
||||||
|
"name": "calories",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"avg_heart_rate": {
|
||||||
|
"name": "avg_heart_rate",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'manual'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"workouts": {
|
||||||
|
"name": "workouts",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"template_id": {
|
||||||
|
"name": "template_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pool_length": {
|
||||||
|
"name": "pool_length",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 25
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,20 @@
|
|||||||
"when": 1785854381700,
|
"when": 1785854381700,
|
||||||
"tag": "0000_mushy_groot",
|
"tag": "0000_mushy_groot",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1785940781700,
|
||||||
|
"tag": "0001_workout_sessions",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1786525398056,
|
||||||
|
"tag": "0002_gifted_annihilus",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+81
-2
@@ -54,9 +54,88 @@ CREATE TABLE \`workouts\` (
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const m0001 = `
|
||||||
|
CREATE TABLE \`workout_sessions\` (
|
||||||
|
\`id\` text PRIMARY KEY NOT NULL,
|
||||||
|
\`workout_id\` text,
|
||||||
|
\`name\` text,
|
||||||
|
\`pool_length\` real DEFAULT 25 NOT NULL,
|
||||||
|
\`started_at\` text NOT NULL,
|
||||||
|
\`finished_at\` text,
|
||||||
|
\`total_distance\` integer,
|
||||||
|
\`total_duration\` real,
|
||||||
|
\`calories\` real,
|
||||||
|
\`avg_heart_rate\` integer,
|
||||||
|
\`source\` text DEFAULT 'manual' NOT NULL,
|
||||||
|
FOREIGN KEY (\`workout_id\`) REFERENCES \`workouts\`(\`id\`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO \`workout_sessions\` (\`id\`, \`workout_id\`, \`name\`, \`pool_length\`, \`started_at\`, \`finished_at\`, \`total_distance\`, \`total_duration\`, \`calories\`, \`avg_heart_rate\`, \`source\`)
|
||||||
|
SELECT \`id\`, \`id\`, \`name\`, \`pool_length\`, \`started_at\`, \`finished_at\`, \`total_distance\`, \`total_duration\`, \`calories\`, \`avg_heart_rate\`, \`source\`
|
||||||
|
FROM \`workouts\`
|
||||||
|
WHERE \`finished_at\` IS NOT NULL;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE \`laps\` RENAME COLUMN \`workout_id\` TO \`session_id\`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE \`workouts\` DROP COLUMN \`started_at\`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE \`workouts\` DROP COLUMN \`finished_at\`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE \`workouts\` DROP COLUMN \`total_distance\`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE \`workouts\` DROP COLUMN \`total_duration\`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE \`workouts\` DROP COLUMN \`calories\`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE \`workouts\` DROP COLUMN \`avg_heart_rate\`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE \`workouts\` DROP COLUMN \`source\`;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const m0002 = `
|
||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE \`__new_laps\` (
|
||||||
|
\`id\` text PRIMARY KEY NOT NULL,
|
||||||
|
\`session_id\` text NOT NULL,
|
||||||
|
\`set_position\` integer NOT NULL,
|
||||||
|
\`rep_number\` integer NOT NULL,
|
||||||
|
\`distance\` integer NOT NULL,
|
||||||
|
\`duration\` real NOT NULL,
|
||||||
|
\`stroke_count\` integer,
|
||||||
|
\`heart_rate\` integer,
|
||||||
|
\`swolf\` real,
|
||||||
|
FOREIGN KEY (\`session_id\`) REFERENCES \`workout_sessions\`(\`id\`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO \`__new_laps\`("id", "session_id", "set_position", "rep_number", "distance", "duration", "stroke_count", "heart_rate", "swolf") SELECT "id", "session_id", "set_position", "rep_number", "distance", "duration", "stroke_count", "heart_rate", "swolf" FROM \`laps\`;--> statement-breakpoint
|
||||||
|
DROP TABLE \`laps\`;--> statement-breakpoint
|
||||||
|
ALTER TABLE \`__new_laps\` RENAME TO \`laps\`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
CREATE TABLE \`__new_workout_sessions\` (
|
||||||
|
\`id\` text PRIMARY KEY NOT NULL,
|
||||||
|
\`workout_id\` text,
|
||||||
|
\`name\` text,
|
||||||
|
\`pool_length\` real DEFAULT 25 NOT NULL,
|
||||||
|
\`started_at\` text NOT NULL,
|
||||||
|
\`finished_at\` text,
|
||||||
|
\`total_distance\` integer,
|
||||||
|
\`total_duration\` real,
|
||||||
|
\`calories\` real,
|
||||||
|
\`avg_heart_rate\` integer,
|
||||||
|
\`source\` text DEFAULT 'manual' NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO \`__new_workout_sessions\`("id", "workout_id", "name", "pool_length", "started_at", "finished_at", "total_distance", "total_duration", "calories", "avg_heart_rate", "source") SELECT "id", "workout_id", "name", "pool_length", "started_at", "finished_at", "total_distance", "total_duration", "calories", "avg_heart_rate", "source" FROM \`workout_sessions\`;--> statement-breakpoint
|
||||||
|
DROP TABLE \`workout_sessions\`;--> statement-breakpoint
|
||||||
|
ALTER TABLE \`__new_workout_sessions\` RENAME TO \`workout_sessions\`;--> statement-breakpoint
|
||||||
|
ALTER TABLE \`workouts\` ADD \`created_at\` text NOT NULL;
|
||||||
|
`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
journal,
|
journal,
|
||||||
migrations: {
|
migrations: {
|
||||||
m0000
|
m0000,
|
||||||
|
m0001,
|
||||||
|
m0002
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+147
-76
@@ -1,8 +1,9 @@
|
|||||||
import { db } from "./client";
|
import { db } from "./client";
|
||||||
import { generateUUID } from "../utils/uuid";
|
import { generateUUID } from "../utils/uuid";
|
||||||
import { workouts, sets, laps, templates } from "./schema";
|
import { workouts, workoutSessions, sets, laps, templates } from "./schema";
|
||||||
import { eq, desc, sql } from "drizzle-orm";
|
import { eq, desc, sql } from "drizzle-orm";
|
||||||
import type { WorkoutSet, LapData } from "@types/workout";
|
import type { WorkoutSet, LapData } from "@types/workout";
|
||||||
|
import WatchConnectivity from "../modules/watch-connectivity";
|
||||||
|
|
||||||
// ─── Templates ───────────────────────────────────────────────
|
// ─── Templates ───────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export async function toggleTemplateFavorite(id: string, isFavorite: boolean) {
|
|||||||
await db.update(templates).set({ isFavorite }).where(eq(templates.id, id));
|
await db.update(templates).set({ isFavorite }).where(eq(templates.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Workouts ────────────────────────────────────────────────
|
// ─── Workout Plans ───────────────────────────────────────────
|
||||||
|
|
||||||
export async function saveWorkout(
|
export async function saveWorkout(
|
||||||
name: string | null,
|
name: string | null,
|
||||||
@@ -39,16 +40,13 @@ export async function saveWorkout(
|
|||||||
templateId?: string
|
templateId?: string
|
||||||
) {
|
) {
|
||||||
const workoutId = generateUUID();
|
const workoutId = generateUUID();
|
||||||
const totalDistance = setsData.reduce((acc, s) => acc + s.distancePerRep * s.repCount, 0);
|
|
||||||
|
|
||||||
await db.insert(workouts).values({
|
await db.insert(workouts).values({
|
||||||
id: workoutId,
|
id: workoutId,
|
||||||
name,
|
name,
|
||||||
templateId: templateId ?? null,
|
templateId: templateId ?? null,
|
||||||
poolLength,
|
poolLength,
|
||||||
startedAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
totalDistance,
|
|
||||||
source: "manual",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < setsData.length; i++) {
|
for (let i = 0; i < setsData.length; i++) {
|
||||||
@@ -69,74 +67,50 @@ export async function saveWorkout(
|
|||||||
return workoutId;
|
return workoutId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function finishWorkout(
|
|
||||||
workoutId: string,
|
|
||||||
duration: number,
|
|
||||||
calories: number,
|
|
||||||
avgHeartRate: number
|
|
||||||
) {
|
|
||||||
await db
|
|
||||||
.update(workouts)
|
|
||||||
.set({
|
|
||||||
finishedAt: new Date().toISOString(),
|
|
||||||
totalDuration: duration,
|
|
||||||
calories,
|
|
||||||
avgHeartRate,
|
|
||||||
})
|
|
||||||
.where(eq(workouts.id, workoutId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkoutWithSets(workoutId: string) {
|
export async function getWorkoutWithSets(workoutId: string) {
|
||||||
const workout = await db.select().from(workouts).where(eq(workouts.id, workoutId)).get();
|
const workout = await db.select().from(workouts).where(eq(workouts.id, workoutId)).get();
|
||||||
if (!workout) return null;
|
if (!workout) return null;
|
||||||
|
|
||||||
const workoutSets = await db.select().from(sets).where(eq(sets.workoutId, workoutId));
|
const workoutSets = await db.select().from(sets).where(eq(sets.workoutId, workoutId));
|
||||||
const workoutLaps = await db.select().from(laps).where(eq(laps.workoutId, workoutId));
|
return { ...workout, sets: workoutSets };
|
||||||
|
|
||||||
return { ...workout, sets: workoutSets, laps: workoutLaps };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllWorkouts() {
|
export async function getAllWorkouts() {
|
||||||
return db.select().from(workouts).orderBy(desc(workouts.startedAt));
|
return db.select().from(workouts).orderBy(desc(workouts.createdAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteWorkout(id: string) {
|
// ─── Workout Sessions ────────────────────────────────────────
|
||||||
await db.delete(laps).where(eq(laps.workoutId, id));
|
|
||||||
await db.delete(sets).where(eq(sets.workoutId, id));
|
|
||||||
await db.delete(workouts).where(eq(workouts.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Laps ────────────────────────────────────────────────────
|
export async function finishWorkout(
|
||||||
|
workoutId: string,
|
||||||
|
duration: number,
|
||||||
|
calories: number,
|
||||||
|
avgHeartRate: number,
|
||||||
|
totalDistance: number
|
||||||
|
) {
|
||||||
|
const sessionId = generateUUID();
|
||||||
|
const workout = await db.select().from(workouts).where(eq(workouts.id, workoutId)).get();
|
||||||
|
|
||||||
export async function saveLap(workoutId: string, lap: LapData) {
|
await db.insert(workoutSessions).values({
|
||||||
const swolf =
|
id: sessionId,
|
||||||
lap.strokeCount != null ? lap.strokeCount + Math.round(lap.duration) : null;
|
|
||||||
|
|
||||||
await db.insert(laps).values({
|
|
||||||
id: generateUUID(),
|
|
||||||
workoutId,
|
workoutId,
|
||||||
setPosition: lap.setPosition,
|
name: workout?.name ?? null,
|
||||||
repNumber: lap.repNumber,
|
poolLength: workout?.poolLength ?? 25,
|
||||||
distance: lap.distance,
|
startedAt: new Date().toISOString(),
|
||||||
duration: lap.duration,
|
finishedAt: new Date().toISOString(),
|
||||||
strokeCount: lap.strokeCount ?? null,
|
totalDistance,
|
||||||
heartRate: lap.heartRate ?? null,
|
totalDuration: duration,
|
||||||
swolf,
|
calories,
|
||||||
|
avgHeartRate,
|
||||||
|
source: "phone",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveLaps(workoutId: string, lapsData: LapData[]) {
|
return sessionId;
|
||||||
for (const lap of lapsData) {
|
|
||||||
await saveLap(workoutId, lap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkoutLaps(workoutId: string) {
|
|
||||||
return db.select().from(laps).where(eq(laps.workoutId, workoutId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveWatchCompletedWorkout(data: {
|
export async function saveWatchCompletedWorkout(data: {
|
||||||
workoutId: string;
|
workoutId: string;
|
||||||
|
sessionId?: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
totalDistance: number;
|
totalDistance: number;
|
||||||
calories: number;
|
calories: number;
|
||||||
@@ -154,36 +128,48 @@ export async function saveWatchCompletedWorkout(data: {
|
|||||||
? Math.round(data.laps.reduce((sum, l) => sum + (l.heartRate || 0), 0) / data.laps.length)
|
? Math.round(data.laps.reduce((sum, l) => sum + (l.heartRate || 0), 0) / data.laps.length)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const existing = await db
|
// Haal plan op als het bestaat
|
||||||
.select({ id: workouts.id })
|
const plan = await db
|
||||||
|
.select()
|
||||||
.from(workouts)
|
.from(workouts)
|
||||||
.where(eq(workouts.id, data.workoutId))
|
.where(eq(workouts.id, data.workoutId))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Gebruik sessionId van de watch, of genereer een nieuwe
|
||||||
|
const sessionId = data.sessionId ?? generateUUID();
|
||||||
|
|
||||||
|
// Check of er al een sessie bestaat voor dit sessionId
|
||||||
|
const existingSession = await db
|
||||||
|
.select({ id: workoutSessions.id })
|
||||||
|
.from(workoutSessions)
|
||||||
|
.where(eq(workoutSessions.id, sessionId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existing.length === 0) {
|
if (existingSession.length === 0) {
|
||||||
await db.insert(workouts).values({
|
await db.insert(workoutSessions).values({
|
||||||
id: data.workoutId,
|
id: sessionId,
|
||||||
name: "Watch Workout",
|
workoutId: data.workoutId,
|
||||||
poolLength: 25,
|
name: plan?.name ?? "Watch Workout",
|
||||||
|
poolLength: plan?.poolLength ?? 25,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
finishedAt: new Date().toISOString(),
|
finishedAt: new Date().toISOString(),
|
||||||
totalDuration: data.duration,
|
|
||||||
totalDistance: data.totalDistance || 0,
|
totalDistance: data.totalDistance || 0,
|
||||||
|
totalDuration: data.duration,
|
||||||
calories: data.calories || 0,
|
calories: data.calories || 0,
|
||||||
avgHeartRate,
|
avgHeartRate,
|
||||||
source: "watch",
|
source: "watch",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await db
|
await db
|
||||||
.update(workouts)
|
.update(workoutSessions)
|
||||||
.set({
|
.set({
|
||||||
finishedAt: new Date().toISOString(),
|
finishedAt: new Date().toISOString(),
|
||||||
totalDuration: data.duration,
|
totalDuration: data.duration,
|
||||||
totalDistance: data.totalDistance || 0,
|
totalDistance: data.totalDistance || 0,
|
||||||
calories: data.calories || 0,
|
calories: data.calories || 0,
|
||||||
avgHeartRate,
|
avgHeartRate,
|
||||||
source: "watch",
|
|
||||||
})
|
})
|
||||||
.where(eq(workouts.id, data.workoutId));
|
.where(eq(workoutSessions.id, sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const lap of data.laps) {
|
for (const lap of data.laps) {
|
||||||
@@ -192,7 +178,7 @@ export async function saveWatchCompletedWorkout(data: {
|
|||||||
|
|
||||||
await db.insert(laps).values({
|
await db.insert(laps).values({
|
||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
workoutId: data.workoutId,
|
sessionId,
|
||||||
setPosition: lap.setPosition,
|
setPosition: lap.setPosition,
|
||||||
repNumber: lap.repNumber,
|
repNumber: lap.repNumber,
|
||||||
distance: lap.distance,
|
distance: lap.distance,
|
||||||
@@ -204,6 +190,91 @@ export async function saveWatchCompletedWorkout(data: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAllSessions() {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(workoutSessions)
|
||||||
|
.orderBy(desc(workoutSessions.startedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionWithDetails(sessionId: string) {
|
||||||
|
const session = await db
|
||||||
|
.select()
|
||||||
|
.from(workoutSessions)
|
||||||
|
.where(eq(workoutSessions.id, sessionId))
|
||||||
|
.get();
|
||||||
|
if (!session) return null;
|
||||||
|
|
||||||
|
const sessionLaps = await db.select().from(laps).where(eq(laps.sessionId, sessionId));
|
||||||
|
|
||||||
|
let sessionSets: any[] = [];
|
||||||
|
if (session.workoutId) {
|
||||||
|
sessionSets = await db.select().from(sets).where(eq(sets.workoutId, session.workoutId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...session, laps: sessionLaps, sets: sessionSets };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Laps ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function saveLap(sessionId: string, lap: LapData) {
|
||||||
|
const swolf =
|
||||||
|
lap.strokeCount != null ? lap.strokeCount + Math.round(lap.duration) : null;
|
||||||
|
|
||||||
|
await db.insert(laps).values({
|
||||||
|
id: generateUUID(),
|
||||||
|
sessionId,
|
||||||
|
setPosition: lap.setPosition,
|
||||||
|
repNumber: lap.repNumber,
|
||||||
|
distance: lap.distance,
|
||||||
|
duration: lap.duration,
|
||||||
|
strokeCount: lap.strokeCount ?? null,
|
||||||
|
heartRate: lap.heartRate ?? null,
|
||||||
|
swolf,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveLaps(sessionId: string, lapsData: LapData[]) {
|
||||||
|
for (const lap of lapsData) {
|
||||||
|
await saveLap(sessionId, lap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionLaps(sessionId: string) {
|
||||||
|
return db.select().from(laps).where(eq(laps.sessionId, sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Delete ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function deleteSession(sessionId: string) {
|
||||||
|
await db.delete(laps).where(eq(laps.sessionId, sessionId));
|
||||||
|
await db.delete(workoutSessions).where(eq(workoutSessions.id, sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSessionAndSync(sessionId: string) {
|
||||||
|
await deleteSession(sessionId);
|
||||||
|
await WatchConnectivity.sendWorkoutDeleted(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteWorkout(id: string) {
|
||||||
|
// Verwijder alle sessies die aan dit plan gelinkt zijn
|
||||||
|
const sessions = await db
|
||||||
|
.select({ id: workoutSessions.id })
|
||||||
|
.from(workoutSessions)
|
||||||
|
.where(eq(workoutSessions.workoutId, id));
|
||||||
|
for (const session of sessions) {
|
||||||
|
await db.delete(laps).where(eq(laps.sessionId, session.id));
|
||||||
|
}
|
||||||
|
await db.delete(workoutSessions).where(eq(workoutSessions.workoutId, id));
|
||||||
|
await db.delete(sets).where(eq(sets.workoutId, id));
|
||||||
|
await db.delete(workouts).where(eq(workouts.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteWorkoutAndSync(id: string) {
|
||||||
|
await deleteWorkout(id);
|
||||||
|
await WatchConnectivity.sendWorkoutDeleted(id);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Stats ───────────────────────────────────────────────────
|
// ─── Stats ───────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getWeeklyStats() {
|
export async function getWeeklyStats() {
|
||||||
@@ -213,12 +284,12 @@ export async function getWeeklyStats() {
|
|||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
count: sql<number>`count(*)`,
|
count: sql<number>`count(*)`,
|
||||||
totalDistance: sql<number>`coalesce(sum(${workouts.totalDistance}), 0)`,
|
totalDistance: sql<number>`coalesce(sum(${workoutSessions.totalDistance}), 0)`,
|
||||||
totalDuration: sql<number>`coalesce(sum(${workouts.totalDuration}), 0)`,
|
totalDuration: sql<number>`coalesce(sum(${workoutSessions.totalDuration}), 0)`,
|
||||||
avgCalories: sql<number>`coalesce(avg(${workouts.calories}), 0)`,
|
avgCalories: sql<number>`coalesce(avg(${workoutSessions.calories}), 0)`,
|
||||||
})
|
})
|
||||||
.from(workouts)
|
.from(workoutSessions)
|
||||||
.where(sql`${workouts.startedAt} >= ${weekAgo.toISOString()}`)
|
.where(sql`${workoutSessions.startedAt} >= ${weekAgo.toISOString()}`)
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,10 +309,10 @@ export async function getTotalStats() {
|
|||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
totalWorkouts: sql<number>`count(*)`,
|
totalWorkouts: sql<number>`count(*)`,
|
||||||
totalDistance: sql<number>`coalesce(sum(${workouts.totalDistance}), 0)`,
|
totalDistance: sql<number>`coalesce(sum(${workoutSessions.totalDistance}), 0)`,
|
||||||
totalDuration: sql<number>`coalesce(sum(${workouts.totalDuration}), 0)`,
|
totalDuration: sql<number>`coalesce(sum(${workoutSessions.totalDuration}), 0)`,
|
||||||
})
|
})
|
||||||
.from(workouts)
|
.from(workoutSessions)
|
||||||
.where(sql`${workouts.finishedAt} IS NOT NULL`)
|
.where(sql`${workoutSessions.finishedAt} IS NOT NULL`)
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -1,5 +1,5 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { workouts, sets, laps, templates } from "./schema";
|
import { templates, workouts, workoutSessions, sets, laps } from "./schema";
|
||||||
|
|
||||||
export const templatesRelations = relations(templates, ({ many }) => ({
|
export const templatesRelations = relations(templates, ({ many }) => ({
|
||||||
workouts: many(workouts),
|
workouts: many(workouts),
|
||||||
@@ -11,6 +11,14 @@ export const workoutsRelations = relations(workouts, ({ one, many }) => ({
|
|||||||
references: [templates.id],
|
references: [templates.id],
|
||||||
}),
|
}),
|
||||||
sets: many(sets),
|
sets: many(sets),
|
||||||
|
sessions: many(workoutSessions),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const workoutSessionsRelations = relations(workoutSessions, ({ one, many }) => ({
|
||||||
|
workout: one(workouts, {
|
||||||
|
fields: [workoutSessions.workoutId],
|
||||||
|
references: [workouts.id],
|
||||||
|
}),
|
||||||
laps: many(laps),
|
laps: many(laps),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -22,8 +30,8 @@ export const setsRelations = relations(sets, ({ one }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const lapsRelations = relations(laps, ({ one }) => ({
|
export const lapsRelations = relations(laps, ({ one }) => ({
|
||||||
workout: one(workouts, {
|
session: one(workoutSessions, {
|
||||||
fields: [laps.workoutId],
|
fields: [laps.sessionId],
|
||||||
references: [workouts.id],
|
references: [workoutSessions.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
+10
-2
@@ -15,6 +15,14 @@ export const workouts = sqliteTable("workouts", {
|
|||||||
name: text("name"),
|
name: text("name"),
|
||||||
templateId: text("template_id"),
|
templateId: text("template_id"),
|
||||||
poolLength: real("pool_length").notNull().default(25),
|
poolLength: real("pool_length").notNull().default(25),
|
||||||
|
createdAt: text("created_at").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workoutSessions = sqliteTable("workout_sessions", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
workoutId: text("workout_id"),
|
||||||
|
name: text("name"),
|
||||||
|
poolLength: real("pool_length").notNull().default(25),
|
||||||
startedAt: text("started_at").notNull(),
|
startedAt: text("started_at").notNull(),
|
||||||
finishedAt: text("finished_at"),
|
finishedAt: text("finished_at"),
|
||||||
totalDistance: integer("total_distance"),
|
totalDistance: integer("total_distance"),
|
||||||
@@ -40,9 +48,9 @@ export const sets = sqliteTable("sets", {
|
|||||||
|
|
||||||
export const laps = sqliteTable("laps", {
|
export const laps = sqliteTable("laps", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
workoutId: text("workout_id")
|
sessionId: text("session_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => workouts.id),
|
.references(() => workoutSessions.id),
|
||||||
setPosition: integer("set_position").notNull(),
|
setPosition: integer("set_position").notNull(),
|
||||||
repNumber: integer("rep_number").notNull(),
|
repNumber: integer("rep_number").notNull(),
|
||||||
distance: integer("distance").notNull(),
|
distance: integer("distance").notNull(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useNavigation, useRoute } from "@react-navigation/native";
|
|||||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||||
import type { RouteProp } from "@react-navigation/native";
|
import type { RouteProp } from "@react-navigation/native";
|
||||||
import type { RootStackParamList } from "@navigation/types";
|
import type { RootStackParamList } from "@navigation/types";
|
||||||
import { getWorkoutWithSets, getWorkoutLaps } from "@db/operations";
|
import { getSessionWithDetails } from "@db/operations";
|
||||||
import { WorkoutSummary } from "../components/WorkoutSummary";
|
import { WorkoutSummary } from "../components/WorkoutSummary";
|
||||||
|
|
||||||
type Set = {
|
type Set = {
|
||||||
@@ -38,6 +38,8 @@ export function PostSwimScreen() {
|
|||||||
sets: Set[];
|
sets: Set[];
|
||||||
laps: Lap[];
|
laps: Lap[];
|
||||||
avgSwolf: number | null;
|
avgSwolf: number | null;
|
||||||
|
totalLengths: number | null;
|
||||||
|
avgPace: number | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,29 +47,27 @@ export function PostSwimScreen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadSummary = async () => {
|
const loadSummary = async () => {
|
||||||
const workout = await getWorkoutWithSets(route.params.workoutId);
|
const session = await getSessionWithDetails(route.params.sessionId);
|
||||||
const laps = await getWorkoutLaps(route.params.workoutId);
|
if (!session) return;
|
||||||
|
|
||||||
if (!workout) return;
|
const totalDuration = session.totalDuration ?? 0;
|
||||||
|
|
||||||
const totalDuration = workout.laps?.reduce((acc: number, l: any) => acc + l.duration, 0) ?? 0;
|
|
||||||
const avgSwolf =
|
const avgSwolf =
|
||||||
laps.length > 0
|
session.laps.length > 0
|
||||||
? Math.round(laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / laps.length)
|
? Math.round(session.laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / session.laps.length)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const totalLengths =
|
const totalLengths =
|
||||||
workout.totalDistance && workout.poolLength
|
session.totalDistance && session.poolLength
|
||||||
? Math.round(workout.totalDistance / workout.poolLength)
|
? Math.round(session.totalDistance / session.poolLength)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const avgPace =
|
const avgPace =
|
||||||
workout.totalDistance && totalDuration
|
session.totalDistance && totalDuration
|
||||||
? workout.totalDistance / totalDuration
|
? session.totalDistance / totalDuration
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const setsWithPace = (workout.sets ?? []).map((set: any) => {
|
const setsWithPace = (session.sets ?? []).map((set: any) => {
|
||||||
const setLaps = laps.filter((l: any) => l.setPosition === set.position);
|
const setLaps = session.laps.filter((l: any) => l.setPosition === set.position);
|
||||||
const setDuration = setLaps.reduce((acc: number, l: any) => acc + l.duration, 0);
|
const setDuration = setLaps.reduce((acc: number, l: any) => acc + l.duration, 0);
|
||||||
const setDistance = setLaps.reduce((acc: number, l: any) => acc + l.distance, 0);
|
const setDistance = setLaps.reduce((acc: number, l: any) => acc + l.distance, 0);
|
||||||
const poolPace = setDistance > 0 ? setDuration / setDistance : null;
|
const poolPace = setDistance > 0 ? setDuration / setDistance : null;
|
||||||
@@ -75,14 +75,14 @@ export function PostSwimScreen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setData({
|
setData({
|
||||||
name: workout.name,
|
name: session.name,
|
||||||
poolLength: workout.poolLength,
|
poolLength: session.poolLength,
|
||||||
totalDistance: workout.totalDistance ?? 0,
|
totalDistance: session.totalDistance ?? 0,
|
||||||
totalDuration,
|
totalDuration,
|
||||||
calories: workout.calories,
|
calories: session.calories,
|
||||||
avgHeartRate: workout.avgHeartRate,
|
avgHeartRate: session.avgHeartRate,
|
||||||
sets: setsWithPace,
|
sets: setsWithPace,
|
||||||
laps,
|
laps: session.laps,
|
||||||
avgSwolf,
|
avgSwolf,
|
||||||
totalLengths,
|
totalLengths,
|
||||||
avgPace,
|
avgPace,
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { useNavigation } from "@react-navigation/native";
|
|||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||||
import type { HistoryStackParamList } from "@navigation/types";
|
import type { HistoryStackParamList } from "@navigation/types";
|
||||||
import { getAllWorkouts, deleteWorkout } from "@db/operations";
|
import { getAllSessions, deleteSessionAndSync } from "@db/operations";
|
||||||
import { formatTime } from "@utils/formatTime";
|
import { formatTime } from "@utils/formatTime";
|
||||||
import { formatDistance } from "@utils/formatDistance";
|
import { formatDistance } from "@utils/formatDistance";
|
||||||
import { SwipeableRow } from "@components/SwipeableRow";
|
import { SwipeableRow } from "@components/SwipeableRow";
|
||||||
|
|
||||||
type WorkoutRow = {
|
type SessionRow = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
totalDistance: number | null;
|
totalDistance: number | null;
|
||||||
@@ -24,23 +24,23 @@ export function HistoryScreen() {
|
|||||||
const { t } = useTranslation("history");
|
const { t } = useTranslation("history");
|
||||||
const { t: tc } = useTranslation("common");
|
const { t: tc } = useTranslation("common");
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<HistoryStackParamList>>();
|
const navigation = useNavigation<NativeStackNavigationProp<HistoryStackParamList>>();
|
||||||
const [workouts, setWorkouts] = useState<WorkoutRow[]>([]);
|
const [sessions, setSessions] = useState<SessionRow[]>([]);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const loadWorkouts = async () => {
|
const loadSessions = async () => {
|
||||||
const data = await getAllWorkouts();
|
const data = await getAllSessions();
|
||||||
setWorkouts((data as WorkoutRow[]).filter((w) => w.finishedAt));
|
setSessions(data as SessionRow[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
loadWorkouts();
|
loadSessions();
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
||||||
const onRefresh = async () => {
|
const onRefresh = async () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
await loadWorkouts();
|
await loadSessions();
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,17 +51,17 @@ export function HistoryScreen() {
|
|||||||
text: tc("delete"),
|
text: tc("delete"),
|
||||||
style: "destructive",
|
style: "destructive",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
await deleteWorkout(id);
|
await deleteSessionAndSync(id);
|
||||||
await loadWorkouts();
|
await loadSessions();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const grouped = workouts.reduce<Record<string, WorkoutRow[]>>((acc, w) => {
|
const grouped = sessions.reduce<Record<string, SessionRow[]>>((acc, s) => {
|
||||||
const date = new Date(w.startedAt).toLocaleDateString();
|
const date = new Date(s.startedAt).toLocaleDateString();
|
||||||
if (!acc[date]) acc[date] = [];
|
if (!acc[date]) acc[date] = [];
|
||||||
acc[date].push(w);
|
acc[date].push(s);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
@@ -85,25 +85,25 @@ export function HistoryScreen() {
|
|||||||
<Text className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 px-1">
|
<Text className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 px-1">
|
||||||
{date}
|
{date}
|
||||||
</Text>
|
</Text>
|
||||||
{rows.map((w) => (
|
{rows.map((s) => (
|
||||||
<SwipeableRow key={w.id} onDelete={() => onDelete(w.id)} deleteLabel={tc("delete")}>
|
<SwipeableRow key={s.id} onDelete={() => onDelete(s.id)} deleteLabel={tc("delete")}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => navigation.navigate("SessionDetail", { sessionId: w.id })}
|
onPress={() => navigation.navigate("SessionDetail", { sessionId: s.id })}
|
||||||
className="p-4"
|
className="p-4"
|
||||||
>
|
>
|
||||||
<View className="flex-row justify-between items-center">
|
<View className="flex-row justify-between items-center">
|
||||||
<View>
|
<View>
|
||||||
<Text className="font-semibold text-slate-900">
|
<Text className="font-semibold text-slate-900">
|
||||||
{w.name || tc("swimSession")}
|
{s.name || tc("swimSession")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-slate-500 text-sm">
|
<Text className="text-slate-500 text-sm">
|
||||||
{formatDistance(w.totalDistance ?? 0)}
|
{formatDistance(s.totalDistance ?? 0)}
|
||||||
{w.totalDuration ? ` · ${formatTime(w.totalDuration)}` : ""}
|
{s.totalDuration ? ` · ${formatTime(s.totalDuration)}` : ""}
|
||||||
{w.calories ? ` · ${tc("kcal", { count: Math.round(w.calories) })}` : ""}
|
{s.calories ? ` · ${tc("kcal", { count: Math.round(s.calories) })}` : ""}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-slate-400 text-xs">
|
<Text className="text-slate-400 text-xs">
|
||||||
{new Date(w.startedAt).toLocaleTimeString([], {
|
{new Date(s.startedAt).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { View, Text, Pressable, Alert } from "react-native";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import type { HistoryStackProps } from "@navigation/types";
|
import type { HistoryStackProps } from "@navigation/types";
|
||||||
import { getWorkoutWithSets, deleteWorkout } from "@db/operations";
|
import { getSessionWithDetails, deleteSessionAndSync } from "@db/operations";
|
||||||
import { WorkoutSummary } from "../../active-workout/components/WorkoutSummary";
|
import { WorkoutSummary } from "../../active-workout/components/WorkoutSummary";
|
||||||
|
|
||||||
type WorkoutDetail = {
|
type SessionDetail = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
poolLength: number;
|
poolLength: number;
|
||||||
@@ -16,6 +16,7 @@ type WorkoutDetail = {
|
|||||||
avgHeartRate: number | null;
|
avgHeartRate: number | null;
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
finishedAt: string | null;
|
finishedAt: string | null;
|
||||||
|
source: string;
|
||||||
sets: {
|
sets: {
|
||||||
stroke: string;
|
stroke: string;
|
||||||
distancePerRep: number;
|
distancePerRep: number;
|
||||||
@@ -35,15 +36,15 @@ type WorkoutDetail = {
|
|||||||
export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail">) {
|
export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail">) {
|
||||||
const { t: tc } = useTranslation("common");
|
const { t: tc } = useTranslation("common");
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const [workout, setWorkout] = useState<WorkoutDetail | null>(null);
|
const [session, setSession] = useState<SessionDetail | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadWorkout();
|
loadSession();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadWorkout = async () => {
|
const loadSession = async () => {
|
||||||
const data = await getWorkoutWithSets(route.params.sessionId);
|
const data = await getSessionWithDetails(route.params.sessionId);
|
||||||
setWorkout(data as WorkoutDetail | null);
|
setSession(data as SessionDetail | null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
@@ -53,7 +54,7 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail"
|
|||||||
text: tc("delete"),
|
text: tc("delete"),
|
||||||
style: "destructive",
|
style: "destructive",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
await deleteWorkout(route.params.sessionId);
|
await deleteSessionAndSync(route.params.sessionId);
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -61,7 +62,7 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail"
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workout) {
|
if (session) {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<Pressable onPress={handleDelete} hitSlop={8}>
|
<Pressable onPress={handleDelete} hitSlop={8}>
|
||||||
@@ -70,9 +71,9 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail"
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [workout]);
|
}, [session]);
|
||||||
|
|
||||||
if (!workout) {
|
if (!session) {
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-white items-center justify-center">
|
<View className="flex-1 bg-white items-center justify-center">
|
||||||
<Text className="text-slate-400">{tc("loading")}</Text>
|
<Text className="text-slate-400">{tc("loading")}</Text>
|
||||||
@@ -81,24 +82,24 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail"
|
|||||||
}
|
}
|
||||||
|
|
||||||
const avgSwolf =
|
const avgSwolf =
|
||||||
workout.laps.length > 0
|
session.laps.length > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
workout.laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / workout.laps.length
|
session.laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / session.laps.length
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const totalLengths =
|
const totalLengths =
|
||||||
workout.totalDistance && workout.poolLength
|
session.totalDistance && session.poolLength
|
||||||
? Math.round(workout.totalDistance / workout.poolLength)
|
? Math.round(session.totalDistance / session.poolLength)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const avgPace =
|
const avgPace =
|
||||||
workout.totalDistance && workout.totalDuration
|
session.totalDistance && session.totalDuration
|
||||||
? workout.totalDistance / workout.totalDuration
|
? session.totalDistance / session.totalDuration
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const setsWithPace = workout.sets.map((set) => {
|
const setsWithPace = session.sets.map((set) => {
|
||||||
const setLaps = workout.laps.filter((l) => l.setPosition === set.position);
|
const setLaps = session.laps.filter((l) => l.setPosition === set.position);
|
||||||
const setDuration = setLaps.reduce((acc, l) => acc + l.duration, 0);
|
const setDuration = setLaps.reduce((acc, l) => acc + l.duration, 0);
|
||||||
const setDistance = setLaps.reduce((acc, l) => acc + l.distance, 0);
|
const setDistance = setLaps.reduce((acc, l) => acc + l.distance, 0);
|
||||||
const poolPace = setDistance > 0 ? setDuration / setDistance : null;
|
const poolPace = setDistance > 0 ? setDuration / setDistance : null;
|
||||||
@@ -107,17 +108,17 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail"
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkoutSummary
|
<WorkoutSummary
|
||||||
name={workout.name}
|
name={session.name}
|
||||||
date={new Date(workout.startedAt).toLocaleDateString()}
|
date={new Date(session.startedAt).toLocaleDateString()}
|
||||||
totalDistance={workout.totalDistance ?? 0}
|
totalDistance={session.totalDistance ?? 0}
|
||||||
totalDuration={workout.totalDuration ?? 0}
|
totalDuration={session.totalDuration ?? 0}
|
||||||
totalLengths={totalLengths}
|
totalLengths={totalLengths}
|
||||||
calories={workout.calories}
|
calories={session.calories}
|
||||||
avgHeartRate={workout.avgHeartRate}
|
avgHeartRate={session.avgHeartRate}
|
||||||
avgPace={avgPace}
|
avgPace={avgPace}
|
||||||
poolLength={workout.poolLength}
|
poolLength={session.poolLength}
|
||||||
sets={setsWithPace}
|
sets={setsWithPace}
|
||||||
laps={workout.laps}
|
laps={session.laps}
|
||||||
avgSwolf={avgSwolf}
|
avgSwolf={avgSwolf}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import WatchConnectivity from "../../../modules/watch-connectivity";
|
import WatchConnectivity from "../../../modules/watch-connectivity";
|
||||||
import WearConnectivity from "../../../modules/wear-connectivity";
|
import WearConnectivity from "../../../modules/wear-connectivity";
|
||||||
import { saveWatchCompletedWorkout } from "@db/operations";
|
import { saveWatchCompletedWorkout, deleteSession } from "@db/operations";
|
||||||
|
|
||||||
export function useWatchWorkoutSync() {
|
export function useWatchWorkoutSync() {
|
||||||
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
@@ -22,11 +22,14 @@ export function useWatchWorkoutSync() {
|
|||||||
if (event.type === "workoutCompleted") {
|
if (event.type === "workoutCompleted") {
|
||||||
await saveWatchCompletedWorkout({
|
await saveWatchCompletedWorkout({
|
||||||
workoutId: event.workoutId,
|
workoutId: event.workoutId,
|
||||||
|
sessionId: event.sessionId,
|
||||||
duration: event.duration ?? 0,
|
duration: event.duration ?? 0,
|
||||||
totalDistance: event.totalDistance ?? 0,
|
totalDistance: event.totalDistance ?? 0,
|
||||||
calories: event.calories ?? 0,
|
calories: event.calories ?? 0,
|
||||||
laps: event.laps ?? [],
|
laps: event.laps ?? [],
|
||||||
});
|
});
|
||||||
|
} else if (event.type === "workoutDeleted") {
|
||||||
|
await deleteSession(event.workoutId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -5,18 +5,16 @@ import { useNavigation } from "@react-navigation/native";
|
|||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||||
import type { WorkoutStackParamList } from "@navigation/types";
|
import type { WorkoutStackParamList } from "@navigation/types";
|
||||||
import { getAllWorkouts, deleteWorkout } from "@db/operations";
|
import { getAllWorkouts, getWorkoutWithSets, deleteWorkoutAndSync } from "@db/operations";
|
||||||
import { formatTime } from "@utils/formatTime";
|
|
||||||
import { formatDistance } from "@utils/formatDistance";
|
import { formatDistance } from "@utils/formatDistance";
|
||||||
import { SwipeableRow } from "@components/SwipeableRow";
|
import { SwipeableRow } from "@components/SwipeableRow";
|
||||||
|
|
||||||
type WorkoutRow = {
|
type WorkoutRow = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
totalDistance: number | null;
|
poolLength: number;
|
||||||
totalDuration: number | null;
|
createdAt: string;
|
||||||
startedAt: string;
|
totalDistance?: number;
|
||||||
finishedAt: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WorkoutListScreen() {
|
export function WorkoutListScreen() {
|
||||||
@@ -28,7 +26,22 @@ export function WorkoutListScreen() {
|
|||||||
|
|
||||||
const loadWorkouts = async () => {
|
const loadWorkouts = async () => {
|
||||||
const data = await getAllWorkouts();
|
const data = await getAllWorkouts();
|
||||||
setWorkouts(data as WorkoutRow[]);
|
const rows = data as WorkoutRow[];
|
||||||
|
|
||||||
|
// Bereken totalDistance per workout op basis van sets
|
||||||
|
const withDistance = await Promise.all(
|
||||||
|
rows.map(async (w) => {
|
||||||
|
const detail = await getWorkoutWithSets(w.id);
|
||||||
|
if (!detail) return w;
|
||||||
|
const totalDistance = (detail.sets ?? []).reduce(
|
||||||
|
(acc: number, s: any) => acc + s.distancePerRep * s.repCount,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return { ...w, totalDistance };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setWorkouts(withDistance);
|
||||||
};
|
};
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
@@ -50,7 +63,7 @@ export function WorkoutListScreen() {
|
|||||||
text: tc("delete"),
|
text: tc("delete"),
|
||||||
style: "destructive",
|
style: "destructive",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
await deleteWorkout(id);
|
await deleteWorkoutAndSync(id);
|
||||||
await loadWorkouts();
|
await loadWorkouts();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -92,18 +105,13 @@ export function WorkoutListScreen() {
|
|||||||
{item.name || tc("untitledWorkout")}
|
{item.name || tc("untitledWorkout")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-slate-500 text-sm mt-1">
|
<Text className="text-slate-500 text-sm mt-1">
|
||||||
{formatDistance(item.totalDistance ?? 0)}
|
{item.totalDistance ? formatDistance(item.totalDistance) : ""}
|
||||||
{item.totalDuration ? ` · ${formatTime(item.totalDuration)}` : ""}
|
{item.poolLength ? ` · ${tc("pool", { length: item.poolLength })}` : ""}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-slate-400 text-xs mt-1">
|
<Text className="text-slate-400 text-xs mt-1">
|
||||||
{new Date(item.startedAt).toLocaleDateString()}
|
{new Date(item.createdAt).toLocaleDateString()}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{item.finishedAt && (
|
|
||||||
<View className="bg-green-100 rounded-full px-2 py-0.5">
|
|
||||||
<Text className="text-green-700 text-xs font-medium">{tc("done")}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</SwipeableRow>
|
</SwipeableRow>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export type RootStackParamList = {
|
|||||||
Main: undefined;
|
Main: undefined;
|
||||||
WorkoutBuilder: { templateId?: string } | undefined;
|
WorkoutBuilder: { templateId?: string } | undefined;
|
||||||
PreSwim: { workoutId: string };
|
PreSwim: { workoutId: string };
|
||||||
PostSwim: { workoutId: string };
|
PostSwim: { sessionId: string };
|
||||||
SessionDetail: { sessionId: string };
|
SessionDetail: { sessionId: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -526,9 +526,11 @@ class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate {
|
|||||||
// MARK: - Sync to Phone
|
// MARK: - Sync to Phone
|
||||||
|
|
||||||
private func syncCompletedWorkout() {
|
private func syncCompletedWorkout() {
|
||||||
|
let sessionId = UUID().uuidString
|
||||||
let workoutSummary: [String: Any] = [
|
let workoutSummary: [String: Any] = [
|
||||||
"type": "workoutCompleted",
|
"type": "workoutCompleted",
|
||||||
"workoutId": selectedWorkout?.id ?? "",
|
"workoutId": selectedWorkout?.id ?? "",
|
||||||
|
"sessionId": sessionId,
|
||||||
"name": selectedWorkout?.name ?? "Workout",
|
"name": selectedWorkout?.name ?? "Workout",
|
||||||
"poolLength": selectedWorkout?.poolLength ?? 0,
|
"poolLength": selectedWorkout?.poolLength ?? 0,
|
||||||
"laps": completedLaps.map { lap in
|
"laps": completedLaps.map { lap in
|
||||||
|
|||||||
Reference in New Issue
Block a user