diff --git a/migrations/0001_wandering_ogun.sql b/migrations/0001_wandering_ogun.sql new file mode 100644 index 00000000..05172994 --- /dev/null +++ b/migrations/0001_wandering_ogun.sql @@ -0,0 +1,26 @@ +CREATE TABLE `noodle_subtask` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `title` text NOT NULL, + `notes` text NOT NULL, + `done` integer DEFAULT false, + `doneAt` text, + `task_id` integer NOT NULL, + `createdAt` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updatedAt` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`task_id`) REFERENCES `noodle_task`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `noodle_task` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `title` text NOT NULL, + `notes` text NOT NULL, + `done` integer DEFAULT false, + `doneAt` text, + `dueDate` text NOT NULL, + `priority` text NOT NULL, + `tags` text, + `module_id` integer NOT NULL, + `createdAt` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updatedAt` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`module_id`) REFERENCES `noodle_module`(`id`) ON UPDATE no action ON DELETE no action +); diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..fbefbb81 --- /dev/null +++ b/migrations/meta/0001_snapshot.json @@ -0,0 +1,457 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "a46dbab8-7965-4ddd-9b23-e51540474c87", + "prevId": "14800464-77e1-40db-85cb-670940d14a4c", + "tables": { + "noodle_feedback": { + "name": "noodle_feedback", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_module": { + "name": "noodle_module", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'graduation-cap'" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'primary'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "lastVisited": { + "name": "lastVisited", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_notebooks": { + "name": "noodle_notebooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Untitled'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'book'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "moduleId": { + "name": "moduleId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "lastVisited": { + "name": "lastVisited", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "noodle_notebooks_moduleId_noodle_module_id_fk": { + "name": "noodle_notebooks_moduleId_noodle_module_id_fk", + "tableFrom": "noodle_notebooks", + "tableTo": "noodle_module", + "columnsFrom": [ + "moduleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_subtask": { + "name": "noodle_subtask", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "done": { + "name": "done", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "doneAt": { + "name": "doneAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "noodle_subtask_task_id_noodle_task_id_fk": { + "name": "noodle_subtask_task_id_noodle_task_id_fk", + "tableFrom": "noodle_subtask", + "tableTo": "noodle_task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_task": { + "name": "noodle_task", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "done": { + "name": "done", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "doneAt": { + "name": "doneAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dueDate": { + "name": "dueDate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "module_id": { + "name": "module_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "noodle_task_module_id_noodle_module_id_fk": { + "name": "noodle_task_module_id_noodle_module_id_fk", + "tableFrom": "noodle_task", + "tableTo": "noodle_module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_waitlist": { + "name": "noodle_waitlist", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "invitationSentAt": { + "name": "invitationSentAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "noodle_waitlist_email_unique": { + "name": "noodle_waitlist_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index d4aed5da..0630fff0 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1698253502434, "tag": "0000_faithful_thaddeus_ross", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1698770214982, + "tag": "0001_wandering_ogun", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/(dashboard)/_components/module-card.tsx b/src/app/(dashboard)/_components/module-card.tsx index 9facc601..ff339dc5 100644 --- a/src/app/(dashboard)/_components/module-card.tsx +++ b/src/app/(dashboard)/_components/module-card.tsx @@ -1,4 +1,5 @@ import { Icon, type IconNames } from "@/components/icon"; +import { Task } from "@/db"; import { cn } from "@/utils/cn"; import { convertHexToRGBA, primary } from "@/utils/colors"; import { Card } from "@nextui-org/card"; @@ -14,6 +15,7 @@ type ModuleCardProps = { name: string; icon: IconNames; credits: number; + tasks: Task[]; }; export const ModuleCard: FC = ({ @@ -22,10 +24,17 @@ export const ModuleCard: FC = ({ name, icon, credits, + tasks, }) => { const moduleColor = color === "primary" ? primary : colors[color as keyof typeof colors]; + const undone = tasks.filter((task) => !task.done); + const done = tasks.filter((task) => task.done); + const donePercentage = tasks.length + ? (done.length / tasks.length) * 100 + : 100; + return (
  • = ({

    {credits} Credits

    -

    8 Tasks remaining

    +

    + {undone.length} Tasks remaining +

    diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts index 315abc01..b3208cf3 100644 --- a/src/db/schema/index.ts +++ b/src/db/schema/index.ts @@ -1,4 +1,6 @@ export * from "./feedback"; export * from "./module"; export * from "./notebook"; +export * from "./subtask"; +export * from "./task"; export * from "./waitlist"; diff --git a/src/db/schema/module.ts b/src/db/schema/module.ts index e3149fa4..39fb9fd3 100644 --- a/src/db/schema/module.ts +++ b/src/db/schema/module.ts @@ -4,6 +4,7 @@ import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { z } from "zod"; import { sqliteTable } from "./noodle_table"; import { notebooks } from "./notebook"; +import { taskTable } from "./task"; export const moduleTable = sqliteTable("module", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), @@ -26,6 +27,7 @@ export const moduleTable = sqliteTable("module", { export const moduleTableRelations = relations(moduleTable, ({ many }) => ({ notebooks: many(notebooks), + tasks: many(taskTable) })); export const insertModuleSchema = createInsertSchema(moduleTable, { diff --git a/src/db/schema/project.ts b/src/db/schema/project.ts new file mode 100644 index 00000000..ccf16e4f --- /dev/null +++ b/src/db/schema/project.ts @@ -0,0 +1,25 @@ +import { relations, sql } from "drizzle-orm"; +import { text } from "drizzle-orm/sqlite-core"; +import { sqliteTable } from "./noodle_table"; +import { taskTable } from "./task"; + +export const projectTable = sqliteTable("project", { + id: text("id").notNull(), + + userId: text("userId").notNull(), + + name: text("title").notNull(), + description: text("description").notNull(), + + createdAt: text("createdAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + + updatedAt: text("updatedAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const projectRelations = relations(projectTable, ({ many }) => ({ + tasks: many(taskTable), +})); diff --git a/src/db/schema/subtask.ts b/src/db/schema/subtask.ts new file mode 100644 index 00000000..d803d468 --- /dev/null +++ b/src/db/schema/subtask.ts @@ -0,0 +1,58 @@ +import { relations, sql } from "drizzle-orm"; +import { integer, text } from "drizzle-orm/sqlite-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { sqliteTable } from "./noodle_table"; +import { taskTable } from "./task"; + +export const subtaskTable = sqliteTable("subtask", { + id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + + title: text("title").notNull(), + + completed: integer("completed", { mode: "boolean" }).default(false), + completedAt: text("completedAt"), + + taskId: integer("task_id") + .references(() => taskTable.id) + .notNull(), + + createdAt: text("createdAt") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + + updatedAt: text("updatedAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export type Subtask = typeof subtaskTable.$inferSelect; +export type NewSubtask = typeof subtaskTable.$inferInsert; + +export const subtaskRelations = relations(subtaskTable, ({ one }) => ({ + task: one(taskTable), +})); + +export const insertSubtaskSchema = createInsertSchema(subtaskTable).omit({ + id: true, + createdAt: true, + done: true, + doneAt: true, +}); + +export const updateSubtaskSchema = createInsertSchema(subtaskTable).omit({ + moduleId: true, + taskId: true, + doneAt: true, + createdAt: true, +}); + +export const selectSubtaskSchema = createSelectSchema(subtaskTable).omit({ + createdAt: true, + description: true, + dueDate: true, + priority: true, + title: true, + notes: true, + doneAt: true, + done: true, +}); diff --git a/src/db/schema/tag.ts b/src/db/schema/tag.ts new file mode 100644 index 00000000..80ab1c8d --- /dev/null +++ b/src/db/schema/tag.ts @@ -0,0 +1,52 @@ +import { relations, sql } from "drizzle-orm"; +import { primaryKey, text } from "drizzle-orm/sqlite-core"; +import { sqliteTable } from "./noodle_table"; +import { taskTable } from "./task"; + +export const tagTable = sqliteTable("tag", { + id: text("id").notNull(), + + userId: text("userId").notNull(), + + name: text("name").notNull(), + + color: text("color").notNull().default("primary"), + + createdAt: text("createdAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + + updatedAt: text("updatedAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const tagRelation = relations(tagTable, ({ many }) => ({ + tasks: many(taskTable), +})); + +export const tasksToTags = sqliteTable( + "tasksToTags", + { + taskId: text("taskId") + .notNull() + .references(() => taskTable.id), + tagId: text("tagId") + .notNull() + .references(() => tagTable.id), + }, + (t) => ({ + pk: primaryKey(t.taskId, t.tagId), + }), +); + +export const tasksToTagsRelations = relations(tasksToTags, ({ one }) => ({ + task: one(taskTable, { + fields: [tasksToTags.taskId], + references: [taskTable.id], + }), + tag: one(tagTable, { + fields: [tasksToTags.tagId], + references: [tagTable.id], + }), +})); diff --git a/src/db/schema/task.ts b/src/db/schema/task.ts new file mode 100644 index 00000000..96e3baaf --- /dev/null +++ b/src/db/schema/task.ts @@ -0,0 +1,69 @@ +import { relations, sql } from "drizzle-orm"; +import { integer, text } from "drizzle-orm/sqlite-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { moduleTable } from "./module"; +import { sqliteTable } from "./noodle_table"; +import { subtaskTable } from "./subtask"; +import { tagTable } from "./tag"; + +export const taskTable = sqliteTable("task", { + id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + + title: text("title").notNull(), + + notes: text("notes").notNull(), + + completed: integer("completed", { mode: "boolean" }).default(false), + completedAt: text("completedAt"), + + dueDate: text("dueDate").notNull(), + personalDueDate: text("dueDate").notNull(), + reminderDate: text("reminderDate").notNull(), + + priority: text("priority", { enum: ["LOW", "MEDIUM", "URGENT"] }).notNull(), + + tags: text("tags", { mode: "json" }).$type(), + + moduleId: integer("module_id") + .references(() => moduleTable.id) + .notNull(), + + createdAt: text("createdAt") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + + updatedAt: text("updatedAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const taskRelations = relations(taskTable, ({ one, many }) => ({ + module: one(moduleTable, { + fields: [taskTable.moduleId], + references: [moduleTable.id], + }), + tags: many(tagTable), + subtask: many(subtaskTable), +})); + +export const insertTaskSchema = createInsertSchema(taskTable).omit({ + id: true, + done: true, + doneAt: true, + createdAt: true, +}); + +export const updateTaskSchema = createInsertSchema(taskTable).omit({ + moduleId: true, + done: true, +}); + +export const selectTaskSchema = createSelectSchema(taskTable).omit({ + createdAt: true, + description: true, + priority: true, + title: true, +}); + +export type Task = typeof taskTable.$inferSelect; +export type NewTask = typeof taskTable.$inferInsert; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index dbc936a2..0af511ee 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,5 +1,7 @@ import { feedbackRouter } from "./routers/feedback"; import { moduleRouter } from "./routers/module"; +import { subtaskRouter } from "./routers/subtask"; +import { taskRouter } from "./routers/task"; import { waitlistRouter } from "./routers/waitlist"; import { weatherRouter } from "./routers/weather"; import { createTRPCRouter } from "./trpc"; @@ -9,6 +11,8 @@ export const appRouter = createTRPCRouter({ waitlist: waitlistRouter, feedback: feedbackRouter, module: moduleRouter, + task: taskRouter, + subtask: subtaskRouter, }); export type AppRouter = typeof appRouter; diff --git a/src/server/api/routers/module.ts b/src/server/api/routers/module.ts index 764fc6e7..4e5e1fdf 100644 --- a/src/server/api/routers/module.ts +++ b/src/server/api/routers/module.ts @@ -1,6 +1,6 @@ import { insertModuleSchema, moduleTable, selectModuleSchema } from "@/db"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; import { and, eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; export const moduleRouter = createTRPCRouter({ get: createTRPCRouter({ @@ -8,6 +8,9 @@ export const moduleRouter = createTRPCRouter({ try { const modules = await ctx.db.query.moduleTable.findMany({ where: (table, { eq }) => eq(table.userId, ctx.auth.userId), + with:{ + tasks: true + } }); return modules; diff --git a/src/server/api/routers/subtask.ts b/src/server/api/routers/subtask.ts new file mode 100644 index 00000000..29c46de5 --- /dev/null +++ b/src/server/api/routers/subtask.ts @@ -0,0 +1,139 @@ +import { + insertSubtaskSchema, + moduleTable, + selectSubtaskSchema, + subtaskTable, + taskTable, +} from "@/db"; +import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const subtaskRouter = createTRPCRouter({ + get: createTRPCRouter({ + byTask: protectedProcedure + .input(selectSubtaskSchema.pick({ taskId: true })) + .query(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where( + and( + eq(moduleTable.userId, ctx.auth.userId), + eq(taskTable.id, input.taskId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "The task you are trying to get subtasks for does not exist", + }); + } + + const { task } = taskWithModule; + const subtasks = await ctx.db.query.subtaskTable.findMany({ + where: (table, { eq }) => eq(table.taskId, task.id), + }); + + return subtasks; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error ocurred while getting subtasks", + }); + } + }), + }), + post: createTRPCRouter({ + create: protectedProcedure + .input(insertSubtaskSchema) + .mutation(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where( + and( + eq(moduleTable.userId, ctx.auth.userId), + eq(taskTable.id, input.taskId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "The task you are trying to add a subtask to does not exist.", + }); + } + + const { task } = taskWithModule; + const createdSubtask = await ctx.db + .insert(subtaskTable) + .values({ + ...input, + taskId: task.id, + }) + .returning(); + + return createdSubtask; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error ocurred while creating subtask.", + }); + } + }), + }), + delete: createTRPCRouter({ + byId: protectedProcedure + .input(selectSubtaskSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + try { + const [subtaskWithTask] = await ctx.db + .select() + .from(subtaskTable) + .leftJoin(taskTable, eq(taskTable.id, subtaskTable.taskId)) + .leftJoin(moduleTable, eq(moduleTable.id, taskTable.moduleId)) + .where( + and( + eq(subtaskTable.id, input.id), + eq(moduleTable.userId, ctx.auth.userId), + ), + ) + .limit(1); + + if (!subtaskWithTask) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `The task you are trying to delete does not exist`, + }); + } + + const { subtask } = subtaskWithTask; + + const deleteResult = await ctx.db + .delete(subtaskTable) + .where(eq(subtaskTable.id, subtask.id)) + .returning(); + + return deleteResult; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while deleting the task.", + }); + } + }), + }), +}); diff --git a/src/server/api/routers/task.ts b/src/server/api/routers/task.ts new file mode 100644 index 00000000..225d9758 --- /dev/null +++ b/src/server/api/routers/task.ts @@ -0,0 +1,231 @@ +import { + insertTaskSchema, + moduleTable, + selectTaskSchema, + taskTable, +} from "@/db"; +import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const taskRouter = createTRPCRouter({ + get: createTRPCRouter({ + all: protectedProcedure.query(async ({ ctx }) => { + try { + const tasks = ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where(eq(moduleTable.userId, ctx.auth.userId)); + + return tasks; + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error ocurred while getting your tasks.", + }); + } + }), + byModule: protectedProcedure + .input(selectTaskSchema.pick({ moduleId: true })) + .query(async ({ ctx, input }) => { + try { + const tasksWithModule = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where( + and( + eq(moduleTable.userId, ctx.auth.userId), + eq(taskTable.moduleId, input.moduleId), + ), + ); + + return tasksWithModule.map((taskWithModule) => taskWithModule.task); + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while getting tasks by module.", + }); + } + }), + byDate: protectedProcedure + .input(selectTaskSchema.pick({ dueDate: true })) + .query(async ({ ctx, input }) => { + try { + const tasksWithModule = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where( + and( + eq(taskTable.dueDate, input.dueDate), + eq(moduleTable.userId, ctx.auth.userId), + ), + ); + + return tasksWithModule.map((taskWithModule) => taskWithModule.task); + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while getting tasks by date.", + }); + } + }), + }), + post: createTRPCRouter({ + create: protectedProcedure + .input(insertTaskSchema) + .mutation(async ({ ctx, input }) => { + try { + const createdTask = await ctx.db + .insert(taskTable) + .values({ + ...input, + tags: [], + }) + .returning(); + + return createdTask; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while creating the task.", + }); + } + }), + repeat: protectedProcedure + .input(selectTaskSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(moduleTable.id, taskTable.moduleId)) + .where( + and( + eq(taskTable.id, input.id), + eq(moduleTable.userId, ctx.auth.userId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "The task you are trying to repeat does not exist.", + }); + } + + const { id, createdAt, updatedAt, done, doneAt, ...rest } = + taskWithModule.task; + const repeatedTask = await ctx.db + .insert(taskTable) + .values({ + ...rest, + }) + .returning(); + + return repeatedTask; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while repeating the task.", + }); + } + }), + }), + patch: createTRPCRouter({ + switchDone: protectedProcedure + .input(selectTaskSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(moduleTable.id, taskTable.moduleId)) + .where( + and( + eq(taskTable.id, input.id), + eq(moduleTable.userId, ctx.auth.userId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `The task you are trying to mark as done does not exist`, + }); + } + + const { task } = taskWithModule; + + const res = await ctx.db + .update(taskTable) + .set({ + done: !task.done, + doneAt: !task.done ? new Date().toString() : null, + }) + .where(eq(taskTable.id, input.id)) + .returning(); + + return res; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "An error occurred while switching the task's done status.", + }); + } + }), + }), + delete: createTRPCRouter({ + byId: protectedProcedure + .input(selectTaskSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(moduleTable.id, taskTable.moduleId)) + .where( + and( + eq(taskTable.id, input.id), + eq(moduleTable.userId, ctx.auth.userId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `The task you are trying to delete does not exist`, + }); + } + + const { task } = taskWithModule; + + const deleteResult = await ctx.db + .delete(taskTable) + .where(eq(taskTable.id, task.id)) + .returning(); + + return deleteResult; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while deleting the task.", + }); + } + }), + }), +});