Hour get and update works - and CLI for it
This commit is contained in:
parent
f2d1ecd097
commit
4d6840559d
@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"@date-fns/tz": "^1.2.0",
|
||||
"@lifetracker/db": "workspace:*",
|
||||
"@lifetracker/shared": "workspace:^",
|
||||
"@lifetracker/trpc": "workspace:^",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.1",
|
||||
|
||||
@ -10,6 +10,7 @@ import { Command } from "@commander-js/extra-typings";
|
||||
import { getBorderCharacters, table } from "table";
|
||||
import { format } from "date-fns";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours";
|
||||
|
||||
function moodToStars(mood: number) {
|
||||
// const full_stars = Math.floor(mood / 2);
|
||||
@ -41,17 +42,21 @@ export const daysCmd = new Command()
|
||||
|
||||
const moodStr = moodToStars(day.mood);
|
||||
const dateStr = format(day.date, "EEEE, MMMM do");
|
||||
const data: string[][] = [[dateStr, moodStr], [day.comment ?? "No comment", '',]];
|
||||
const data: string[][] = [[dateStr, '', moodStr], [day.comment ?? "No comment", '', '']];
|
||||
|
||||
day.hours.forEach((h) => {
|
||||
data.push([getHourFromTime(h.time), h.categoryCode ?? "--", h.comment ?? ""]);
|
||||
})
|
||||
|
||||
console.log(table(data, {
|
||||
// border: getBorderCharacters("ramac"),
|
||||
// singleLine: true,
|
||||
spanningCells: [{ col: 0, row: 1, colSpan: 2 }],
|
||||
spanningCells: [{ col: 0, row: 1, colSpan: 3 }, { col: 1, row: 2, colSpan: 2 }],
|
||||
drawVerticalLine: (lineIndex, columnCount) => {
|
||||
return lineIndex === 0 || lineIndex === columnCount || (lineIndex === 0 && columnCount === 2);
|
||||
},
|
||||
drawHorizontalLine: (lineIndex, rowCount) => {
|
||||
return (lineIndex < 2 || lineIndex === rowCount);
|
||||
return (lineIndex < 2 || lineIndex === 2 || lineIndex === rowCount);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
67
apps/cli/src/commands/hours.ts
Normal file
67
apps/cli/src/commands/hours.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { getGlobalOptions } from "@/lib/globals";
|
||||
import {
|
||||
printError,
|
||||
printErrorMessageWithReason,
|
||||
printObject,
|
||||
printSuccess,
|
||||
} from "@/lib/output";
|
||||
import { getAPIClient } from "@/lib/trpc";
|
||||
import { Command } from "@commander-js/extra-typings";
|
||||
import { getBorderCharacters, table } from "table";
|
||||
import { format } from "date-fns";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours";
|
||||
import { ZHour } from "@lifetracker/shared/types/days";
|
||||
|
||||
export const hoursCmd = new Command()
|
||||
.name("hour")
|
||||
.description("Get or set data for a specific hour")
|
||||
.argument('<date>', 'A date in ISO-8601 format, or "yesterday", "today", "tomorrow", etc.')
|
||||
.argument('<hour>', 'An hour between 0-23, or 1-12 [AM/PM]')
|
||||
.argument('[code]', 'Optionally set the code for this hour')
|
||||
.option('-c, --comment <comment>', "edit this hour's comment")
|
||||
.action(async (dateQuery = "today", hour: string, code: string | undefined, flags?) => {
|
||||
const api = getAPIClient();
|
||||
|
||||
let res: string | ZHour;
|
||||
try {
|
||||
const props = { dateQuery: dateQuery, time: getTimeFromHour(hour), code: code, ...flags };
|
||||
if (code) {
|
||||
// Update
|
||||
console.log(props);
|
||||
res = await api.hours.update.mutate({ ...props });
|
||||
}
|
||||
else {
|
||||
// Get
|
||||
res = await api.hours.get.query({ ...props });
|
||||
}
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
printErrorMessageWithReason("Failed to manipulate hour", error as object);
|
||||
}
|
||||
|
||||
if (getGlobalOptions().json) {
|
||||
printObject(res);
|
||||
} else {
|
||||
const data = [
|
||||
['Doing:', res.categoryName ?? "Undefined"],
|
||||
|
||||
];
|
||||
if (res.comment) {
|
||||
data.push(['Comment:', res.comment ?? "Undefined"]);
|
||||
}
|
||||
console.log(table(data, {
|
||||
// border: getBorderCharacters("ramac"),
|
||||
// singleLine: true,
|
||||
// spanningCells: [{ col: 0, row: 0, colSpan: 2 },],
|
||||
header: { alignment: "center", content: `${format(res.date, "EEEE, MMMM dd")} at ${getHourFromTime(res.time, true)}` },
|
||||
drawVerticalLine: (lineIndex, columnCount) => {
|
||||
return lineIndex === 0 || lineIndex === columnCount || (lineIndex === 0 && columnCount === 2);
|
||||
},
|
||||
drawHorizontalLine: (lineIndex, rowCount) => {
|
||||
return (lineIndex < 2 || lineIndex === 2 || lineIndex === rowCount);
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
@ -5,6 +5,7 @@ import { whoamiCmd } from "@/commands/whoami";
|
||||
import { colorsCmd } from "@/commands/colors";
|
||||
import { categoriesCmd } from "@/commands/categories";
|
||||
import { daysCmd } from "@/commands/days";
|
||||
import { hoursCmd } from "./commands/hours";
|
||||
|
||||
import { config } from "dotenv";
|
||||
|
||||
@ -40,6 +41,7 @@ program.addCommand(whoamiCmd);
|
||||
program.addCommand(daysCmd);
|
||||
program.addCommand(colorsCmd);
|
||||
program.addCommand(categoriesCmd);
|
||||
program.addCommand(hoursCmd);
|
||||
|
||||
|
||||
setGlobalOptions(program.opts());
|
||||
|
||||
@ -42,6 +42,11 @@ export default async function DayView({
|
||||
<EditableDayComment day={day}
|
||||
className="text-xl"
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
{day.hours}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -57,19 +57,21 @@ CREATE TABLE `day` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`date` text NOT NULL,
|
||||
`mood` integer,
|
||||
`comment` text
|
||||
`comment` text,
|
||||
`userId` text NOT NULL,
|
||||
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `entry` (
|
||||
CREATE TABLE `hour` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`createdAt` integer NOT NULL,
|
||||
`userId` text NOT NULL,
|
||||
`comment` text,
|
||||
`dayId` integer,
|
||||
`startedAt` integer NOT NULL,
|
||||
`endedAt` integer NOT NULL,
|
||||
`categoryId` text NOT NULL,
|
||||
`time` integer,
|
||||
`dayId` text NOT NULL,
|
||||
`categoryId` text,
|
||||
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`dayId`) REFERENCES `day`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`categoryId`) REFERENCES `category`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
@ -102,5 +104,6 @@ CREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);--
|
||||
CREATE UNIQUE INDEX `category_userId_code_unique` ON `category` (`userId`,`code`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `color_userId_name_unique` ON `color` (`userId`,`name`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `day_date_unique` ON `day` (`date`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `entry_userId_startedAt_endedAt_unique` ON `entry` (`userId`,`startedAt`,`endedAt`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `hour_time_unique` ON `hour` (`time`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `hour_dayId_time_unique` ON `hour` (`dayId`,`time`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "ceed45ad-37d0-4e59-92bd-3be02d271535",
|
||||
"id": "ac3ad6ee-ccd2-4f91-9be7-83bd5b1d9ec8",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"account": {
|
||||
@ -434,6 +434,13 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
@ -445,12 +452,26 @@
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"foreignKeys": {
|
||||
"day_userId_user_id_fk": {
|
||||
"name": "day_userId_user_id_fk",
|
||||
"tableFrom": "day",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"entry": {
|
||||
"name": "entry",
|
||||
"hour": {
|
||||
"name": "hour",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
@ -480,23 +501,16 @@
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dayId": {
|
||||
"name": "dayId",
|
||||
"time": {
|
||||
"name": "time",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"startedAt": {
|
||||
"name": "startedAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"endedAt": {
|
||||
"name": "endedAt",
|
||||
"type": "integer",
|
||||
"dayId": {
|
||||
"name": "dayId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
@ -505,25 +519,31 @@
|
||||
"name": "categoryId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"entry_userId_startedAt_endedAt_unique": {
|
||||
"name": "entry_userId_startedAt_endedAt_unique",
|
||||
"hour_time_unique": {
|
||||
"name": "hour_time_unique",
|
||||
"columns": [
|
||||
"userId",
|
||||
"startedAt",
|
||||
"endedAt"
|
||||
"time"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"hour_dayId_time_unique": {
|
||||
"name": "hour_dayId_time_unique",
|
||||
"columns": [
|
||||
"dayId",
|
||||
"time"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"entry_userId_user_id_fk": {
|
||||
"name": "entry_userId_user_id_fk",
|
||||
"tableFrom": "entry",
|
||||
"hour_userId_user_id_fk": {
|
||||
"name": "hour_userId_user_id_fk",
|
||||
"tableFrom": "hour",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
@ -534,9 +554,22 @@
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"entry_categoryId_category_id_fk": {
|
||||
"name": "entry_categoryId_category_id_fk",
|
||||
"tableFrom": "entry",
|
||||
"hour_dayId_day_id_fk": {
|
||||
"name": "hour_dayId_day_id_fk",
|
||||
"tableFrom": "hour",
|
||||
"tableTo": "day",
|
||||
"columnsFrom": [
|
||||
"dayId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"hour_categoryId_category_id_fk": {
|
||||
"name": "hour_categoryId_category_id_fk",
|
||||
"tableFrom": "hour",
|
||||
"tableTo": "category",
|
||||
"columnsFrom": [
|
||||
"categoryId"
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1732544381615,
|
||||
"tag": "0000_powerful_zodiak",
|
||||
"when": 1732648521848,
|
||||
"tag": "0000_even_mysterio",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"scripts": {
|
||||
"dev": "drizzle-kit studio",
|
||||
"generate": "drizzle-kit generate",
|
||||
"reset": "tsc reset.ts",
|
||||
"reset": "tsx reset.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"migrate": "tsx migrate.ts",
|
||||
"studio": "drizzle-kit studio"
|
||||
@ -26,7 +26,8 @@
|
||||
"@lifetracker/typescript-config": "workspace:*",
|
||||
"@tsconfig/node21": "^21.0.1",
|
||||
"@types/better-sqlite3": "^7.6.9",
|
||||
"drizzle-kit": "^0.24.2"
|
||||
"drizzle-kit": "^0.24.2",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
||||
93
packages/db/reset.ts
Normal file
93
packages/db/reset.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import sqlite3 from 'sqlite3';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
|
||||
// Read JSON files
|
||||
const users = JSON.parse(fs.readFileSync('./seed/user.json', 'utf-8'));
|
||||
const apiKeys = JSON.parse(fs.readFileSync('./seed/apiKey.json', 'utf-8'));
|
||||
const colors = JSON.parse(fs.readFileSync('./seed/color.json', 'utf-8'));
|
||||
const categories = JSON.parse(fs.readFileSync('./seed/category.json', 'utf-8'));
|
||||
|
||||
// Connect to SQLite
|
||||
const db = new sqlite3.Database('/home/ryan/Notes/lifetracker/lifetracker.db');
|
||||
|
||||
// Helper function to insert data
|
||||
const insertData = (table, columns, values) => {
|
||||
const placeholders = columns.map(() => '?').join(', ');
|
||||
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
db.run(sql, values, (err) => {
|
||||
if (err) {
|
||||
console.error(`Error inserting into ${table}:`, err.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Insert users
|
||||
users.forEach(user => {
|
||||
insertData(
|
||||
'user',
|
||||
['id', 'name', 'email', 'emailVerified', 'image', 'password', 'role'],
|
||||
[
|
||||
user.id,
|
||||
user.name,
|
||||
user.email,
|
||||
user.emailVerified || null,
|
||||
user.image || null,
|
||||
user.password,
|
||||
user.role || 'user'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Insert API keys
|
||||
apiKeys.forEach(apiKey => {
|
||||
insertData(
|
||||
'apiKey',
|
||||
['id', 'keyHash', 'keyId', 'name', 'userId', 'createdAt'],
|
||||
[
|
||||
apiKey.id,
|
||||
apiKey.keyHash,
|
||||
apiKey.keyId,
|
||||
apiKey.name,
|
||||
apiKey.userId,
|
||||
apiKey.createdAt || new Date().toISOString()
|
||||
]
|
||||
);
|
||||
});
|
||||
categories.forEach(category => {
|
||||
insertData(
|
||||
'category',
|
||||
['id', 'code', 'colorId', 'createdAt', 'description', 'name', 'parentId', 'userId'],
|
||||
[
|
||||
category.id,
|
||||
category.code,
|
||||
category.colorId || null,
|
||||
category.createdAt || new Date().toISOString(),
|
||||
category.description || null,
|
||||
category.name,
|
||||
category.parentId || null,
|
||||
category.userId
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
colors.forEach(color => {
|
||||
insertData(
|
||||
'color',
|
||||
['id', 'name', 'hexcode', 'inverse', 'userId', 'createdAt'],
|
||||
[
|
||||
color.id,
|
||||
color.name,
|
||||
color.hexcode,
|
||||
color.inverse || null,
|
||||
color.userId,
|
||||
color.createdAt || new Date().toISOString()
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Close the connection
|
||||
db.close(() => {
|
||||
console.log('Database population completed!');
|
||||
});
|
||||
@ -130,8 +130,34 @@ export const days = sqliteTable("day", {
|
||||
date: text("date").notNull().unique(),
|
||||
mood: integer("mood"),
|
||||
comment: text("comment"),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
});
|
||||
|
||||
export const hours = sqliteTable(
|
||||
"hour",
|
||||
{
|
||||
id: text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId()),
|
||||
createdAt: createdAtField(),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
comment: text("comment"),
|
||||
time: integer("time").unique(),
|
||||
dayId: text("dayId").notNull().references(() => days.id),
|
||||
categoryId: text("categoryId").references(() => categories.id),
|
||||
},
|
||||
(e) => ({
|
||||
uniq: unique().on(e.dayId, e.time)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
export const colors = sqliteTable(
|
||||
"color",
|
||||
{
|
||||
@ -179,27 +205,7 @@ export const categories = sqliteTable(
|
||||
}),
|
||||
);
|
||||
|
||||
export const entries = sqliteTable(
|
||||
"entry",
|
||||
{
|
||||
id: text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId()),
|
||||
createdAt: createdAtField(),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
comment: text("comment"),
|
||||
dayId: integer("dayId"),
|
||||
startedAt: integer("startedAt", { mode: "timestamp" }).notNull(),
|
||||
endedAt: integer("endedAt", { mode: "timestamp" }).notNull(),
|
||||
categoryId: text("categoryId").notNull().references(() => categories.id),
|
||||
},
|
||||
(e) => ({
|
||||
uniq: unique().on(e.userId, e.startedAt, e.endedAt)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
// Relations
|
||||
|
||||
@ -212,7 +218,8 @@ export const apiKeyRelations = relations(apiKeys, ({ one }) => ({
|
||||
export const userRelations = relations(users, ({ many }) => ({
|
||||
categories: many(categories),
|
||||
colors: many(colors),
|
||||
entries: many(entries),
|
||||
days: many(days),
|
||||
hours: many(hours),
|
||||
}));
|
||||
|
||||
|
||||
@ -242,31 +249,35 @@ export const categoriesRelations = relations(
|
||||
fields: [categories.parentId],
|
||||
references: [categories.id],
|
||||
}),
|
||||
entries: many(entries),
|
||||
hours: many(hours),
|
||||
}),
|
||||
);
|
||||
|
||||
export const daysRelations = relations(
|
||||
days,
|
||||
({ many }) => ({
|
||||
entries: many(entries),
|
||||
({ many, one }) => ({
|
||||
user: one(users, {
|
||||
fields: [days.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
hours: many(hours),
|
||||
}),
|
||||
);
|
||||
|
||||
export const entriesRelations = relations(
|
||||
entries,
|
||||
export const hoursRelations = relations(
|
||||
hours,
|
||||
({ many, one }) => ({
|
||||
user: one(users, {
|
||||
fields: [entries.userId],
|
||||
fields: [hours.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
categories: one(categories, {
|
||||
fields: [entries.categoryId],
|
||||
fields: [hours.categoryId],
|
||||
references: [categories.id],
|
||||
}
|
||||
),
|
||||
day: one(days, {
|
||||
fields: [entries.dayId],
|
||||
fields: [hours.dayId],
|
||||
references: [days.id],
|
||||
}),
|
||||
}),
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@date-fns/tz": "^1.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
||||
@ -1,9 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Define the schema for the "hour" object
|
||||
export const zHourSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
dayId: z.string(),
|
||||
date: z.string().optional(),
|
||||
time: z.number(),
|
||||
categoryCode: z.coerce.number().nullable(),
|
||||
categoryId: z.string().nullable(),
|
||||
categoryName: z.string().nullable(),
|
||||
comment: z.string().nullable(),
|
||||
});
|
||||
export type ZHour = z.infer<typeof zHourSchema>;
|
||||
|
||||
export const zDaySchema = z.object({
|
||||
id: z.string().optional(),
|
||||
date: z.string(),
|
||||
mood: z.number().nullable(),
|
||||
comment: z.string().nullable(),
|
||||
hours: z.array(zHourSchema),
|
||||
|
||||
});
|
||||
export type ZDay = z.infer<typeof zDaySchema>;
|
||||
|
||||
13
packages/shared/utils/days.ts
Normal file
13
packages/shared/utils/days.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { format } from "date-fns";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
|
||||
export function dateFromInput(input: { dateQuery: string }) {
|
||||
let t: string;
|
||||
if (input.dateQuery == "today") {
|
||||
t = TZDate.tz("America/Los_Angeles");
|
||||
}
|
||||
else {
|
||||
t = new TZDate(input.dateQuery, "Etc/UTC");
|
||||
}
|
||||
return format(t, "yyyy-MM-dd") + "T00:00:00";
|
||||
}
|
||||
28
packages/shared/utils/hours.ts
Normal file
28
packages/shared/utils/hours.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export function getHourFromTime(time: number, includePeriod = false) {
|
||||
const hour = time == 0 || time == 12 ? 12 : time % 12;
|
||||
const period = time < 12 ? "AM" : "PM";
|
||||
return includePeriod ? `${hour} ${period}` : `${hour}`;
|
||||
}
|
||||
|
||||
export function getTimeFromHour(hour: string) {
|
||||
const match = hour.match(/^(\d{1,2}) ?([aApP][mM])?$/);
|
||||
if (!match) {
|
||||
throw new Error("Invalid time format");
|
||||
}
|
||||
|
||||
let time = parseInt(match[1]);
|
||||
const period = match[2] ? match[2].toUpperCase() : null;
|
||||
|
||||
if (time > 12 || time < 1) {
|
||||
throw new Error("Invalid hour");
|
||||
}
|
||||
|
||||
if (period === 'PM' && time !== 12) {
|
||||
time += 12;
|
||||
} else if (period === 'AM' && time === 12) {
|
||||
time = 0;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
@ -6,12 +6,14 @@ import { adminAppRouter } from "./admin";
|
||||
import { categoriesAppRouter } from "./categories";
|
||||
import { colorsAppRouter } from "./colors";
|
||||
import { daysAppRouter } from "./days";
|
||||
import { hoursAppRouter } from "./hours";
|
||||
|
||||
export const appRouter = router({
|
||||
users: usersAppRouter,
|
||||
apiKeys: apiKeysAppRouter,
|
||||
admin: adminAppRouter,
|
||||
days: daysAppRouter,
|
||||
hours: hoursAppRouter,
|
||||
colors: colorsAppRouter,
|
||||
categories: categoriesAppRouter,
|
||||
});
|
||||
|
||||
@ -3,33 +3,22 @@ import { and, desc, eq, inArray, notExists } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SqliteError } from "@lifetracker/db";
|
||||
import { days, } from "@lifetracker/db/schema";
|
||||
import { categories, days, hours, } from "@lifetracker/db/schema";
|
||||
import {
|
||||
zDaySchema, ZDay
|
||||
} from "@lifetracker/shared/types/days";
|
||||
import type { Context } from "../index";
|
||||
import { authedProcedure, router } from "../index";
|
||||
import { format } from "date-fns";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
|
||||
function dateFromInput(input: { dateQuery: string }) {
|
||||
let t: string;
|
||||
if (input.dateQuery == "today") {
|
||||
t = TZDate.tz("America/Los_Angeles");
|
||||
}
|
||||
else {
|
||||
t = new TZDate(input.dateQuery, "Etc/UTC");
|
||||
}
|
||||
|
||||
return format(t, "yyyy-MM-dd") + "T00:00:00";
|
||||
}
|
||||
import { dateFromInput } from "@lifetracker/shared/utils/days";
|
||||
|
||||
async function createDay(date: string, ctx: Context) {
|
||||
return await ctx.db.transaction(async (trx) => {
|
||||
try {
|
||||
const result = await trx
|
||||
// Create the Day object
|
||||
const dayRes = await trx
|
||||
.insert(days)
|
||||
.values({
|
||||
userId: ctx.user!.id,
|
||||
date: date,
|
||||
})
|
||||
.returning({
|
||||
@ -38,7 +27,20 @@ async function createDay(date: string, ctx: Context) {
|
||||
mood: days.mood,
|
||||
comment: days.comment,
|
||||
});
|
||||
return result[0];
|
||||
|
||||
const dayId = dayRes[0].id;
|
||||
|
||||
// Generate 24 "hour" objects
|
||||
const hoursData = Array.from({ length: 24 }, (_, hour) => ({
|
||||
userId: ctx.user!.id,
|
||||
dayId: dayId,
|
||||
time: hour
|
||||
}));
|
||||
|
||||
// Insert the "hour" objects
|
||||
await trx.insert(hours).values(hoursData);
|
||||
|
||||
return dayRes;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (e instanceof SqliteError) {
|
||||
@ -66,7 +68,9 @@ export const daysAppRouter = router({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const date = dateFromInput(input);
|
||||
|
||||
const res = await ctx.db
|
||||
// Fetch the day data
|
||||
let dayRes;
|
||||
dayRes = await ctx.db
|
||||
.select({
|
||||
id: days.id,
|
||||
date: days.date,
|
||||
@ -76,8 +80,32 @@ export const daysAppRouter = router({
|
||||
.from(days)
|
||||
.where(eq(days.date, date));
|
||||
|
||||
const day = res.length == 0 ? createDay(date, ctx) : res[0];
|
||||
return day;
|
||||
if (dayRes.length === 0) {
|
||||
dayRes = await createDay(date, ctx);
|
||||
}
|
||||
|
||||
const day = dayRes[0];
|
||||
|
||||
// Fetch the hours data for the corresponding dayId
|
||||
const hoursRes = await ctx.db
|
||||
.select({
|
||||
id: hours.id,
|
||||
dayId: hours.dayId,
|
||||
time: hours.time,
|
||||
categoryId: hours.categoryId,
|
||||
categoryCode: categories.code,
|
||||
comment: hours.comment,
|
||||
})
|
||||
.from(hours)
|
||||
.where(eq(hours.dayId, day.id))
|
||||
.leftJoin(categories, eq(categories.id, hours.categoryId))
|
||||
|
||||
// Combine the day and hours data
|
||||
const result = {
|
||||
...day,
|
||||
hours: hoursRes,
|
||||
};
|
||||
return result;
|
||||
}),
|
||||
update: authedProcedure
|
||||
.input(
|
||||
|
||||
111
packages/trpc/routers/hours.ts
Normal file
111
packages/trpc/routers/hours.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, inArray, notExists } from "drizzle-orm";
|
||||
import { date, z } from "zod";
|
||||
|
||||
import { SqliteError } from "@lifetracker/db";
|
||||
import { categories, days, hours, } from "@lifetracker/db/schema";
|
||||
import {
|
||||
zDaySchema, ZDay, ZHour, zHourSchema
|
||||
} from "@lifetracker/shared/types/days";
|
||||
import type { Context } from "../index";
|
||||
import { authedProcedure, router } from "../index";
|
||||
import { format } from "date-fns";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import { dateFromInput } from "@lifetracker/shared/utils/days";
|
||||
|
||||
export const hoursAppRouter = router({
|
||||
get: authedProcedure
|
||||
.input(z.object({
|
||||
dateQuery: z.string(),
|
||||
time: z.number()
|
||||
}))
|
||||
.output(zHourSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const date = dateFromInput(input);
|
||||
console.log(input);
|
||||
const hourRes = await ctx.db
|
||||
.select({
|
||||
id: hours.id,
|
||||
dayId: hours.dayId,
|
||||
time: hours.time,
|
||||
categoryId: hours.categoryId,
|
||||
categoryCode: categories.code,
|
||||
categoryName: categories.name,
|
||||
comment: hours.comment,
|
||||
})
|
||||
.from(hours)
|
||||
.leftJoin(days, eq(days.id, hours.dayId)) // Ensure days table is joined first
|
||||
.leftJoin(categories, eq(categories.id, hours.categoryId))
|
||||
.where(and(eq(hours.time, input.time), eq(days.date, date))) // Use correct alias for days table
|
||||
|
||||
console.log(hourRes);
|
||||
return {
|
||||
date: format(date, "yyyy-MM-dd"),
|
||||
...hourRes[0]
|
||||
};
|
||||
}),
|
||||
update: authedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
dateQuery: z.string(),
|
||||
time: z.number(),
|
||||
code: z.string().optional(),
|
||||
comment: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
// .output(zHourSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { dateQuery, time, code, ...updatedProps } = input;
|
||||
var date = dateFromInput({ dateQuery: dateQuery });
|
||||
const category = await ctx.db.select(
|
||||
{
|
||||
id: categories.id,
|
||||
name: categories.name,
|
||||
}
|
||||
)
|
||||
.from(categories)
|
||||
.where(
|
||||
and(
|
||||
eq(categories.code, code),
|
||||
eq(categories.userId, ctx.user!.id),
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
const day = await ctx.db.select(
|
||||
{ id: days.id }
|
||||
)
|
||||
.from(days)
|
||||
.where(
|
||||
and(
|
||||
eq(days.date, date),
|
||||
eq(days.userId, ctx.user!.id),
|
||||
)
|
||||
);
|
||||
|
||||
const newProps = {
|
||||
categoryId: category[0].id,
|
||||
...updatedProps
|
||||
};
|
||||
|
||||
if (newProps.comment == "") { newProps.comment = null }
|
||||
|
||||
const hourRes = await ctx.db
|
||||
.update(hours)
|
||||
.set(newProps)
|
||||
.where(
|
||||
and(
|
||||
eq(hours.time, time),
|
||||
eq(hours.dayId, day[0].id),
|
||||
eq(hours.userId, ctx.user!.id)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
return {
|
||||
date: format(date, "yyyy-MM-dd"),
|
||||
categoryName: category[0].name,
|
||||
...hourRes[0]
|
||||
}
|
||||
}),
|
||||
});
|
||||
543
pnpm-lock.yaml
generated
543
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
93
scripts/create_user.sh
Executable file
93
scripts/create_user.sh
Executable file
@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const fs = require('fs');
|
||||
|
||||
// Read JSON files
|
||||
const users = JSON.parse(fs.readFileSync('user.json'));
|
||||
const apikeys = JSON.parse(fs.readFileSync('apikey.json'));
|
||||
const colors = JSON.parse(fs.readFileSync('color.json'));
|
||||
const categories = JSON.parse(fs.readFileSync('category.json'));
|
||||
|
||||
// Connect to SQLite
|
||||
const db = new sqlite3.Database('/home/ryan/Notes/lifetracker/lifetracker.db');
|
||||
|
||||
// Helper function to insert data
|
||||
const insertData = (table, columns, values) => {
|
||||
const placeholders = columns.map(() => '?').join(', ');
|
||||
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
db.run(sql, values, (err) => {
|
||||
if (err) {
|
||||
console.error(`Error inserting into ${table}:`, err.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Insert users
|
||||
users.forEach(user => {
|
||||
insertData(
|
||||
'user',
|
||||
['id', 'name', 'email', 'emailVerified', 'image', 'password', 'role'],
|
||||
[
|
||||
user.id,
|
||||
user.name,
|
||||
user.email,
|
||||
user.emailVerified || null,
|
||||
user.image || null,
|
||||
user.password,
|
||||
user.role || 'user'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Insert API keys
|
||||
apiKeys.forEach(apiKey => {
|
||||
insertData(
|
||||
'apiKey',
|
||||
['id', 'keyHash', 'keyId', 'name', 'userId', 'createdAt'],
|
||||
[
|
||||
apiKey.id,
|
||||
apiKey.keyHash,
|
||||
apiKey.keyId,
|
||||
apiKey.name,
|
||||
apiKey.userId,
|
||||
apiKey.createdAt || new Date().toISOString()
|
||||
]
|
||||
);
|
||||
});
|
||||
categories.forEach(category => {
|
||||
insertData(
|
||||
'category',
|
||||
['id', 'code', 'colorId', 'createdAt', 'description', 'name', 'parentId', 'userId'],
|
||||
[
|
||||
category.id,
|
||||
category.code,
|
||||
category.colorId || null,
|
||||
category.createdAt || new Date().toISOString(),
|
||||
category.description || null,
|
||||
category.name,
|
||||
category.parentId || null,
|
||||
category.userId
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
colors.forEach(color => {
|
||||
insertData(
|
||||
'color',
|
||||
['id', 'name', 'hexcode', 'inverse', 'userId', 'createdAt'],
|
||||
[
|
||||
color.id,
|
||||
color.name,
|
||||
color.hexcode,
|
||||
color.inverse || null,
|
||||
color.userId,
|
||||
color.createdAt || new Date().toISOString()
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Close the connection
|
||||
db.close(() => {
|
||||
console.log('Database population completed!');
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user