diff --git a/src/app/api/portfolio/route.ts b/src/app/api/portfolio/route.ts new file mode 100644 index 0000000..4d7ac79 --- /dev/null +++ b/src/app/api/portfolio/route.ts @@ -0,0 +1,17 @@ +import { eq } from "drizzle-orm"; +import { portfolio, portfolioUsers } from "@/app/db/schema/portfolio"; +import { NextRequest, NextResponse } from "next/server"; +import { db } from "@/app/db/db"; + +export async function GET(request: NextRequest) { + // Get all portfolios for a user - use a relation + console.log(request); + + // const portfolios = await db + // .select() + // .from(portfolioUsers) + // .where(eq(portfolioUsers.userId, 1)); + // + const portfolios: String[] = []; + return NextResponse.json(portfolios); +} diff --git a/src/app/db/migrations/0003_past_gamora.sql b/src/app/db/migrations/0003_past_gamora.sql new file mode 100644 index 0000000..934900a --- /dev/null +++ b/src/app/db/migrations/0003_past_gamora.sql @@ -0,0 +1,56 @@ +DO $$ BEGIN + CREATE TYPE "goal" AS ENUM('Valuation Improvement', 'Increasing EPC', 'Reducing CO2 emissions', 'Energy Savings', 'None'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + CREATE TYPE "role" AS ENUM('creator', 'admin', 'read', 'write'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + CREATE TYPE "status" AS ENUM('scoping', 'assessment', 'tendering', 'project underway', 'completion; status: on track', 'completion; status: delayed', 'completion; status: at risk', 'completion; status: completed', 'needs review'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "portfolio" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "budget" real, + "status" "status" NOT NULL, + "goal" "goal" NOT NULL, + "cost" real, + "number_of_properties" integer, + "co2_equivalent_savings" real, + "energy_savings" real, + "energy_cost_savings" real, + "property_valuation_increase" real, + "rental_yield_increase" real, + "total_work_hours" real, + "created_at" timestamp (6) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "portfolioUsers" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "portfolio_id" integer NOT NULL, + "role" "role" NOT NULL, + "created_at" timestamp (6) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "portfolioUsers" ADD CONSTRAINT "portfolioUsers_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "portfolioUsers" ADD CONSTRAINT "portfolioUsers_portfolio_id_portfolio_id_fk" FOREIGN KEY ("portfolio_id") REFERENCES "portfolio"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/app/db/migrations/meta/0003_snapshot.json b/src/app/db/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..84cd0bf --- /dev/null +++ b/src/app/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,277 @@ +{ + "version": "5", + "dialect": "pg", + "id": "e43a69c5-c8ed-4de7-8915-0f0a23455bf1", + "prevId": "4347e9a7-f388-4bac-8860-a482db2a5c8b", + "tables": { + "portfolio": { + "name": "portfolio", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "portfolioUsers": { + "name": "portfolioUsers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioUsers_user_id_user_id_fk": { + "name": "portfolioUsers_user_id_user_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portfolioUsers_portfolio_id_portfolio_id_fk": { + "name": "portfolioUsers_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "enums": { + "goal": { + "name": "goal", + "values": { + "Valuation Improvement": "Valuation Improvement", + "Increasing EPC": "Increasing EPC", + "Reducing CO2 emissions": "Reducing CO2 emissions", + "Energy Savings": "Energy Savings", + "None": "None" + } + }, + "role": { + "name": "role", + "values": { + "creator": "creator", + "admin": "admin", + "read": "read", + "write": "write" + } + }, + "status": { + "name": "status", + "values": { + "scoping": "scoping", + "assessment": "assessment", + "tendering": "tendering", + "project underway": "project underway", + "completion; status: on track": "completion; status: on track", + "completion; status: delayed": "completion; status: delayed", + "completion; status: at risk": "completion; status: at risk", + "completion; status: completed": "completion; status: completed", + "needs review": "needs review" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/app/db/schema/portfolio.ts b/src/app/db/schema/portfolio.ts new file mode 100644 index 0000000..f366f0b --- /dev/null +++ b/src/app/db/schema/portfolio.ts @@ -0,0 +1,122 @@ +import { + serial, + text, + timestamp, + pgTable, + real, + pgEnum, + integer, +} from "drizzle-orm/pg-core"; +import { user } from "./users"; +import { InferModel, relations } from "drizzle-orm"; + +export const PortfolioStatus: [string, ...string[]] = [ + "scoping", + "assessment", + "tendering", + "project underway", + "completion; status: on track", + "completion; status: delayed", + "completion; status: at risk", + "completion; status: completed", + "needs review", +]; + +export const PortfolioGoal: [string, ...string[]] = [ + "Valuation Improvement", + "Increasing EPC", + "Reducing CO2 emissions", + "Energy Savings", + "None", +]; + +export const PortfolioRole: [string, ...string[]] = [ + "creator", + "admin", + "read", + "write", +]; + +export const statusEnum = pgEnum("status", PortfolioStatus); +export const goalEnum = pgEnum("goal", PortfolioGoal); +export const roleEnum = pgEnum("role", PortfolioRole); + +export const portfolio = pgTable("portfolio", { + id: serial("id").primaryKey(), + name: text("name").notNull(), + budget: real("budget"), + status: statusEnum("status").notNull(), + goal: goalEnum("goal").notNull(), + cost: real("cost"), + numberOfProperties: integer("number_of_properties"), + co2EquivalentSavings: real("co2_equivalent_savings"), // Unit is always tonnes so we don't need to store unit + energySavings: real("energy_savings"), // Unit is always kWh so we don't need to store unit + energyCostSavings: real("energy_cost_savings"), // Unit is always £ so we don't need to store unit for the moment + propertyValuationIncrease: real("property_valuation_increase"), // Unit is always £ so we don't need to store unit for the moment + rentalYieldIncrease: real("rental_yield_increase"), // Unit is always £ so we don't need to store unit for the moment + totalWorkHours: real("total_work_hours"), + createdAt: timestamp("created_at", { + precision: 6, + withTimezone: true, + }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { + precision: 6, + withTimezone: true, + }) + .defaultNow() + .notNull(), +}); + +// We have a many to many relationship between users and portfolios +// One user can have many portfolios, and one portfolio can have many users +// We use the Dizzle relational queries pattern to facilitate this + +// Define relation from users to portfolios +export const usersToPortfolioRelations = relations(user, ({ many }) => ({ + portfolios: many(portfolio), +})); + +export const portfolioUsers = pgTable("portfolioUsers", { + id: serial("id").primaryKey(), + // Define the foreign key constraints using references from Drizzle, from user_id to the users table + userId: integer("user_id") + .notNull() + .references(() => user.id), + // Define the foreign key constraints using references from Drizzle, from portfolio_id to the portfolios table + portfolioId: integer("portfolio_id") + .notNull() + .references(() => portfolio.id), + role: roleEnum("role").notNull(), + createdAt: timestamp("created_at", { + precision: 6, + withTimezone: true, + }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { + precision: 6, + withTimezone: true, + }) + .defaultNow() + .notNull(), +}); + +// Define relation from portfolios to users +export const portfolioToUsersRelations = relations(portfolio, ({ many }) => ({ + users: many(user), +})); + +// Define relation from portfolioUsers to portfolios (we can have many users to a portfolio) +export const portfolioUsersToPortfolioRelations = relations( + portfolioUsers, + ({ many }) => ({ + portfolio: many(portfolio), + }) +); + +export type Portfolio = InferModel; +export type NewPortfolio = InferModel; +export type PortfolioUsers = InferModel; +export type NewPortfolioUsers = InferModel; diff --git a/src/app/home/utils.ts b/src/app/home/utils.ts new file mode 100644 index 0000000..9c78b22 --- /dev/null +++ b/src/app/home/utils.ts @@ -0,0 +1,21 @@ +import { portfolioUsers, PortfolioUsers } from "./../db/schema/portfolio"; +import { eq } from "drizzle-orm"; +import { user } from "@/app/db/schema/users"; +import { db } from "@/app/db/db"; +import { NextRequest, NextResponse } from "next/server"; +import { portfolio } from "@/app/db/schema/portfolio"; +import type { Portfolio } from "@/app/db/schema/portfolio"; + +export default async function getPortfolios( + userId: number +): Promise { + const userPortfolios = await db + .select() + .from(portfolio) + .leftJoin(portfolioUsers, eq(portfolio.id, portfolioUsers.portfolioId)) + .where(eq(portfolioUsers.userId, userId)); + + const portfolios = userPortfolios.map((data) => data.portfolio); + + return portfolios; +}